延迟消息
使用示例
消费者没什么变化,和普通消费者一样,这里就不贴了
DefaultMQProducer producer = new DefaultMQProducer("ms-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("orderMsTopic", "我是一个延迟消息".getBytes());
// 给消息设置一个延迟时间,不同的延迟级别表示不同的延迟时间
// messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
message.setDelayTimeLevel(3);
producer.send(message);
System.out.println("发送时间" + new Date());
producer.shutdown();
实现原理
1. 服务初始化阶段
Broker 启动时会初始化 ScheduleMessageService
延迟消息服务,这个服务会创建 18 个定时任务,分别对应 18 种延迟级别
ScheduleMessageService
服务内部创建一个线程池,线程个数默认是 CPU 核心数,由这些线程共同创建 18 个任务,可能一个线程负责多个任务
2. 消息存储阶段
Broker 接收到延迟消息后,先存入一个特殊的 Topic:SCHEDULE_TOPIC_XXXX
,这个 Topic 下有 18 个队列(从 0 开始),对应 18 种延迟级别
store/consumequeue/SCHEDULE_TOPIC_XXXX/
├── 0/ # 级别1(1s延迟)的队列
├── 1/ # 级别2(5s延迟)的队列
...
└── 17/ # 级别18(2h延迟)的队列
SCHEDULE_TOPIC_XXXX
不是目标 Topic 所以,消息此时不会投递给消费者
3. 定时任务执行
定时任务初始化完成后,第一次执行的时间是延迟级别的时间,之后是每秒执行一次,执行流程如下图:
flowchart LR
A[任务触发] --> B{检查消息存储时间}
B -->|存储时间+延迟≤当前时间| C[转移到目标 Topic]
B -->|未到期| D[保持原位]
C --> E[Broker 投递消息给消费者]
D --> F[等待下次扫描]
如果定时任务扫描后消息还未到期,跳过这条消息,进入下一轮扫描(时间轮)
每次每个任务扫描对应队列的 100 条消息,不会全量扫描
4. 消息投递阶段
到期消息处理:
- 从
SCHEDULE_TOPIC_XXXX
删除消息,并转移到目标 Topic - 将消息写入 CommitLog 中(是个文件,每个消息都会写入到这个文件,包含每个消息的Topic、Tag、Body等属性)
- 为消息构建索引(所有消息都放到这一个文件中,构建索引为了快速定位每条消息)
- 这时 Broker 就可以把消息推送给消费者了
注意事项
- 误差在秒级(业务需容忍±1秒偏差)
- 消息发送后无法修改延迟时间
- 大量延迟消息会增加定时任务压力
- 延迟消息不能和顺序、事务、批量消息组合
4.x
只支持级别,不同级别对应不同延迟时间,这个可以改,要到配置文件 conf 里改5.x
可以指定时间了(上面的原理是 4.x 的,5.x 的估计要动态创建定时任务)