上一篇文章:rocketmq延迟消息实现原理(上)分析了rocketmq4.x版本中支持的默认级别的延迟消息的实现原理,默认的延迟级别无法满足用户对任意秒级时间的诉求,rocketmq5版本中支持了定时任意秒级时间的,所以本文一起来看支持任意秒级时间的定时消息的实现原理。
发送消息到服务端完整流程
发送定时消息
发送定时或延迟消息的代码示例,如下代码块所示:
// 实例化一个生产者来产生消息
DefaultMQProducer producer = new DefaultMQProducer("TimerMessageProducerGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动生产者
producer.start();
Message message = new Message("TimerTopic", ("Hello timer message ").getBytes()); // 设置定时时间为10s
// 设置自定义 【延迟10s】
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L);
// 发送消息
producer.send(message);
// 关闭生产者
producer.shutdown()
发送定时消息是通过对要发送的消息调用setDelayTimeSec这个方法去设置的。Message类中提供下面这三个方法都可以实现消息的定时或延迟。
public void setDelayTimeSec(long sec) {
this.putProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC, String.valueOf(sec));
}
public void setDelayTimeMs(long timeMs) {
this.putProperty(MessageConst.PROPERTY_TIMER_DELAY_MS, String.valueOf(timeMs));
}
public void setDeliverTimeMs(long timeMs) {
this.putProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS, String.valueOf(timeMs));
}
存储预处理定时消息
消息发送到broker后,执行putMessage之前,先是调用了3个hook方法进行预处理,针对定时或延迟消息的预处理逻辑,是在HookUtils#handleScheduleMessage进行执行的,主要是做了2件事:
1、合法性与流控校验;
2、把真实的消息的topic修改设置为默认的定时消息topic:rmq_sys_wheel_timer,queueId为固定的0,原来真实的消息topic与queueId先放到消息的扩展属性中。
其他的步骤处理逻辑与普通消息没有区别。
时间轮算法
涉及两个核心的数据结构:TimerWheel(时间轮,org.apache.rocketmq.store.timer.TimerWheel用于定时消息到时)与TimerLog(org.apache.rocketmq.store.timer.TimerLog存储消息索引)
TimerLog,设计的定时消息的记录文件,Append Only。每条记录包含一个prev_pos,指向前一条定时到同样时刻的记录。每条记录的内容可以包含定时消息本身,也可以只包含定时消息的位置信息。
TimerWheel,是对时刻表的一种抽象,通常使用数组实现。时刻表上的每一秒,顺序对应到数组中的位置,然后数组循环使用。时间轮的每一格(org.apache.rocketmq.store.timer.Slot),指向了TimerLog中的对应位置,如果这一格的时间到了,则按TimerLog中的对应位置以及prev_pos位置依次读出每条消息。
时间轮一格一格向前推进,配合TimerLog,依次读出到期的消息,从而达到定时消息的目的。
org.apache.rocketmq.store.timer.Slot设计成固定32个字节的,包含的属性及长度如下所示。
public class Slot {
public static final short SIZE = 32;
//delayed time
public final long timeMs;
// 首个消息位置
public final long firstPos;
// 最后一个消息位置
public final long lastPos;
// 消息数量
public final int num;
public final int magic;
}
定时消息处理逻辑
基于上面的算法与Timerlog、TimerWheel的数据结构设计,具体实现定时消息的处理的逻辑都在org.apache.rocketmq.store.timer.TimerMessageStore类中,主要涉及到下面5个线程的执行。
private TimerEnqueueGetService enqueueGetService;
private TimerEnqueuePutService enqueuePutService;
private TimerDequeueGetService dequeueGetService;
private TimerDequeueGetMessageService[] dequeueGetMessageServices;
private TimerDequeuePutMessageService[] dequeuePutMessageServices;
这几个线程配合执行的流程图如下所示:
从图中可以看出,共有五个Service线程分别处理定时消息的放置和存储。工作流如下:
1、针对放置定时消息的service,每100ms从commitLog读取指定主题(TIMER_TOPIC)的定时消息。
- TimerEnqueueGetService线程启动后,不停的从topic为rmq_sys_wheel_timer,队列id为0的队列拉取消息,并先将其放入本地的enqueuePutQueue中,如果拉取不到,则每隔100ms后继续此操作。
- 另一个线程TimerEnqueuePutService不停的从enqueuePutQueue拉取放入的定时消息,将其以append的方式放入timerLog中,更新timeWheel时间轮的存储内容。将该任务放进时间轮的指定位置。
2、针对取出定时消息的service,每50ms读取下一秒的Slot。有三个线程将读取到的消息重新放回commitLog。
- 首先,TimerDequeueGetService线程启动后,不停地从timerWheel中获取下一秒的Slot,从timerLog中得到指定的msgs,并放进dequeueGetQueue,如果获取不到,则等待100ms继续;
- 而后TimerDequeueGetMessageService线程,从dequeueGetQueue中取出msg,并将其放入队列待写入CommitLog的队列dequeuePutQueue中;
- 最后TimerDequeuePutMessageService线程启动后,不停地从这个dequeuePutQueue中取出消息,放回commitlog,若定时时间已到期则修改topic为真实的topic(这时消息可以被消费),否则继续按原topic(rmq_sys_wheel_timer)写回CommitLog滚动。
感兴趣可以关注公众号