因全表扫描效率低,高并发下数据库压力大,且无法精准控制超时时间,易漏检或重复处理,应改用延迟队列或Redis过期监听。

为什么不能用数据库定时轮询做支付超时检查
直接查表加 WHERE status = 'unpaid' AND create_time 然后批量更新,看似简单,但在高并发订单系统里会迅速暴露问题:数据库连接数暴涨、行锁竞争加剧、定时任务延迟不可控,尤其当未支付订单量达数十万时,一次扫描可能卡住几秒甚至触发死锁。RabbitMQ 的 <code>delayed message exchange 插件或 TTL + 死信队列方案,能把“时间判断”从数据库卸载到消息中间件,让超时处理变成事件驱动而非轮询驱动。
用 TTL + 死信队列实现可靠超时通知
这是 RabbitMQ 原生支持、无需额外插件的方案,关键在于利用队列级 TTL 和死信路由机制,避免消息在内存中堆积或丢失。注意不是给单条消息设 expiration(它不保证准时),而是通过队列的 x-message-ttl 和 x-dead-letter-exchange 组合来控制生命周期。
- 声明一个专用延时队列,设置
x-message-ttl=1500000(25 分钟),并绑定x-dead-letter-exchange="dlx.order"和x-dead-letter-routing-key="order.timeout" - 下单成功后,把
order_id发送到该队列(不设消息级 TTL);消息进入队列后开始倒计时 - 死信交换器
dlx.order将超时消息路由到监听队列,消费者收到后执行checkOrderPaymentStatus(orderId)并根据实际支付状态决定是否关单 - 务必关闭该延时队列的
auto-delete,否则空闲时被自动删除会导致后续消息无处投递
消费者端必须幂等且能处理重复超时消息
RabbitMQ 不保证死信只投递一次——网络抖动、消费者 crash 后重连、手动 nack 都可能导致同一条超时消息被多次投递。如果直接调用 closeOrder(orderId) 而不做校验,会出现“已支付订单被误关”的严重资损。
- 在处理前先查库:仅当
status == 'unpaid'且pay_time == null时才执行关单逻辑 - 关单操作本身用
UPDATE orders SET status = 'closed', closed_time = NOW() WHERE order_id = ? AND status = 'unpaid',依赖数据库 CAS 语义确保原子性 - 记录日志时带上
delivery_tag和当前status,便于排查重复消费源头
生产环境必须监控的三个关键指标
这套机制一旦出问题,往往表现为“大量订单莫名关单”或“超时完全不触发”,但错误日志里可能只有消费者端的空指针,根本看不出是消息没进延时队列还是死信路由失败。
立即学习“Java免费学习笔记(深入)”;
- 监控延时队列中的
messages_ready数量:持续增长说明下游消费者吞吐不足或死信路由配置错误 - 检查
dead_letter_exchange是否存在且类型为direct,并确认其绑定关系在管理后台可见(rabbitmqctl list_bindings可查) - 在消费者代码中捕获
AmqpRejectAndDontRequeueException并上报,它意味着消息因反序列化失败或业务异常被丢弃,不会进死信流程
真正难调的是 TTL 和业务超时时间的对齐——比如你设了 30 分钟队列 TTL,但用户支付回调平均耗时 28 分钟,那大量消息还没超时就被路由走了。得结合历史支付完成时间分布,把队列 TTL 设为 P99.9 支付耗时再加缓冲,而不是拍脑袋定 30 分钟。


















