1️⃣ 为什么“待付款”就减库存,而不是“付款成功”再减?
这是一个经典的“库存扣减时机”问题。电商界主要有两种策略:
A. 下单减库存(拼多多、淘宝、京东的主流模式)✅
- 动作:用户点击“提交订单”(生成订单号),立刻扣减库存。此时状态是
待支付。
- 为什么这么做?
- 防止超卖:这是最核心的原因。假设只有 1 台 iPhone,100 个人同时点击购买。如果等到“支付成功”才减库存,那么这 100 个人都能完成支付(因为支付时库存还是 1)。结果就是:1 台手机卖给了 100 个人。商家发不出货,会面临巨额赔偿和信誉破产。
- 用户体验:用户付完款,发现没货了要退款,体验极差。不如在下单时就告诉他“没货了”,或者锁定给他。
- 代价:会有“库存被占用但未成交”的情况(比如你刚才说的,关了支付窗口不买了)。这就需要“超时自动取消 + 回滚库存”机制来补救。
B. 支付减库存(某些秒杀活动或特殊商品)
- 动作:下单时不减库存,只预占一个“名额”。等银行回调说“钱到了”,才真正扣减库存。
- 缺点:极易超卖。通常只用于库存极大、或者允许超卖赔付的场景。
结论:
为了绝对保证不超卖,主流电商都选择“下单即锁库存”。
所以,当你点击“提交订单”那一刻,数据库里的 stock 字段就已经 -1 了。哪怕你还没付钱,甚至关掉了支付窗口,这个库存依然被你的订单“锁住”了,别人买不了。
2️⃣ 为什么 TTL 过期后,如果不转死信队列,订单还是“待付款”?
这里有一个巨大的思维误区:计算机是“指令驱动”的,不是“时间驱动”的。
- 你的直觉:时间到了(15 分钟),系统应该自动知道“哦,超时了”,然后把订单关掉。
- 计算机的现实:数据库里的一条记录(订单),它没有生命,它不会自己看时间,也不会自己改变自己。
- 除非有人(代码)拿着刀(UPDATE 语句)去砍它,否则它哪怕过了一万年,状态也永远是
待支付。
流程拆解:
-
设置 TTL 的本质:
- 你在 RabbitMQ 里设置
TTL=15m,只是给消息贴了个标签:“15 分钟后如果我还在,我就过期了”。
- 注意:RabbitMQ 的默认行为是,消息过期 = 垃圾 = 删除。它不会自动触发任何业务逻辑,也不会自动去改数据库。
-
如果没有死信队列:
- 15 分钟到 →→ 消息过期 →→ RabbitMQ 把消息删了。
- 消费者(那段负责改数据库状态的代码):根本没收到消息,所以根本没运行。
- 数据库:没人发
UPDATE order SET status='cancelled' 指令。
- 结果:订单状态纹丝不动,依然是
待支付。
-
如果有死信队列:
- 15 分钟到 →→ 消息过期 →→ RabbitMQ 发现配置了死信 →→ 把消息转发给死信队列。
- 消费者:监听到死信队列有新消息 →→ 代码运行了!
- 消费者执行:
UPDATE order SET status='cancelled' WHERE ...
- 结果:数据库状态变成了
已取消,库存回滚。
结论:
TTL 只是一个计时器,它本身不会执行逻辑。必须配合死信队列把“过期的信号”传递给“干活的代码(消费者)”,代码才能去修改数据库。如果没有这个传递过程,时间到了也白搭,订单永远卡在那里。
3️⃣ 数据库状态为什么会变成“待支付”?是关掉付款界面变的吗?
不是的! 这是一个因果关系的误解。
真相是:
“待支付”状态是在你点击“提交订单”按钮的那一瞬间生成的,而不是在你关掉付款界面时生成的。
详细时间线:
- 浏览商品:你在看手机,此时数据库里没有你的订单。
- 点击“立即购买” -> “提交订单”(关键步骤!):
- 前端发送请求给后端。
- 后端代码执行:
- 创建订单记录:在数据库
orders 表插入一行新数据。
- 设置初始状态:这行数据的
status 字段直接被写入为 UNPAID (待支付)。
- 扣减库存:
stock = stock - 1。
- 发送延迟消息:告诉 MQ,“15 分钟后提醒我检查这个订单”。
- 此时:数据库里已经有一条
待支付 的订单了。
- 弹出支付框:
- 后端告诉前端:“订单创建成功了,去调起微信支付/支付宝吧。”
- 前端弹出二维码或密码输入框。
- 你点击“❌”关掉支付框:
- 发生了什么? 其实什么都没发生!
- 你只是关闭了前端的一个弹窗(UI 层面)。
- 你没有发送“取消订单”的请求给服务器。
- 服务器根本不知道你关掉了窗口,它还以为你在输入密码呢。
- 数据库状态:依然是第 2 步写入的
待支付,没有任何变化。
什么时候状态会变?
只有两种情况状态才会变:
- 你付钱了:支付平台回调通知服务器 →→ 服务器执行
UPDATE status = 'PAID'。
- 超时取消了:15 分钟后,消费者收到死信消息 →→ 服务器执行
UPDATE status = 'CANCELLED'。
- 你主动点了“取消订单”按钮:前端发送请求 →→ 服务器执行
UPDATE status = 'CANCELLED'。
结论:
关掉支付界面只是一个前端动作,不会触发任何后端状态变更。订单之所以是“待支付”,是因为它在创建之初就被定义为了“待支付”,并且在等待后续的“支付成功”或“超时取消”指令来改变它。如果后续指令没来(比如消息丢了),它就永远孤独地保持“待支付”。
🌟 终极总结(串联三点)
-
为什么下单就减库存?
为了防超卖。宁可错杀(锁住库存最后没买),不可放过(卖多了发不出货)。
-
为什么超时不自动取消?
因为数据库不会自己变,MQ 过期默认是删除。必须靠死信队列把“过期信号”捡回来,交给代码去执行 UPDATE 语句。没这个环节,订单就卡死了。
-
为什么关掉支付框还是待支付?
因为“待支付”是出生证明(创建订单时就有了),关掉支付框只是“无视它”,并没有“杀掉它”。只有明确的“支付成功”或“超时取消”指令才能改变它的命运。
这下是不是彻底通透了?这就是分布式系统中最终一致性和可靠性设计的精髓所在!