1. 项目概述:从零构建一个高可用的外卖系统
去年我接手了一个餐饮连锁品牌的外卖系统升级项目,客户原有的系统已经无法支撑日均5000+订单的业务量。经过技术选型,我们最终采用Java+SpringBoot+SSM这套技术栈,仅用三个月就完成了系统重构。现在这个系统已经稳定运行一年多,日均处理订单超过2万单。本文将分享这套外卖系统的完整实现方案,包括技术架构设计、核心功能实现和实际部署中的经验教训。
这个外卖系统包含商家端、用户端和配送端三个核心模块,采用前后端分离架构。商家端主要处理菜单管理、订单处理和营业统计;用户端实现浏览、下单和支付功能;配送端则负责订单分配和骑手管理。系统采用微服务架构,各个模块可以独立部署和扩展,特别适合快速发展的餐饮业务。
提示:在开始开发前,建议先绘制完整的业务流程图和数据ER图,这能避免后期大量的接口返工。我们最初就因为没有明确配送状态流转逻辑,导致后来不得不重构整个订单状态机。
2. 技术栈选型与架构设计
2.1 为什么选择Java+SpringBoot+SSM组合
在技术选型阶段,我们对比了多种方案,最终确定使用:
- Java 11作为基础语言
- SpringBoot 2.7作为框架基础
- MyBatis作为ORM框架
- Redis作为缓存
- MySQL 8.0作为主数据库
这套组合有以下几个显著优势:
- 开发效率高:SpringBoot的自动配置和起步依赖让项目搭建变得极其简单,原本需要数天的环境配置现在只需几分钟
- 性能稳定:Java的强类型和JVM优化保证了系统在高并发下的稳定性
- 生态完善:SSM框架有丰富的中间件支持,如Redis、RabbitMQ等
- 易于维护:清晰的MVC分层让后续维护和迭代更加方便
2.2 系统架构设计
我们采用分层架构设计,整体分为:
- 表现层:使用Thymeleaf模板引擎+HTML5,适配PC和移动端
- 业务层:SpringBoot处理核心业务逻辑
- 数据访问层:MyBatis实现数据库操作
- 存储层:MySQL主从复制+Redis缓存
// 典型的控制器层代码示例 @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public Result createOrder(@RequestBody OrderDTO orderDTO) { return orderService.createOrder(orderDTO); } }数据库设计方面,核心表包括:
- 用户表(user)
- 商家表(merchant)
- 商品表(product)
- 订单表(order)
- 订单明细表(order_detail)
- 配送表(delivery)
注意:订单表一定要设计好索引,我们最初因为缺少合适的索引,在订单量达到1万时查询性能急剧下降。后来添加了(user_id, create_time)的联合索引才解决问题。
3. 核心功能实现细节
3.1 用户端关键功能实现
3.1.1 智能推荐与搜索功能
外卖系统的搜索功能不能简单使用LIKE查询,我们采用Elasticsearch实现商品搜索,配合用户历史行为数据实现个性化推荐。
// 搜索服务实现示例 @Service public class SearchServiceImpl implements SearchService { @Autowired private RestHighLevelClient esClient; public List<Product> searchProducts(String keyword, Long userId) { // 构建复合查询:基础关键词+用户偏好 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("name", keyword)) .should(QueryBuilders.termQuery("tags", getUserPreference(userId))); SearchRequest request = new SearchRequest("products") .source(new SearchSourceBuilder().query(boolQuery)); // 执行查询并处理结果 // ... } }3.1.2 购物车与下单流程
购物车设计需要考虑并发问题,我们采用Redis存储临时购物车数据,下单时再持久化到MySQL。
// 购物车服务关键代码 @Service public class CartServiceImpl implements CartService { @Autowired private RedisTemplate<String, Object> redisTemplate; public void addToCart(Long userId, Long productId, Integer quantity) { String key = "cart:" + userId; redisTemplate.opsForHash().put(key, productId.toString(), quantity); redisTemplate.expire(key, 7, TimeUnit.DAYS); } public void checkout(Long userId) { // 1. 从Redis获取购物车 Map<Object, Object> cart = redisTemplate.opsForHash() .entries("cart:" + userId); // 2. 创建订单 Order order = createOrder(userId, cart); // 3. 清空购物车 redisTemplate.delete("cart:" + userId); } }3.2 商家端功能实现
3.2.1 菜单管理与库存控制
商家需要实时更新菜单和库存,我们使用WebSocket实现变更实时推送。
@Controller public class MenuController { @Autowired private SimpMessagingTemplate messagingTemplate; @PostMapping("/menu") public String updateMenu(@RequestBody MenuDTO menuDTO) { // 更新数据库 menuService.updateMenu(menuDTO); // 通知所有连接的客户端 messagingTemplate.convertAndSend("/topic/menuUpdate", menuDTO); return "success"; } }3.2.2 订单处理与打印
商家端需要处理大量并发订单,我们采用线程池+队列的方式平滑处理订单高峰。
// 订单处理服务 @Service public class OrderProcessService { private ExecutorService executor = Executors.newFixedThreadPool(10); private BlockingQueue<Order> queue = new LinkedBlockingQueue<>(1000); @PostConstruct public void init() { // 启动10个消费者线程 for (int i = 0; i < 10; i++) { executor.submit(this::processOrder); } } private void processOrder() { while (true) { try { Order order = queue.take(); // 处理订单逻辑 // 1. 更新数据库状态 // 2. 通知厨房打印 // 3. 通知配送系统 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void submitOrder(Order order) { if (!queue.offer(order)) { throw new RuntimeException("系统繁忙,请稍后再试"); } } }3.3 配送系统实现
3.3.1 骑手调度算法
我们实现了基于地理位置的最优骑手分配算法:
// 骑手调度服务 @Service public class DispatchService { @Autowired private RedisTemplate<String, Object> redisTemplate; public Rider dispatchOrder(Long orderId, Location restaurantLoc) { // 1. 获取3公里内的在线骑手 Set<Object> riderIds = redisTemplate.opsForGeo() .radius("riders", restaurantLoc.getLng(), restaurantLoc.getLat(), 3, Metrics.KILOMETERS) .getMembers(); // 2. 选择最近且订单最少的骑手 return selectBestRider(riderIds); } }3.3.2 实时轨迹追踪
使用WebSocket+高德地图API实现骑手实时位置更新:
@Controller public class LocationController { @Autowired private SimpMessagingTemplate messagingTemplate; @PostMapping("/location") public void updateLocation(@RequestBody LocationDTO locationDTO) { // 更新Redis中的位置 redisTemplate.opsForGeo().add("riders", new Point(locationDTO.getLng(), locationDTO.getLat()), locationDTO.getRiderId()); // 广播给相关用户 messagingTemplate.convertAndSendToUser( locationDTO.getOrderId().toString(), "/queue/location", locationDTO); } }4. 性能优化与安全实践
4.1 高并发优化策略
- 缓存策略:
- 使用Redis缓存热门商家和商品
- 采用多级缓存:本地缓存(Caffeine)+分布式缓存(Redis)
// 多级缓存实现示例 @Service public class ProductServiceImpl implements ProductService { @Autowired private RedisTemplate<String, Object> redisTemplate; private Cache<Long, Product> localCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public Product getProduct(Long id) { // 1. 检查本地缓存 Product product = localCache.getIfPresent(id); if (product != null) return product; // 2. 检查Redis product = (Product)redisTemplate.opsForValue().get("product:"+id); if (product != null) { localCache.put(id, product); return product; } // 3. 查询数据库 product = productMapper.selectById(id); if (product != null) { redisTemplate.opsForValue().set("product:"+id, product, 1, TimeUnit.HOURS); localCache.put(id, product); } return product; } }- 数据库优化:
- 主从分离:写操作走主库,读操作走从库
- 分库分表:订单表按月分表,商家数据按地区分库
4.2 安全防护措施
- 支付安全:
- 使用HTTPS传输敏感数据
- 支付接口实现防重放攻击机制
- 金额校验在前后端都要进行
// 支付接口安全示例 @RestController @RequestMapping("/payment") public class PaymentController { @PostMapping public Result pay(@RequestBody PaymentDTO paymentDTO, HttpServletRequest request) { // 1. 验证签名 if (!verifySign(paymentDTO, request)) { throw new SecurityException("非法请求"); } // 2. 防重放检查 if (redisTemplate.hasKey("payment:"+paymentDTO.getNonce())) { throw new SecurityException("请勿重复提交"); } redisTemplate.opsForValue().set("payment:"+paymentDTO.getNonce(), "1", 30, TimeUnit.MINUTES); // 3. 执行支付逻辑 return paymentService.processPayment(paymentDTO); } }- API防护:
- 使用Spring Security实现权限控制
- 敏感操作添加二次验证
- 接口限流防止恶意刷单
5. 部署与监控方案
5.1 容器化部署
我们使用Docker+Jenkins实现CI/CD流水线:
# Dockerfile示例 FROM openjdk:11-jre WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","app.jar"]Jenkins流水线关键步骤:
- 代码检出
- Maven构建
- 单元测试
- Docker镜像构建
- 部署到K8s集群
5.2 监控与告警
应用监控:
- SpringBoot Actuator暴露健康指标
- Prometheus收集指标数据
- Grafana展示监控仪表盘
业务监控:
- 订单异常监控
- 支付成功率监控
- 配送超时监控
# Prometheus配置示例 scrape_configs: - job_name: 'takeaway' metrics_path: '/actuator/prometheus' static_configs: - targets: ['app1:8080', 'app2:8080']6. 典型问题与解决方案
6.1 订单超卖问题
问题现象:热门商品在促销时出现超卖
解决方案:使用Redis分布式锁+乐观锁
// 库存扣减解决方案 @Service public class InventoryServiceImpl implements InventoryService { @Autowired private RedissonClient redissonClient; public boolean deductInventory(Long productId, int quantity) { RLock lock = redissonClient.getLock("inventory_lock:" + productId); try { lock.lock(3, TimeUnit.SECONDS); Product product = productMapper.selectById(productId); if (product.getStock() >= quantity) { product.setStock(product.getStock() - quantity); int rows = productMapper.updateById(product); return rows > 0; } return false; } finally { lock.unlock(); } } }6.2 高峰期系统响应慢
问题现象:中午12点系统响应时间明显变长
优化措施:
- 增加自动扩容机制
- 非核心功能降级处理
- 静态资源CDN加速
- 数据库查询优化
6.3 分布式事务问题
问题场景:下单需要同时操作订单服务、库存服务和支付服务
解决方案:采用Seata实现分布式事务
// 分布式事务示例 @GlobalTransactional public Result createOrder(OrderDTO orderDTO) { // 1. 扣减库存 inventoryService.deduct(orderDTO.getItems()); // 2. 创建订单 Order order = orderService.create(orderDTO); // 3. 发起支付 paymentService.createPayment(order); return Result.success(order); }7. 项目演进与扩展方向
在实际运营过程中,我们陆续添加了以下增强功能:
- 智能调度系统:基于历史数据预测订单高峰,提前调配骑手资源
- 会员成长体系:增加用户粘性和复购率
- 营销活动平台:支持多种促销活动配置
- 数据分析看板:帮助商家优化经营策略
未来可能的扩展方向:
- 接入智能语音点餐
- 实现无人配送对接
- 增加供应链管理系统
- 开发小程序端应用
这套系统经过多个项目的验证,已经形成了一套成熟的实现方案。在开发过程中,最重要的经验是前期要做好充分的需求分析和架构设计,特别是订单状态流转和分布式事务处理这些核心环节。我们在第二个项目迭代时重写了80%的订单相关代码,就是因为初期设计考虑不够全面。