面试知识库
极高 进阶

订单超时取消方案#

一句话答案#

推荐延迟 MQ(RocketMQ 延迟消息)作主力 + DB 定时扫描兜底,权衡精度、可靠性和复杂度。

核心要点

方案精度可靠性复杂度
RocketMQ延迟消息4.x 固定18级;5.x 任意时间戳高低(推荐)
Redis ZSET+轮询秒级中中
时间轮高低(内存)中
定时扫描DB分钟级高低

推荐: 延迟MQ作主力 + 定时扫描兜底

面试回答(2分钟版)

订单超时取消我推荐”主力加兜底”的双保险方案。主力用延迟消息队列,比如RocketMQ(4.x 有18个固定延迟级别,5.x 定时消息可指定任意时间戳),下单时发一条30分钟延迟消息,到时间后消费者用条件更新取消订单:UPDATE … SET status=已取消 WHERE id=? AND status=未支付,影响行数为 0 说明已支付,直接跳过,避免和支付回调并发时误取消。兜底用DB定时扫描,每分钟跑一次查询超时未付订单,捞出延迟消息丢失的漏网之鱼。为什么不单用一种?单靠延迟MQ有消息丢失风险,单靠定时扫描精度只有分钟级且对DB有压力。取消订单不只是改状态,还要释放库存、归还优惠券、退还积分、通知用户,这些操作建议通过事件机制解耦。如果用Redis方案,可以把订单ID放进ZSET,score是过期时间戳,轮询取出到期订单,精度能到秒级但可靠性不如MQ。实际生产中延迟MQ加DB扫描的组合是最稳妥的选择。

追问与易错

追问方向:

  • “延迟 MQ 不够精确怎么办?”→ RocketMQ 4.x 延迟级别固定 18 级(1s/5s/10s/30s/1m/…/30m/1h/2h),只能选最近的级别;RocketMQ 5.x 定时消息直接指定投递时间戳,可精确到任意时刻(有最大延迟上限)。RabbitMQ 的 TTL+死信队列只在队头检查过期,不同 TTL 的消息混在一个队列会被队头阻塞,通常按固定延迟档位建多个队列;rabbitmq-delayed-message-exchange 插件官方已停止维护
  • “兜底扫描频率怎么设?”→ 通常每分钟扫描一次,查询条件为 status=未支付 AND create_time < NOW()-超时时间,加索引保证查询效率;频率太高 DB 压力大,太低漏单时间长,1 分钟是平衡点
  • “取消订单需要做哪些操作?”→ 不只是改订单状态,还需要:释放库存(Redis+DB)、归还优惠券、退还积分、取消支付预授权、通知用户;建议通过发布领域事件让各模块订阅处理,实现解耦
  • “消费者取消订单和用户支付同时发生怎么办?”→ 取消和支付都用带状态条件的更新(WHERE status=未支付)抢同一行,谁先更新成功谁生效;如果订单已取消后支付回调才到,走自动退款并告警,不能把已取消订单改回已支付

易错点:

  • ❌ 用一种方案就够——推荐主力+兜底双保险
  • ❌ 定时扫描不能用——作为兜底完全可以