连载中 10/15

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

2026-09-07 · 541 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞