1. 项目背景与核心需求
在餐饮外卖系统中,订单状态流转的实时性和准确性直接影响用户体验和商家运营效率。传统轮询方式不仅浪费服务器资源,还难以保证时效性。我们基于Spring Cloud架构开发的"苍穹外卖"系统,需要解决以下三个核心问题:
- 订单超时自动取消(15分钟未支付)
- 新订单实时推送商家端
- 用户催单提醒及时处理
2. 技术方案选型
2.1 定时任务实现对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Spring Scheduled | 简单易用,零配置 | 单机运行,不支持动态调整 | 小型单体应用 |
| Quartz | 分布式支持,动态调度 | 配置复杂,需要额外存储 | 企业级复杂调度 |
| XXL-JOB | 可视化管控,弹性扩容 | 需要独立部署调度中心 | 微服务架构 |
| Elastic-Job | 分布式协调,故障转移 | 依赖Zookeeper,学习成本高 | 高可用要求场景 |
最终选择Quartz方案,因其:
- 与Spring Boot生态无缝集成
- 支持集群环境下避免重复执行
- 可通过数据库持久化任务状态
- 动态调整cron表达式无需重启
2.2 实时通知方案
WebSocket相比传统轮询的优势:
- 全双工通信,服务端可主动推送
- 单个TCP连接持续复用
- 低延迟(毫秒级响应)
- 减少无效请求(节约70%+带宽)
3. 核心实现细节
3.1 Quartz集群配置
# application-quartz.yml spring: quartz: job-store-type: jdbc jdbc: initialize-schema: never properties: org.quartz.scheduler.instanceName: ClusterQuartzScheduler org.quartz.scheduler.instanceId: AUTO org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.tablePrefix: QRTZ_ org.quartz.jobStore.isClustered: true org.quartz.jobStore.clusterCheckinInterval: 20000 org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount: 10关键配置说明:
- clusterCheckinInterval:节点心跳间隔(ms)
- isClustered=true 启用集群模式
- tablePrefix 避免与业务表冲突
3.2 订单状态定时任务
@Slf4j public class OrderStatusJob implements Job { @Override public void execute(JobExecutionContext context) { List<Order> unpaidOrders = orderMapper.selectUnpaidOrders(15); unpaidOrders.forEach(order -> { order.setStatus(OrderStatus.CANCELLED); order.setCancelReason("超时未支付"); orderMapper.updateById(order); // 释放库存 inventoryService.unlock(order.getItems()); }); } }执行策略:
- cron表达式:
0 */1 * * * ?每分钟扫描 - 幂等设计:通过version字段乐观锁控制
- 性能优化:分批处理(每批100条)
3.3 WebSocket消息推送
@ServerEndpoint("/ws/notification/{shopId}") @Component public class OrderWebSocket { private static final Map<Long, Session> sessions = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("shopId") Long shopId) { sessions.put(shopId, session); } public static void sendNewOrder(Long shopId, OrderVO order) { Session session = sessions.get(shopId); if(session != null) { session.getAsyncRemote().sendText(JSON.toJSONString(order)); } } }消息协议设计:
{ "type": "NEW_ORDER|REMINDER", "data": { "orderId": "202308011001", "createTime": "2023-08-01 10:00:00", "totalAmount": 38.50 } }4. 生产环境问题排查
4.1 定时任务常见故障
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 任务重复执行 | 集群节点时间不同步 | 部署NTP时间同步服务 |
| 任务堆积 | 单线程阻塞 | 增加线程池大小 |
| 数据库连接泄漏 | Job中未关闭数据库连接 | 使用try-with-resources |
| 错过触发时间 | 服务器重启未持久化 | 配置持久化存储 |
4.2 WebSocket优化实践
- 心跳检测机制:
// 客户端每30秒发送ping setInterval(() => { ws.send('{"type":"ping"}'); }, 30000); // 服务端配置 @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxSessionIdleTimeout(60000L); return container; }- 断线重连策略:
let reconnectAttempts = 0; function connect() { ws = new WebSocket(url); ws.onclose = function() { let timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); setTimeout(connect, timeout); reconnectAttempts++; }; }5. 性能压测数据
5.1 Quartz集群表现
| 节点数 | QPS | 平均延迟 | CPU使用率 |
|---|---|---|---|
| 1 | 1200 | 45ms | 68% |
| 2 | 2300 | 51ms | 72% |
| 3 | 3400 | 53ms | 75% |
5.2 WebSocket连接容量
| 连接数 | 内存占用 | 消息延迟 | 带宽消耗 |
|---|---|---|---|
| 1000 | 1.2GB | 12ms | 3Mbps |
| 5000 | 4.8GB | 18ms | 15Mbps |
| 10000 | 9.5GB | 25ms | 30Mbps |
优化建议:
- 超过5000连接时考虑分片部署
- 使用Protobuf替代JSON可减少40%带宽
- 开启GZIP压缩进一步降低传输量
6. 扩展设计思考
6.1 状态机模式优化订单流转
public enum OrderState { INIT { @Override public void pay(Order order) { if(validatePayment(order)) { order.setState(PAID); } } }, PAID { @Override public void deliver(Order order) { order.setState(DELIVERING); } }; // 其他状态和方法... }优势:
- 集中管理状态转换逻辑
- 避免if-else分支污染业务代码
- 新增状态只需扩展枚举
6.2 分布式事务补偿方案
对于库存扣减与订单创建的一致性:
- TCC模式实现:
@Transactional public void tryCreateOrder(OrderDTO dto) { // 1. 冻结库存 inventoryService.freeze(dto.getItems()); // 2. 创建待支付订单 orderMapper.insert(dto.toOrder()); } @Transactional public void confirmCreateOrder(Long orderId) { Order order = getById(orderId); order.setStatus(PAID); updateById(order); } @Transactional public void cancelCreateOrder(Long orderId) { Order order = getById(orderId); inventoryService.unfreeze(order.getItems()); deleteById(orderId); }- 定时任务兜底:
-- 每小时扫描悬挂事务 SELECT * FROM t_order WHERE status = 'TRY' AND create_time < NOW() - INTERVAL 1 HOUR;7. 监控与告警配置
7.1 Prometheus监控指标
# application.yml management: endpoints: web: exposure: include: health,info,metrics,prometheus metrics: tags: application: ${spring.application.name}关键指标:
quartz_jobs_executed_total任务执行次数websocket_connections_active当前连接数order_status_changes_total状态变更统计
7.2 Grafana看板配置
-- 订单状态分布 SELECT status, COUNT(*) as count FROM t_order GROUP BY status; -- WebSocket消息速率 SELECT rate(websocket_messages_sent_total[1m]) FROM metrics;告警规则示例:
alert: HighOrderCancelRate expr: rate(order_status_changes_total{status="cancelled"}[5m]) > 0.1 for: 10m labels: severity: warning annotations: summary: "高订单取消率 ({{ $value }})"8. 实际部署经验
- Quartz集群注意事项:
- 各节点必须配置相同的时间源
- 数据库连接池建议使用HikariCP
- 避免在Job中执行长时间阻塞操作
- WebSocket生产建议:
- Nginx配置需要添加:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400s;- 对于移动端,建议实现退后台重连机制
- 重要消息需要添加客户端ACK确认
- 订单表设计优化:
CREATE TABLE t_order ( id BIGINT PRIMARY KEY, status ENUM('INIT','PAID','DELIVERING','COMPLETED','CANCELLED'), user_id BIGINT, shop_id BIGINT, amount DECIMAL(10,2), INDEX idx_shop_status (shop_id, status), INDEX idx_user_createtime (user_id, create_time) ) ENGINE=InnoDB;