延迟消息是在业务场景中比较常用的功能,可以作为延迟队列。比如订单n分钟未支付自动取消,活动倒计时,定时发消息都可以使用到延迟消息。
一、延迟消息介绍
5.X版本比4.X版本新增了配置,支持自定义任意延迟时间。延迟消息实现任意时间的任务是基于时间轮做的,时间轮存储定时任务的环形队列,底层采用数组实现,数组中的每一个元素可以存放一个定时任务。在rocketMQ中,使用TimerWheel(类似定长的数组)对于时间轮进行描述和存储,TimerWheel中的每一格代表着一个时间刻度,同时会有一个firstPos指向这个时刻下所有定时消息的首条TimerLog记录的地址,一个lastPos指向这个时刻下所有定时消息的最后一条TimerLog的记录的地址,同一个刻度的消息,TimerLog使用prevPos串联成一个链表。TimerLog记录内存储元数据用于时间到了把消息转移到实际的topic里面。时间轮一格一格向前走配合TimerLog依次读出到期的消息达到定时的目的。
TimerWheel和TimerLog的关系图:
二、生产者延迟消息举例
Message msg = new Message(topic, tag, body);
Long deliverTimeStamp = System.currentTimeMillis() + 3000;//消息在3秒后投递
//指定延迟时间(当前时间之后)。最大40天(超过失败),单位毫秒(ms)
msg.setDeliveryTimestamp(deliverTimeStamp)
三、延时消息实现原理
RocketMQ延迟消息在Broker内部流程图:
步骤说明:
(1)broker接受接收到消息,如果请求参数包含了延迟时间,就会把消息投递到Topic为rmq_sys_wheel_timer的队列中,保存原始的topic信息。
(2)针对放置定时消息的service,每50ms从commitLog读取指定主题(rmq_sys_wheel_timer)的定时消息。TimerEnqueueGetService从commitLog读取得到定时主题的消息,并先将其放入enqueuePutQueue。
//org.apache.rocketmq.store.timer.TimerMessageStore.TimerEnqueueGetService#run
class TimerEnqueueGetService extends ServiceThread {
.....
@Override
public void run() {
TimerMessageStore.LOGGER.info(this.getServiceName() + " service start");
while (!this.isStopped()) {
try {
//读取消息放入enqueuePutQueue
if (!TimerMessageStore.this.enqueue(0)) {
waitForRunning(100 * precisionMs / 1000);
}
} catch (Throwable e) {
TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e);
}
}
TimerMessageStore.LOGGER.info(this.getServiceName() + " service end");
}
}
//org.apache.rocketmq.store.timer.TimerMessageStore#enqueue
public boolean enqueue(int queueId) {
.....
//从commitLog读取指定主题(rmq_sys_wheel_timer)的定时消息
ConsumeQueue cq = (ConsumeQueue) this.messageStore.getConsumeQueue(TIMER_TOPIC, queueId);
if (null == cq) {
return false;
}
.....
//把读取到的消息先放入enqueuePutQueue
if (enqueuePutQueue.offer(timerRequest, 3, TimeUnit.SECONDS)) {
break;
}
.....
}
(3)另一个线程TimerEnqueuePutService将enqueuePutQueue