订单超时自动关单
下单 30 分钟不付款自动关单,支付回调撞车也不能误关。
面试官问题
用户下单后 30 分钟未支付要自动关闭订单并释放库存,你会怎么设计?如果关单消息和支付回调同时到达,怎么保证已支付订单不会被关?
考延迟任务选型、订单状态机、幂等消费、支付与关单并发竞态,以及漏消息后的补偿能力。
把关单建模成 WAIT_PAY 到 CLOSED 的受保护状态迁移。下单时登记 expireAt 和延迟消息;触发后先查支付状态,再用 CAS 只允许 WAIT_PAY -> CLOSED;最后用补偿扫描捞漏。
业务背景
电商下单会短暂占用库存;如果用户长时间不付款,库存必须释放给其他用户,但已支付订单绝不能被误关。
- 延迟任务为什么不能只靠扫表?
- 支付和关单并发时怎么保证状态正确?
- 消息重复、消息丢失时怎么兜底?
关键约束
- 准时:允许秒级误差,不允许长期漏关。
- 可靠:触发器可以重复,业务结果必须只生效一次。
- 并发:支付和关单可能同时发生。
- 可补偿:消息丢失、服务宕机后要能修复。
系统建模
- 1
订单状态机:WAIT_PAY、PAID、CLOSED 是互斥状态。
- 2
业务时钟:createdAt + 30m 生成 expireAt。
- 3
延迟队列:负责到点唤醒,不负责最终正确性。
- 4
订单库:用 CAS / 乐观锁守住状态迁移。
- 5
补偿扫描器:低频扫描异常过期订单。
动画推演
看业务时钟到点后,延迟队列如何触发消费者;消费者先读支付状态,再通过 CAS 推动订单状态机;补偿扫描器只处理异常漏网订单。
- 01 创建订单,状态 WAIT_PAY。
- 02 expireAt = createdAt + 30m。
- 03 延迟任务触发。
- 04 查询支付状态。
- 05 CAS 更新 WAIT_PAY -> CLOSED。
- 06 如果已支付,拒绝关单。
- 07 补偿任务扫描异常订单。
切换延迟方案,看业务时钟、延迟队列、支付服务和订单状态机如何协同。
expireAt = createdAt + 30m
故障注入 / 错误做法
我开一个定时任务每分钟扫全表,找到超时订单就直接改成 CLOSED。支付了的话理论上不会扫到。
把关单建模成 WAIT_PAY 到 CLOSED 的受保护状态迁移。下单时登记 expireAt 和延迟消息;触发后先查支付状态,再用 CAS 只允许 WAIT_PAY -> CLOSED;最后用补偿扫描捞漏。
错误做法是消费者收到消息就直接关单,不查支付状态、不做 CAS。动画会注入“支付已成功但延迟消息刚到”的竞态,展示误关如何发生。
- 延迟消息重复投递会导致重复退库存。
- 支付回调和关单消费者并发更新同一订单。
- MQ 丢失或消费者宕机会留下过期 WAIT_PAY 订单。
- 关单成功但库存释放失败会造成库存不一致。
30 秒面试表达
- 01
我会把关单建模成一个延迟任务:下单写 WAIT_PAY、expireAt=createdAt+30m,同时投一条 30 分钟延迟消息。
- 02
消息到期后消费者先查订单和支付状态,只允许 WAIT_PAY 通过 CAS 更新为 CLOSED;如果已经 PAID,就直接 ack 丢弃。
- 03
重复消息靠状态机幂等,第二次看到 CLOSED 就不再退库存。
- 04
最后加一个低频补偿扫描,扫描 expireAt 已过但仍 WAIT_PAY 的异常订单,补掉消息丢失或消费者宕机的漏洞。
常见追问
支付回调和关单消息同时到怎么办?▶
两边都不能盲改状态。支付只允许 WAIT_PAY -> PAID,关单只允许 WAIT_PAY -> CLOSED,用版本号或 where status='WAIT_PAY' 的 CAS 保证只有一方成功。
消息重复消费怎么办?▶
重复消费只会再次尝试 WAIT_PAY -> CLOSED。订单已 CLOSED 或 PAID 时直接 ack,库存释放也用流水或状态判断做幂等。
MQ 消息丢了怎么办?▶
MQ 是快路径,补偿扫描是安全网。扫 expireAt 已过且 WAIT_PAY 的订单,按同样的状态机逻辑补关单。
复盘卡片
- 正确性不靠延迟队列,靠订单状态机和 CAS。
- MQ 是触发器,补偿扫描是安全网。
- 库存释放必须和关单结果保持幂等一致。