D
一致性中级25 分钟

订单超时自动关单

下单 30 分钟不付款自动关单,支付回调撞车也不能误关。

设计一致性MQ订单状态机延迟消息CAS幂等补偿扫描
Java 场景 PDF Ch02 · 电商订单超时自动关单
01

面试官问题

真实问法

用户下单后 30 分钟未支付要自动关闭订单并释放库存,你会怎么设计?如果关单消息和支付回调同时到达,怎么保证已支付订单不会被关?

这题考什么

考延迟任务选型、订单状态机、幂等消费、支付与关单并发竞态,以及漏消息后的补偿能力。

正确建模思路

把关单建模成 WAIT_PAY 到 CLOSED 的受保护状态迁移。下单时登记 expireAt 和延迟消息;触发后先查支付状态,再用 CAS 只允许 WAIT_PAY -> CLOSED;最后用补偿扫描捞漏。

02

业务背景

电商下单会短暂占用库存;如果用户长时间不付款,库存必须释放给其他用户,但已支付订单绝不能被误关。

学完后你能回答
  • 延迟任务为什么不能只靠扫表?
  • 支付和关单并发时怎么保证状态正确?
  • 消息重复、消息丢失时怎么兜底?
03

关键约束

  • 准时:允许秒级误差,不允许长期漏关。
  • 可靠:触发器可以重复,业务结果必须只生效一次。
  • 并发:支付和关单可能同时发生。
  • 可补偿:消息丢失、服务宕机后要能修复。
04

系统建模

  1. 1

    订单状态机:WAIT_PAY、PAID、CLOSED 是互斥状态。

  2. 2

    业务时钟:createdAt + 30m 生成 expireAt。

  3. 3

    延迟队列:负责到点唤醒,不负责最终正确性。

  4. 4

    订单库:用 CAS / 乐观锁守住状态迁移。

  5. 5

    补偿扫描器:低频扫描异常过期订单。

05

动画推演

动画看什么

看业务时钟到点后,延迟队列如何触发消费者;消费者先读支付状态,再通过 CAS 推动订单状态机;补偿扫描器只处理异常漏网订单。

  1. 01 创建订单,状态 WAIT_PAY。
  2. 02 expireAt = createdAt + 30m。
  3. 03 延迟任务触发。
  4. 04 查询支付状态。
  5. 05 CAS 更新 WAIT_PAY -> CLOSED。
  6. 06 如果已支付,拒绝关单。
  7. 07 补偿任务扫描异常订单。
视觉元素
订单状态机业务时钟延迟队列支付服务订单库补偿扫描器

切换延迟方案,看业务时钟、延迟队列、支付服务和订单状态机如何协同。

订单超时关单实验台
业务时钟
30:00

expireAt = createdAt + 30m

延迟组件
等待方案触发
订单
ORD-202405-8891
status = WAIT_PAY
WAIT_PAYPAIDCLOSED
timeline7 events · 5 frames
业务时钟开始 · 1/5
06

故障注入 / 错误做法

错误回答

我开一个定时任务每分钟扫全表,找到超时订单就直接改成 CLOSED。支付了的话理论上不会扫到。

正确回答

把关单建模成 WAIT_PAY 到 CLOSED 的受保护状态迁移。下单时登记 expireAt 和延迟消息;触发后先查支付状态,再用 CAS 只允许 WAIT_PAY -> CLOSED;最后用补偿扫描捞漏。

故障注入

错误做法是消费者收到消息就直接关单,不查支付状态、不做 CAS。动画会注入“支付已成功但延迟消息刚到”的竞态,展示误关如何发生。

风险点
  • 延迟消息重复投递会导致重复退库存。
  • 支付回调和关单消费者并发更新同一订单。
  • MQ 丢失或消费者宕机会留下过期 WAIT_PAY 订单。
  • 关单成功但库存释放失败会造成库存不一致。
07

30 秒面试表达

  1. 01

    我会把关单建模成一个延迟任务:下单写 WAIT_PAY、expireAt=createdAt+30m,同时投一条 30 分钟延迟消息。

  2. 02

    消息到期后消费者先查订单和支付状态,只允许 WAIT_PAY 通过 CAS 更新为 CLOSED;如果已经 PAID,就直接 ack 丢弃。

  3. 03

    重复消息靠状态机幂等,第二次看到 CLOSED 就不再退库存。

  4. 04

    最后加一个低频补偿扫描,扫描 expireAt 已过但仍 WAIT_PAY 的异常订单,补掉消息丢失或消费者宕机的漏洞。

08

常见追问

支付回调和关单消息同时到怎么办?

两边都不能盲改状态。支付只允许 WAIT_PAY -> PAID,关单只允许 WAIT_PAY -> CLOSED,用版本号或 where status='WAIT_PAY' 的 CAS 保证只有一方成功。

消息重复消费怎么办?

重复消费只会再次尝试 WAIT_PAY -> CLOSED。订单已 CLOSED 或 PAID 时直接 ack,库存释放也用流水或状态判断做幂等。

MQ 消息丢了怎么办?

MQ 是快路径,补偿扫描是安全网。扫 expireAt 已过且 WAIT_PAY 的订单,按同样的状态机逻辑补关单。

09

复盘卡片

  • 正确性不靠延迟队列,靠订单状态机和 CAS。
  • MQ 是触发器,补偿扫描是安全网。
  • 库存释放必须和关单结果保持幂等一致。