P06 订单超时自动取消是怎么实现的?
面试题:下单后 30 分钟未支付自动取消订单,有哪些实现方案?各自优缺点?
方案对比
方案一:定时任务扫表(最常用)
text
定时任务(如每分钟一次)扫描"未支付且创建时间超过 30 分钟"的订单 → 批量取消sql
UPDATE t_order SET status = 'CANCELLED'
WHERE status = 'UNPAID' AND create_time < NOW() - INTERVAL 30 MINUTE- 优点:实现简单、可靠,不会丢任务,适合大量订单批量处理;
- 缺点:有时间误差(最多延迟一个扫描周期)、扫表量大时对 DB 有压力;
- 优化:对
create_time + status建索引、按时间分片扫描、记录上次扫描位置。
方案二:MQ 延迟消息(生产推荐)
- RabbitMQ:死信队列 + TTL 或延迟插件;
- RocketMQ:定时消息(延迟级别);
- Kafka:无原生延迟,需自研或配合时间轮。
下单时发送一条 30 分钟后才投递的延迟消息,消费端收到后校验订单状态,未支付则取消。
- 优点:实时性好、事件驱动;
- 缺点:消息量大时积压,需保证消费幂等(收到消息时订单可能已支付/已取消)。
方案三:Redis 过期监听(不推荐)
给订单 key 设置过期时间 30 分钟,监听 keyexpired 事件触发取消。
- 缺点:过期事件不保证及时可靠(key 到期后由惰性删除触发,事件可能延迟甚至丢失),只适合对实时性要求不高的场景。
方案四:Redisson DelayedQueue / 时间轮
- Redisson 基于 Redis ZSet 的延迟队列,到期才投递;
- 时间轮(HashedWheelTimer):适合单机、延迟任务量可控的场景;
- 缺点:进程重启会丢任务(需持久化补偿),分布式场景要选可靠实现。
关键设计点
- 状态校验:取消前必须校验订单当前状态(只有 UNPAID 才能取消),防止把已支付订单取消;
- 幂等:取消操作支持重复执行(状态机 + 唯一约束);
- 补偿:定时扫表兜底 + 延迟消息双保险,消息丢了由扫表捞回来;
- 库存回补:取消后要释放锁定的库存(Redis 回加 + DB 回补)。
一句话总结
生产环境常用"MQ 延迟消息做实时触发 + 定时扫表做兜底补偿",取消前校验状态,取消后回补库存,保证可靠与幂等。