·更新于 2026年10月25日· 14 次浏览 Node.jsRedis支付系统微信小程序MongoDB工程实践BUG解决

Node.js 支付订单超时关单:我把关单放进 Redis 延迟队列,先查再关

支付超时最怕的不是没付,而是刚关单又收到支付成功。我用 Redis 延迟队列做到期提醒,关单前先校验支付状态,把竞态变成可控流程。

Node.js 支付订单超时关单:我把关单放进 Redis 延迟队列,先查再关 封面
ZHOUYI

作者

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。谁先改成功,谁就占住状态。这样即使延迟任务和回调同时到达,也不会互相覆盖。对支付结果不确定时,我会主动查询支付接口,具体字段和频率以官方文档为准。

  1. 消费前先 zrem 抢占,避免重复消费。
  2. 不要只信本地回调,晚到回调要靠主动查询补齐。
  3. 订单金额、商品快照要不可变,关单前也顺便核对。
  4. 每天保留对账任务,延迟队列只是缩短风险窗口。

“延迟队列不是裁判,它只是闹钟。真正能改订单状态的,是支付事实和状态机。”


这套方案上线后,「刚超时又支付成功」的客诉明显少了。它没有消灭所有异常,但把问题从随机撞车变成了可追踪、可补偿的流程。对支付这种钱相关的链路,我宁愿多查一次,也不想赌运气。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?