源码版本号:版本号:4.9.4
RocketMQ可以发送延时消息,一共有18个延时等级,例如延时等级 1
代表延时 1s
,延时等级 2
代表延时 5s
。
通过 Message#setDelayTimeLevel
方法设置消息的延时等级。
所有延时时间如下所示
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
在Broker进行消息存储的时候会判断消息是否设置了延时等级,如果设置了延时等级则会按照延时消息的逻辑进行处理
public class CommitLog {
/**
* 版本号:4.9.4
* 找到607行
*/
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
// 省略......
/**
* 重要处理代码如下
* 找到624行
*/
// Delay Delivery
if (msg.getDelayTimeLevel() > 0) {
// 判断延时等级是否超过最大, 默认为18
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
// 重新指定topic = SCHEDULE_TOPIC_XXXX
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
// 计算新的 queueId = 延时等级 - 1
int queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// 将真正的topic, queueId设置到属性中
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
// 备份原消息的属性信息
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(topic);
msg.setQueueId(queueId);
}
// 省略......
}
}
可以发现,延时消息的存储,并不会直接保存到指定的Topic队列中,而是先保存到内部Topic = SCHEDULE_TOPIC_XXXX的队列里面,
SCHEDULE_TOPIC_XXXX一共有18个队列(MessageQueue
),对应着延迟消息的18个等级,
RocektMQ会根据指定的DelayTimeLevel来决定选择哪个MessageQueue,这块的计算逻辑很简单:queueId = DelayTimeLevel - 1
看到这里,可以猜测,肯定会有个定时任务将SCHEDULE_TOPIC_XXXX队列里面满足条件的消息投递到指定的Topic
查看Broker的代码,定时任务在 org.apache.rocketmq.store.schedule.ScheduleMessageService
中
查看它的启动方法
public class ScheduleMessageService extends ConfigManager {
// 存放了每个延迟等级对应的延迟时间
private final ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable =
new ConcurrentHashMap<Integer, Long>(32);
// 存放每个延迟等级对应的消费进度
private final ConcurrentMap<Integer /* level */, Long/* offset */> offsetTable =
new ConcurrentHashMap<Integer, Long>(32);
/**
* 版本号:4.9.4
* 找到128行
* 查看启动方法
*/
public void start() {
if (started.compareAndSet(false, true)) {
// 加载一些必要信息
this.load();
// 遍历 delayLevelTable , 找到对应的消费进度, 然后进行消费
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
if (timeDelay != null) {
// ......
/**
* 147行, 生成一个延迟执行的定时任务, 去拉取延时队列里面的消息
* level可以定位到要消费的队列
* offset可以知道从哪里开始消费
*/
this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
}
}
/**
* 省略......
* 后面会生成一个定时任务, 默认每隔10s就会将消费进度 offsetTable 的数据持久化到 delayOffset.json
*/
}
}
@Override
public boolean load() {
boolean result = super.load();
/**
* 解析延迟等级, 初始化 delayLevelTable 字段
* 1. 生成一个Map<String, Long>
* 初始值 "s" -> 1000, "m" -> 1000*60, "h" -> 1000*60*60, "d" -> 1000*60*60*24
* 2.拿到字符串: "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
* 2.根据空格分割, 然后遍历
* 3.比如拿到了"10s", 最后解析成 10 和 s, s 对应 1000, 计算出延时时间为: 10 * 1000
*/
result = result && this.parseDelayLevel();
// 加载延时队列的消费进度, 取的是 delayOffset.json, 初始化 offsetTable 字段
result = result && this.correctDelayOffset();
return result;
}
}
ScheduleMessageService
启动时会去加载SCHEDULE_TOPIC_XXXX的消费进度,在文件delayOffset.json
中
内容如下所示,记录了每个队列被消费到哪里
{
"offsetTable":{1:2,2:1,3:1,4:1,5:1,6:1,7:1}
}
每一个延迟等级都提交了一个 延迟执行的DeliverDelayedMessageTimerTask
任务,上面提交的是一次性的任务,查看它的 run
方法,
可以发现每次拉取完消息后都会再次提交一个延迟执行的DeliverDelayedMessageTimerTask
任务
DeliverDelayedMessageTimerTask
里面有两个重要的属性:
delayLevel: 延迟等级,delayLevel - 1得到对应的队列的queueId
offset: 消费进度,可以知道应该从哪里开始消费
里面的执行逻辑大概如下:
- 拉取Topic为SCHEDULE_TOPIC_XXXX,queueId = (delayLevel - 1),偏移量为offset 的消息
- 遍历拉取到的消息,通过delayLevel到delayLevelTable找到对应的延迟时间间隔,根据消息的存储时间msgStoreTime可以判断当前消息是否应该被投递到指定的Topic
- 如果没有到时间,则延迟执行,再生成一个
DeliverDelayedMessageTimerTask
任务。
由于同一个队列里面的延迟时间是一样的,先进来的消息肯定先到期,所以当前遇到未到期的消息,那么后面的消息肯定也还没有到处理时间。 - 处理完拉取到的消息,再次提交一个延迟执行的
DeliverDelayedMessageTimerTask
任务。