首页 > 项目 > 当前页面

RocketMQ延迟消息是怎么实现的?如果让你设计一个延迟消息系统你会怎么做?

2026-06-09 NEW个对象

🚀 RocketMQ延迟消息是怎么实现的?如果让你设计一个延迟消息系统你会怎么做?

📌 核心结论:
RocketMQ的延迟消息本质上并不是把消息直接存储到未来时间队列,而是采用“时间轮思想 + 多级延迟队列 + 定时任务扫描”的设计。发送延迟消息时先进入特殊Topic,Broker后台线程定时扫描到期消息并重新投递到真实Topic。其核心目标是避免海量定时器导致内存爆炸,同时保证高吞吐、高可靠和可扩展能力。

1️⃣ 问题背景

延迟消息是消息队列系统中非常常见的能力。

例如:

  • 订单30分钟未支付自动取消
  • 外卖超时自动退款
  • 优惠券7天后过期
  • 直播预约提醒
  • 短信定时发送
  • 会员到期通知
  • 物流超时提醒

假设电商平台每天产生1000万订单。

每个订单都需要:

30分钟后检查支付状态

如果直接创建1000万个Java定时任务:

⚠️ JVM会出现大量TimerTask、线程调度和内存消耗问题,系统无法支撑。

因此消息中间件必须提供高性能延迟消息能力。

2️⃣ 核心原理

很多开发者认为延迟消息实现方式是:

消息发送
↓
启动定时器
↓
到时间触发
↓
投递消费者

实际上RocketMQ并不是这样实现。

因为:

  • 消息量可能达到亿级
  • 定时器数量无法控制
  • Broker重启后定时器丢失
  • 维护成本极高

RocketMQ采用:

发送消息
↓
进入延迟Topic
↓
存储CommitLog
↓
定时扫描线程
↓
到期重新投递
↓
进入真实Topic
↓
消费者消费
✅ RocketMQ延迟消息本质是“二次投递”,而不是“定时执行”。

3️⃣ 数据结构分析

真实Topic

OrderTopic

正常订单消息存放位置。

延迟Topic

SCHEDULE_TOPIC_XXXX

RocketMQ内部保留Topic。

所有延迟消息先写入这里。

延迟级别映射

1s
5s
10s
30s
1m
2m
5m
10m
30m
1h
2h

每个延迟级别对应一个Queue。

例如:

DelayLevel 1 → Queue1
DelayLevel 2 → Queue2
DelayLevel 3 → Queue3

这样避免所有延迟消息堆积在一个队列中。

消息存储结构

真实Topic
真实QueueId
投递时间
消息体

延迟消息只是临时修改Topic和Queue信息。

4️⃣ 算法分析

发送阶段

发送:

setDelayTimeLevel(14)

Broker收到消息:

保存真实Topic
↓
替换Topic
↓
写入SCHEDULE_TOPIC_XXXX

扫描阶段

Broker启动后台线程:

ScheduleMessageService

每秒扫描一次。

核心逻辑:

获取消息
↓
判断是否到期
↓
未到期继续等待
↓
已到期重新投递

时间复杂度:

O(N)

但由于按延迟等级分桶,因此扫描成本可控。

5️⃣ 执行流程

RocketMQ延迟消息完整流程

生产者发送消息
↓
指定延迟等级
↓
Broker修改Topic
↓
进入SCHEDULE_TOPIC_XXXX
↓
写入CommitLog
↓
后台线程扫描
↓
判断是否到期
↓
恢复真实Topic
↓
重新投递
↓
消费者消费

为什么性能高?

  • 没有海量Timer
  • 没有大量线程
  • 利用顺序写CommitLog
  • 利用时间分桶降低扫描范围

6️⃣ 实际案例

订单超时取消

用户下单:

创建订单
↓
发送30分钟延迟消息

消息内容:

orderId=10001

30分钟后:

消息到期
↓
重新进入OrderTimeoutTopic
↓
消费者检查订单状态
↓
未支付则取消订单

延迟消息数量估算

假设:

  • 每天订单1000万
  • 延迟30分钟
  • 平均消息1KB

存储:

约10GB消息数据

RocketMQ完全可以支撑。

7️⃣ 如果让我设计延迟消息系统

第一阶段:简单版

MySQL
↓
execute_time索引
↓
定时任务扫描

适合百万级数据。

第二阶段:Redis ZSet方案

score = executeTime
member = msgId

定时扫描:

ZRANGEBYSCORE
↓
获取到期消息
↓
发送MQ

适合千万级场景。

第三阶段:时间轮方案

消息进入时间轮
↓
秒槽位
↓
分钟槽位
↓
小时槽位
↓
触发投递

类似Kafka Timer Wheel。

时间复杂度:

O(1)

适合亿级消息系统。

第四阶段:最终架构

Producer
↓
Delay Topic
↓
Time Wheel
↓
Schedule Service
↓
Real Topic
↓
Consumer

同时增加:

  • 多副本存储
  • 失败重试
  • 死信队列
  • 消息追踪
  • 幂等控制

8️⃣ 面试常见问题

RocketMQ为什么不用定时器?

因为海量定时器会导致内存和线程资源消耗巨大,不适合分布式消息系统。

延迟消息为什么要二次投递?

这样可以复用现有消息存储和消费体系,避免设计全新的调度架构。

RocketMQ延迟消息有什么缺陷?

  • 早期版本仅支持固定延迟等级
  • 不是绝对精确时间
  • 依赖Broker扫描线程

Redis ZSet和时间轮哪个好?

千万级以内ZSet简单高效;亿级以上时间轮更节省资源。

如果你设计延迟消息系统怎么做?

采用时间轮加MQ架构,消息先进入延迟存储层,通过时间轮管理触发时间,到期后重新投递真实Topic,同时结合多副本、重试机制和幂等控制保证可靠性与扩展性。

9️⃣ 总结

✅ RocketMQ延迟消息本质是延迟存储加二次投递。

✅ 消息首先进入SCHEDULE_TOPIC_XXXX。

✅ Broker后台线程定期扫描到期消息。

✅ 到期后恢复真实Topic并重新发送。

✅ RocketMQ避免了海量Timer带来的资源消耗问题。

✅ 如果自研延迟消息系统,推荐演进路线:

MySQL定时扫描
↓
Redis ZSet
↓
多级时间轮
↓
MQ二次投递
↓
分布式延迟消息平台

这也是当前大型互联网公司在订单超时、支付超时、定时通知、营销触达等场景下最主流的延迟消息设计方案。

相关文章

NEW个对象 NEW个对象
JAVA是世界上最好的语言