1. 项目背景与核心需求
苍穹外卖系统作为典型的O2O餐饮服务平台,订单状态的实时性和可靠性直接影响用户体验与商家运营效率。在实际业务中,存在两类关键时序问题:
- 状态滞后问题:用户下单后若长时间未支付,系统需自动释放库存;骑手接单后若超时未完成,需触发超时处理
- 实时通知缺口:商家端需要即时感知新订单到达,用户需要了解订单状态变更(如骑手接单、送达)
传统轮询方案会导致:
- 服务端资源浪费(无效查询)
- 状态更新延迟(轮询间隔)
- 移动端电量消耗加剧
2. 技术方案设计
2.1 状态处理架构
graph TD A[订单创建] --> B{支付状态} B -->|未支付| C[Spring Task定时扫描] B -->|已支付| D[状态机推进] C --> E[15分钟超时关闭] D --> F{配送状态} F -->|接单超时| G[智能转单]2.2 实时通知方案对比
| 方案 | 延迟 | 开销 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 高 | 大 | 最好 | 简单场景 |
| 长轮询 | 中 | 中 | 好 | 兼容性要求高 |
| Server-Sent Events | 低 | 小 | 较好 | 单向通知 |
| WebSocket | 最低 | 最小 | 需适配 | 双向实时交互 |
最终采用混合方案:
- 状态变更:WebSocket全双工通道
- 历史记录:HTTP API补拉
3. Spring Task实现细节
3.1 定时任务配置
@Configuration @EnableScheduling public class OrderTaskConfig { @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix("order-task-"); scheduler.setWaitForTasksToCompleteOnShutdown(true); scheduler.setAwaitTerminationSeconds(60); return scheduler; } }3.2 状态扫描策略
@Slf4j @Component public class OrderStatusTask { @Autowired private OrderMapper orderMapper; // 每5分钟扫描超时未支付订单 @Scheduled(cron = "0 */5 * * * ?") public void checkUnpaidOrders() { List<Order> unpaidOrders = orderMapper.selectUnpaidOrders(15); unpaidOrders.forEach(order -> { order.setStatus(OrderStatus.CLOSED); order.setCloseReason("超时未支付"); orderMapper.updateById(order); log.info("关闭超时订单:{}", order.getId()); // 库存释放逻辑 releaseStock(order); }); } // 智能转单算法 @Scheduled(fixedRate = 300000) public void reassignTimeoutOrders() { List<Order> timeoutOrders = orderMapper.selectDeliveryTimeoutOrders(30); timeoutOrders.forEach(this::smartReassign); } }关键参数说明:
- 支付超时:15分钟(餐饮行业黄金支付窗口期)
- 转单阈值:30分钟(含骑手到店+取餐时间)
- 扫描频率:5分钟(平衡实时性与系统负载)
4. WebSocket集成实践
4.1 服务端实现
@ServerEndpoint("/ws/order/{shopId}") @Component public class OrderWebSocketEndpoint { private static final ConcurrentMap<Long, Session> sessions = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("shopId") Long shopId) { sessions.put(shopId, session); log.info("商家{}连接建立", shopId); } @OnMessage public void onMessage(String message, Session session) { // 处理客户端心跳等消息 } @OnClose public void onClose(@PathParam("shopId") Long shopId) { sessions.remove(shopId); log.info("商家{}连接关闭", shopId); } public static void sendNewOrderNotice(Long shopId, Order order) { Session session = sessions.get(shopId); if (session != null && session.isOpen()) { try { String json = JSON.toJSONString( new OrderNoticeDTO(order.getId(), order.getCreateTime()) ); session.getBasicRemote().sendText(json); } catch (IOException e) { log.error("消息发送失败", e); } } } }4.2 前端连接管理
class OrderSocket { constructor(shopId) { this.socket = new WebSocket(`wss://api.example.com/ws/order/${shopId}`); this.heartbeatInterval = 30000; this.socket.onopen = () => { console.log('WebSocket连接已建立'); this.startHeartbeat(); }; this.socket.onmessage = (event) => { const notice = JSON.parse(event.data); if (notice.type === 'NEW_ORDER') { this.showOrderAlert(notice); } }; } startHeartbeat() { this.heartbeatTimer = setInterval(() => { this.socket.send(JSON.stringify({type: 'PING'})); }, this.heartbeatInterval); } }5. 生产环境优化方案
5.1 WebSocket集群方案
graph LR A[客户端] --> B[NGINX] B --> C[WS节点1] B --> D[WS节点2] C & D --> E[Redis Pub/Sub] E --> F[业务服务]实现要点:
- Nginx配置负载均衡
upstream ws_cluster { ip_hash; server ws1.example.com:8080; server ws2.example.com:8080; }- 使用Redis广播消息
// 订单创建后发布事件 redisTemplate.convertAndSend("order.new", orderId); // 订阅处理 @RedisListener(channel = "order.new") public void handleNewOrder(Long orderId) { Order order = orderService.getById(orderId); OrderWebSocketEndpoint.sendNewOrderNotice(order.getShopId(), order); }5.2 断线重连策略
class ReconnectableSocket extends OrderSocket { constructor(shopId) { super(shopId); this.maxRetries = 5; this.retryCount = 0; this.reconnectDelay = 1000; this.socket.onclose = () => { if (this.retryCount < this.maxRetries) { setTimeout(() => { this.retryCount++; this.reconnectDelay *= 2; new ReconnectableSocket(shopId); }, this.reconnectDelay); } }; } }6. 性能监控指标
| 指标名称 | 采集方式 | 预警阈值 | 处理方案 |
|---|---|---|---|
| WS连接数 | Prometheus+Grafana | > 5000/节点 | 水平扩容 |
| 消息积压量 | Redis队列监控 | > 1000 | 增加消费者 |
| 任务执行耗时 | Spring Actuator | > 3000ms | 优化SQL/拆分任务 |
| 订单状态同步延迟 | 日志时间戳比对 | > 10s | 检查MQ消费者状态 |
7. 典型问题排查
7.1 消息丢失场景
现象:商家端偶发未收到新订单提醒
排查步骤:
- 检查WebSocket连接状态
# 查看活跃连接 netstat -anp | grep 8080 - 验证Redis消息投递
redis-cli monitor | grep order.new - 检查消费者线程堆栈
Thread.getAllStackTraces().keySet().forEach(t -> { if (t.getName().contains("Order")) { System.out.println(t.getName()); } });
解决方案:
- 增加消息确认机制
- 添加补偿消息队列
7.2 定时任务堆积
现象:夜间批量订单处理延迟
优化方案:
// 动态调整线程池 @Scheduled(fixedDelay = 60000) public void adjustThreadPool() { int pendingTasks = taskScheduler.getActiveCount(); if (pendingTasks > 100) { taskScheduler.setPoolSize( Math.min(20, taskScheduler.getPoolSize() + 2) ); } }8. 扩展实践建议
智能调度优化:
# 使用遗传算法优化骑手分配 def fitness_function(assignment): return sum( calculate_distance(rider, restaurant) for rider, restaurant in assignment )多通道通知降级:
// 通知失败时自动降级 public void notifyMerchant(Long shopId, Order order) { try { OrderWebSocketEndpoint.sendNewOrderNotice(shopId, order); } catch (Exception e) { smsService.send(shop.getPhone(), "新订单到达"); } }状态变更溯源:
CREATE TABLE order_status_log ( id BIGINT PRIMARY KEY, order_id BIGINT, prev_status VARCHAR(20), current_status VARCHAR(20), operator VARCHAR(50), created_at TIMESTAMP );
实际部署中,建议通过灰度发布验证定时任务参数,采用渐进式调整策略。某次生产环境数据显示,将支付超时从30分钟调整为15分钟后,系统库存周转率提升22%,而订单取消率仅上升3%,达到业务优化目标。