定时扫描未成团订单易漏单或重复处理,因扫描间隔与成团截止时间存在竞争条件,导致误关团、超时未关团、多实例并发更新等问题。

定时任务扫描未成团订单:为什么容易漏单或重复处理
用 ScheduledThreadPoolExecutor 或 @Scheduled 每隔几秒扫一次数据库,看似简单,但实际线上常出问题。核心矛盾在于:扫描间隔和成团截止时间之间存在天然竞争条件。
常见错误现象:订单已成团但被误关团、超时未关团导致资金冻结过久、高并发下多个实例同时更新同一条订单状态。
使用场景适合低频拼团(比如日结类)、对时效容忍度高(允许±30秒误差)的业务。一旦拼团周期缩到分钟级,或 QPS 超过 50,就必须加分布式锁 + 版本号控制,否则 UPDATE order SET status = 'closed' WHERE id = ? AND status = 'pending' 这类语句会失效。
- 必须给
order表加复合索引:(status, expire_time),否则全表扫描拖垮 DB - 扫描 SQL 务必带上
expire_time 条件,不能只靠 <code>status = 'pending' - 避免在定时任务里做复杂逻辑(如发消息、调第三方),失败会导致整个扫描周期卡住
延迟队列选型:RabbitMQ TTL+DLX vs Redis ZSET 的取舍点
延迟队列不是银弹。RabbitMQ 的 TTL + DLX 方案看着标准,但实际部署中,消息堆积后延迟不准、DLX 死信转发失败静默丢弃 是高频事故。Redis ZSET 手动轮询虽然糙,反而更可控。
立即学习“Java免费学习笔记(深入)”;
使用场景上:RabbitMQ 适合已有消息中间件且拼团量稳定(日均万级以下);Redis ZSET 更适合中小团队快速落地,尤其当你们已经在用 Redis 做订单缓存。
关键参数差异:RabbitMQ TTL 是消息级,而 ZSET score 是毫秒时间戳,后者可随时修正(比如用户参团成功后从 ZSET 删除该订单 ID)。
- RabbitMQ 中必须设置
x-dead-letter-exchange和x-dead-letter-routing-key,漏配就等于没延迟 - Redis ZSET 轮询要用
ZRANGEBYSCORE orders:delay 0 (now_timestamp)+ZREM原子操作,否则会重复触发 - 别用 Redis 的
BLPOP + EXPIRE组合模拟延迟——无法精确到毫秒,且阻塞线程
订单状态机怎么设计才扛得住并发参团与自动关团冲突
拼团的核心状态流转不是“pending → success”或“pending → closed”,而是要区分“是否还有人正在提交参团请求”。否则会出现:A 用户刚点参团,B 的定时任务/延迟消息同时把团关了,A 的请求写入后变成脏数据。
真实状态至少需要五种:PENDING、LOCKED_FOR_CLOSE、SUCCESS、CLOSED、CLOSING。其中 LOCKED_FOR_CLOSE 是关键保护态,只有进入该态后,新参团请求才拒绝。
- 参团接口必须先
SELECT FOR UPDATE查团当前人数和状态,再判断是否允许加入 - 延迟任务触发关团前,要先用
UPDATE ... SET status = 'CLOSING' WHERE status = 'PENDING'尝试抢占,失败说明已有人参团成功 - 所有状态变更必须带
version字段或WHERE status IN (...),防止覆盖中间态
本地延迟队列(ScheduledFuture)为什么绝对不能用于生产环境
ScheduledThreadPoolExecutor.schedule() 返回的 ScheduledFuture 看起来能精准控制某笔订单在 10 分钟后关闭,但它完全依赖 JVM 生命周期。只要服务重启、机器宕机、K8s Pod 重建,所有待执行任务就彻底丢失——连日志都找不到痕迹。
这不是“不推荐”,是“禁止”。哪怕你加了持久化层去恢复任务,也解决不了时间精度漂移问题:schedule() 的延迟是相对调用时刻计算的,而恢复逻辑无法还原原始触发时间点。
- 开发阶段可以用它快速验证业务逻辑,但上线前必须替换为外部延迟机制
- 如果硬要用,至少得配合数据库记录原始到期时间,并在应用启动时扫描未完成任务补发——但这已经不是“本地队列”了
- 别信“用 Redisson 的
RDelayedQueue就算分布式”这种说法,它底层仍是基于 Redis ZSET,和自己手写没本质区别
真正难的不是选哪种技术,而是想清楚:关团动作是否允许少量误差?订单状态能否回滚?下游系统(比如支付、库存)是否支持幂等重试?这些比代码里多写一个 @Scheduled 或少建一个 Exchange 重要得多。










