连载中 10/15

订单超时与库存回补:延迟消息的闭环

2026-09-07 · 537 阅读 · 0 评论 · 0 赞

占着库存不支付的人

预扣模式让抢到的用户拿到 15 分钟支付窗口——窗口是体验,也是风险:不支付的人占着库存,回补机制缺席的话,每 10% 的弃付率就是 10% 的库存少卖,恶意占坑更是全部打了水漂。关单回补是预扣模式的另一半,没有它整个模型不成立。

延迟消息的三种实现

"15 分钟后触发关单"有三种主流做法:MQ 延迟消息——下单时发一条 15 分钟后投递的消息,精度高、实现省,首选;死信队列——消息过期后转入死信消费,档位固定但够用;定时任务扫表——按支付截止时间扫描超时订单,精度分钟级,量大时扫描压力大,适合兜底而非主力。生产组合:延迟消息为主力,扫表任务做兜底——延迟消息万一丢失,扫表最终会收网。

方案精度依赖定位
MQ 延迟消息秒级支持延迟的 MQ主力方案
死信队列固定档位RabbitMQ 等可用替代
定时扫表分钟级仅数据库兜底收网

竞态:关单时用户正在支付

关单最凶险的场景:回补已执行,用户的钱也扣了——订单关了、支付成了,钱货两空。解法是把关单做成原子条件更新:UPDATE order SET status=CLOSED WHERE id=? AND status=UNPAID——状态机 CAS,只有"未支付"能被关;支付回调与关单谁先抢到,谁决定终态:关单先成功,支付回调发现订单已关则走退款;支付先成功,关单更新影响行数为零自动放弃。两个动作共享一个状态闸门,竞态从机制上解掉

// 原子关单:只有未支付状态可关
int rows = orderMapper.closeIfUnpaid(orderId);
if (rows == 1) {
    stockCompensator.refund(orderId, skuId);   // 关单成功才回补库存
    notifyUser(orderId, CLOSED);               // 通知用户订单已关闭
} else {
    log.info("order {} not closable, paid or closed", orderId);
}
// 支付回调侧:订单已关则自动发起退款,绝不发货

回补的幂等:只能补一次

回补消息重复消费会把库存越补越多——比少卖更隐蔽的超额放行。回补幂等的关键是把"是否已回补"记录在订单侧而非凭空判断:回补前 CAS 更新回补标记(未回补→已回补),更新成功才真正加库存;落库失败则标记回滚、消息重投。同一个订单无论消息来几次,库存只加一次。凡是"加库存"的动作,都必须先过幂等闸门

回补后的库存去哪

回补的库存不是立刻重开抢——瞬时放出来又是一轮小洪峰。更稳的做法:回补库存进入候补池,按候补名单顺序通知下一批用户,或者等到下一个整点随新一批资格一起放行。库存的每一次流动都要有明确的去向与节奏,这是把秒杀从"能跑"做到"优雅"的分水岭。下一篇回到请求入口的另一道防线:幂等与一人一单

503

10 年全栈工程师 · 503咖啡馆主理人

#订单超时#库存回补#延迟消息#关单竞态#状态机

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞