SpringBoot+SSM构建高并发外卖系统架构实战

发布时间:2026/8/4 4:01:45
SpringBoot+SSM构建高并发外卖系统架构实战 1. 项目概述从零构建一个高可用的外卖系统2018年我在杭州某餐饮科技公司担任技术负责人时曾主导开发过一个日订单量超5万的外卖平台。这个基于Java技术栈的系统让我深刻体会到一个看似简单的外卖业务背后隐藏着复杂的系统架构设计挑战。今天我就来拆解如何用SpringBootSSM搭建一个完整的外卖系统这套架构经过我们线上环境验证能稳定支撑高并发场景。这个系统包含商家端、用户端、骑手端和管理后台四个核心模块。商家端负责商品管理、订单处理和营业统计用户端实现浏览、下单、支付全流程骑手端处理接单配送管理后台则进行全局监控和数据分析。整套系统采用微服务架构各模块通过RESTful API通信数据库选用MySQL集群配合Redis缓存实测QPS可达3000。2. 技术选型与架构设计2.1 为什么选择SpringBootSSM组合在技术选型阶段我们对比了多种方案最终确定这个技术栈SpringBoot 2.7简化配置内置Tomcat快速启动项目MyBatis 3.5灵活SQL管理配合PageHelper实现高效分页Spring MVC成熟的Web层框架RESTful接口开发利器Redis 6缓存热点数据如商家信息、菜品库存RabbitMQ 3.9异步处理订单状态变更和消息通知关键决策放弃使用SpringCloud而采用轻量级架构主要考虑中小型外卖平台并不需要复杂的服务治理SpringBoot的简单微服务已经足够。但我们在代码结构上预留了服务拆分的可能性。2.2 数据库设计要点外卖系统的数据库设计有几个特殊考量点CREATE TABLE order ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, shop_id bigint NOT NULL COMMENT 商家ID, rider_id bigint DEFAULT NULL COMMENT 骑手ID, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2接单 3配送中 4已完成 5已取消, actual_amount decimal(10,2) NOT NULL COMMENT 实付金额, address_json json NOT NULL COMMENT 收货地址快照, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_shop_status (shop_id,status), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;特别注意地址信息使用JSON字段存储快照避免用户修改地址影响历史订单订单状态设计要预留中间状态如商家接单中、骑手已取餐金额字段必须用DECIMAL类型避免浮点数精度问题3. 核心功能实现细节3.1 高并发下单流程实现外卖系统的下单接口是核心中的核心我们的实现方案Transactional public OrderDTO createOrder(OrderCreateVO vo) { // 1. 校验库存Redis原子操作 Long remain redisTemplate.opsForValue() .decrement(food:stock: vo.getFoodId(), vo.getCount()); if (remain 0) { throw new BusinessException(库存不足); } // 2. 生成订单号雪花算法 String orderNo IdWorker.get32UUID(); // 3. 创建订单主表 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(UserContext.getCurrentUserId()); // ...其他字段设置 orderMapper.insert(order); // 4. 异步扣减真实库存 rabbitTemplate.convertAndSend( order.exchange, stock.decrease, new StockMessage(order.getId(), vo.getFoodId(), vo.getCount())); return convertToDTO(order); }关键优化点使用Redis预扣库存避免超卖订单号采用特殊规则日期(8)用户ID后4位随机数(6)数据库操作和Redis操作要在同一个Transactional中真实库存扣减通过消息队列异步处理3.2 智能配送调度算法骑手调度是外卖系统的技术难点我们的解决方案基于GeoHash计算商家与用户的位置关系考虑骑手当前位置、已有订单量、交通工具类型采用加权评分算法def calculate_score(rider, shop, user): distance_score 1 / (get_distance(rider, shop) 0.1) load_score 1 - rider.current_orders / rider.max_orders vehicle_score 1 if rider.vehicle motor else 0.8 return distance_score * 0.6 load_score * 0.3 vehicle_score * 0.1每5分钟重新计算一次骑手负载情况4. 性能优化实战经验4.1 缓存设计策略我们采用多级缓存架构本地缓存使用Caffeine缓存商家基础信息TTL 5分钟分布式缓存Redis缓存菜品信息、库存数据缓存穿透防护对空结果也进行缓存设置较短TTL缓存更新策略主动更新商家修改商品时立即失效缓存被动更新设置默认TTL 30分钟4.2 数据库分库分表当订单表超过500万行时我们实施了分库分表按商家ID哈希分库4个库按订单创建时间范围分表每月一张表使用ShardingSphere实现路由配置示例spring: shardingsphere: datasource: names: ds0,ds1,ds2,ds3 sharding: tables: order: actual-data-nodes: ds$-{0..3}.order_$-{202301..202312} database-strategy: inline: algorithm-expression: ds$-{shop_id % 4} sharding-column: shop_id table-strategy: standard: precise-algorithm-class-name: com.xxx.OrderMonthPreciseAlgorithm range-algorithm-class-name: com.xxx.OrderMonthRangeAlgorithm sharding-column: create_time5. 典型问题排查实录5.1 订单状态同步延迟现象用户支付成功后商家端显示待支付 排查过程检查MQ消息堆积情况发现order_status队列有积压查看消费者日志大量DuplicateKeyException分析原因消息重试导致状态更新冲突 解决方案RabbitListener(queues order_status) public void handleStatusUpdate(OrderStatusMsg msg) { // 使用CAS方式更新状态 int updated orderMapper.updateStatus( msg.getOrderId(), msg.getOldStatus(), msg.getNewStatus()); if (updated 0) { log.warn(状态更新冲突orderId{}, msg.getOrderId()); } }5.2 高峰期数据库CPU飙升现象每日11:00-13:00数据库CPU达到90% 优化措施慢查询分析发现订单查询缺少复合索引添加索引(shop_id, status, create_time)查询改造-- 优化前 SELECT * FROM order WHERE shop_id ? AND status ? ORDER BY create_time DESC LIMIT 100; -- 优化后 SELECT * FROM order WHERE shop_id ? AND status ? ORDER BY create_time DESC, id DESC LIMIT 100;引入读写分离6. 安全防护方案外卖系统面临的主要安全风险刷单风险同一设备/账号频繁下单解决方案基于设备指纹的风控系统支付篡改修改前端传递的支付金额解决方案后端重新计算金额并签名SQL注入虽然使用MyBatis但仍需防范解决方案定期SQL扫描 参数化查询校验关键代码示例// 支付金额校验 public void checkAmount(Long orderId, BigDecimal payAmount) { Order order orderMapper.selectById(orderId); BigDecimal actualAmount order.getActualAmount(); if (payAmount.compareTo(actualAmount) ! 0) { throw new SecurityException(支付金额异常); } // 签名验证 String sign DigestUtils.md5Hex( orderId actualAmount.toString() SECRET_KEY); if (!sign.equals(request.getSign())) { throw new SecurityException(签名验证失败); } }这套外卖系统架构已经在多个城市落地峰值时处理过每分钟2000订单。最大的经验就是前期要做好扩展性设计特别是订单表和配送系统的设计要预留足够灵活性。我们在2.0版本引入了GeoHash优化配送路线使得平均配送时间缩短了18%。