·更新于 2026年10月25日· 14 次浏览 Node.jsRedis支付系统微信小程序MongoDB工程实践BUG解决
Node.js 支付订单超时关单:我把关单放进 Redis 延迟队列,先查再关
支付超时最怕的不是没付,而是刚关单又收到支付成功。我用 Redis 延迟队列做到期提醒,关单前先校验支付状态,把竞态变成可控流程。

作者
ZHOUYI
做小程序支付后,我最怕的不是支付失败,而是「刚好超时」。用户网络慢,第 5 分钟才付成功;系统第 5 分钟触发关单。两边都觉得自己有理,订单状态就容易错乱。以前我想用 setTimeout,后来发现它更适合演示,不太适合生产。
为什么 setTimeout 不够稳
- 进程重启后,内存里的定时器会丢,订单可能一直挂着。
- 多实例部署时,同一个订单可能被多个节点重复关。
- 支付回调可能晚到几秒,关单前如果不查事实,容易误杀。
我的做法:延迟队列只负责提醒
下单时,我会给订单写一个 expireAt,比如 10 分钟可支付窗口,同时把 orderId 按到期时间塞进 Redis 的有序集合。到点后,消费者不是直接关单,而是把它当成一次提醒:先看订单当前状态,再查支付渠道是否已经支付。只有确认仍未支付,才把订单流转到已关闭。
js
await redis.zadd('pay:close:delay', expireAt, orderId);
const due = await redis.zrangebyscore('pay:close:delay', 0, Date.now(), 'LIMIT', 0, 20);
for (const orderId of due) {
const removed = await redis.zrem('pay:close:delay', orderId);
if (!removed) continue;
const order = await Order.findOne({ orderId });
if (!order || order.status !== 'pending') continue;
const paid = await queryPay(order); // 以官方文档为准
if (paid) await markPaid(order); else await closeOrder(order);
}关单和回调都要走条件更新
我会在 MongoDB 里用条件更新兜住竞态:关单只允许从 pending 变 closed,支付成功只允许从 pending 变 paid。谁先改成功,谁就占住状态。这样即使延迟任务和回调同时到达,也不会互相覆盖。对支付结果不确定时,我会主动查询支付接口,具体字段和频率以官方文档为准。
- 消费前先 zrem 抢占,避免重复消费。
- 不要只信本地回调,晚到回调要靠主动查询补齐。
- 订单金额、商品快照要不可变,关单前也顺便核对。
- 每天保留对账任务,延迟队列只是缩短风险窗口。
“延迟队列不是裁判,它只是闹钟。真正能改订单状态的,是支付事实和状态机。”
这套方案上线后,「刚超时又支付成功」的客诉明显少了。它没有消灭所有异常,但把问题从随机撞车变成了可追踪、可补偿的流程。对支付这种钱相关的链路,我宁愿多查一次,也不想赌运气。