前言
在真实业务中,“延时触发”是一类非常常见但又容易被低估的需求,例如:
- 机票下单后15 分钟未支付自动取消
- 订单创建后30 分钟关闭
- 活动开始前定时推送通知
- 资源锁定一段时间后自动释放
在单机系统中,这类需求实现并不复杂;
但在分布式、高并发、可扩展系统中,延时消息的设计就变得非常关键。
本文将以「购买机票超时未支付自动取消订单」为例,循序渐进讲清楚:
- 本地延时是如何实现的
- 本地方案的局限在哪里
- 分布式延时消息的几种主流设计方案
- 业界(RocketMQ)是如何解决延时消息问题的
- 一个可落地的分布式延时消息设计思路
业务场景抽象:机票超时未支付
典型业务流程
- 用户下单购买机票
- 系统创建订单,状态为「待支付」
- 系统需要在15 分钟后检查订单
如果已支付 → 不处理
如果未支付 → 自动取消订单,释放座位
这本质上是一个:
“现在 + 延迟时间 → 执行一段逻辑”的问题
本地延时任务的实现方式(单机)
在进入分布式之前,先看最基础的实现方式。
Timer(已不推荐)
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { cancelOrder(orderId); } }, 15 * 60 * 1000);问题:
- 单线程执行
- 任务异常会影响整个 Timer
- 无法承载高并发
ScheduledThreadPoolExecutor(推荐)
ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); executor.schedule(() -> { cancelOrder(orderId); }, 15, TimeUnit.MINUTES);优点:
- 支持线程池
- API 简单
- 本地可靠性较好
本地延时方案的问题
虽然ScheduledThreadPoolExecutor很好用,但只能用于单机,在真实生产环境会遇到:
| 问题 | 说明 |
|---|---|
| 服务重启 | 延时任务直接丢失 |
| 集群部署 | 多实例无法协调 |
| 扩容缩容 | 任务归属混乱 |
| 高并发 | 内存压力大 |
结论:本地延时 ≠ 分布式延时
从本地延时中抽象可复用的思想
虽然本地方案不可直接用于分布式,但它给了我们重要启发:
延时任务 = 任务 + 触发时间
换句话说,我们只要解决两个问题:
任务存在哪里
什么时候被取出来执行
分布式延时消息的核心设计思路
核心目标
- 可靠存储:服务重启不丢任务
- 可水平扩展
- 时间精度可控
- 高吞吐
分布式延时消息方案一:外部存储 + 定时扫描
设计思路
将延时消息存储在外部系统中:
(orderId, executeTime, payload, status)然后由后台线程周期性扫描:
SELECT * FROM delay_task WHERE execute_time <= now() AND status = 'NEW' LIMIT 100;架构示意
下单 → 写延时任务表 → 定时扫描 → 执行业务优缺点分析
优点:
- 实现简单
- 可控性强
- 易于理解
缺点:
- 扫描数据库压力大
- 时间精度有限(秒级)
- 高并发下性能瓶颈明显
适合:中小规模系统
分布式延时消息方案二:Redis 实现
Redis ZSet(推荐)
利用 ZSet 的score表示时间戳:
key: delay:order score: executeTimestamp value: orderId写入延时任务
ZADD delay:order 1700000000 order123消费逻辑
ZRANGEBYSCORE delay:order -inf now LIMIT 0 100取到后:
- 执行业务
ZREM删除任务
优缺点
优点:
- 性能极高
- 实现相对简单
- 天然支持排序
缺点:
- Redis 内存成本
- 数据持久性依赖 Redis 配置
- 需要处理重复消费、幂等
业界使用非常广泛
分布式延时消息方案三:时间轮(Time Wheel)
核心思想
将时间划分为多个“槽位”:
| 0 | 1 | 2 | 3 | 4 | 5 | ... |每个槽代表一个时间区间,任务被放入对应槽位。
特点
- 插入和触发复杂度接近 O(1)
- 非常适合大量延时任务
局限
- 实现复杂
- 精度有限
- 通常需要多级时间轮
Netty、Kafka、RocketMQ 都采用了时间轮思想
业界成熟方案:RocketMQ 延时消息
RocketMQ 的做法
RocketMQ不支持任意时间延时,而是采用:
固定等级延时
例如:
| Level | 延时时间 |
|---|---|
| 1 | 1s |
| 2 | 5s |
| 3 | 10s |
| 4 | 30s |
| 5 | 1m |
| ... | ... |
实现原理简述
- 延时消息写入特殊 Topic
- 使用时间轮 + 定时调度
- 到期后转发到真实 Topic
优缺点
优点:
- 高性能
- 高可靠
- 生产级方案
缺点:
- 延时时间不灵活
- 强依赖 MQ
一个完整的分布式延时消息落地方案(机票)
推荐组合方案
下单 ↓ 发送延时消息(Redis ZSet / RocketMQ) ↓ 延时到期 ↓ 消费者校验订单状态 ↓ 未支付 → 取消订单关键设计点
- 业务幂等
- 状态二次校验
- 延时消息 ≠ 定时任务
- 失败重试机制
总结
| 方案 | 适用场景 |
|---|---|
| 本地延时 | 单机、简单系统 |
| DB 扫描 | 小规模、低频 |
| Redis ZSet | 高并发、灵活延时 |
| 时间轮 | 超大规模 |
| RocketMQ | 企业级 |
延时消息的本质不是“等多久”,而是“何时可靠地执行一次”