由于公司之前新的产品需求中希望企业管理员可以自定义通知推送时间,业务线目前使用的主要MQ为开源版本的RocketMQ4.x版本,只支持18个延迟级别,不具备任意时间。基于对源码的阅读后实现了任意时间延迟消息。
源码中延迟消息的实现
rocketMq的源码中初始化了18个级别的延迟级别,其实现在MessageStoreConfig类中

延迟消息的实现核心是在CommitLog类中,这个也比较好理解,commitLog是存储消息的元数据日志文件,所有的消息都会记录在这个文件中,然后再去处理创建更新ConsumerQueue和IndexFile文件。 方法入口asyncPutMessage方法:
commitLog处理入口
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
// Set the storage time
msg.setStoreTimestamp(System.currentTimeMillis());
// Set the message body BODY CRC (consider the most appropriate setting
// on the client)
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
// Back to Results
AppendMessageResult result = null;
StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
String topic = msg.getTopic();
// int queueId msg.getQueueId();
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// Delay Delivery 延迟消息的处理方式
//延迟消息转到系统Topic,如果大于最大的级别就按照最大级别来处理
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
//转存的topic和转存的queueId
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
int queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real 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()));
//延迟消息处理:先放入SCHEDULE_TOPIC_XXXX的topic下,messageQueue的id就是延迟级别-1,创建18个队列
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
}
从上面的代码可以看出首先判断是否是延迟消息,是的话就先存储在topic为
RMQ_SYS_SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX";
queueId为
delayLevel - 1 (延迟)
然后经过设置的延迟时间后再从SCHEDULE_TOPIC_XXXX中转存到用户指定的业务Topic中,实现完整的延迟消息消费。转存服务的核心是scheduleMessageService类,它只在master节点上启动,slave节点上会进行判断主动关闭这个服务。
下面是延迟消息处理的一个核心deliverPendingTable类的数据初始化

在scheduleMessageService的start方法中处理延迟消息转存的核心方法是下面代码,启动一个定时任务线程池,核心线程数为延时消息的级别数,每隔一秒执行一次任务。
arduino
this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
下面介绍核心的任务处理方法 DeliverDelayedMessageTimerTask,其实现是executeOnTimeup方法;此处代码摘要省略了前面扫描获取consumeQueue文件,解析文件获取消费位置等代码,直接上最核心的代码
executeOnTimeup方法
long nextOffset = this.offset;
try {
int i = 0;
ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
//循环过滤ConsumeQueue文件当中的每一条消息索引
for (; i < bufferCQ.getSize() && isStarted(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
//解析每一条ConsumeQueue记录
long offsetPy = bufferCQ.getByteBuffer().getLong();
int sizePy = bufferCQ.getByteBuffer().getInt();
long tagsCode = bufferCQ.getByteBuffer().getLong();
if (cq.isExtAddr(tagsCode)) {
if (cq.getExt(tagsCode, cqExtUnit)) {
tagsCode = cqExtUnit.getTagsCode();
} else {
//can't find ext content.So re compute tags code.
log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
tagsCode, offsetPy, sizePy);
long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
}
}
//计算延迟时间
long now = System.currentTimeMillis();
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
//延迟时间没到就等一等。
long countdown = deliverTimestamp - now;
if (countdown > 0) {
this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
return;
}
//从commitlog中获取完整的消息数据
MessageExt msgExt = ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
if (msgExt == null) {
continue;
}
MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeup(msgExt);
//延时消息 - 不支持事务消息
if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
msgInner.getTopic(), msgInner);
continue;
}
//时间到了就进行转储。
boolean deliverSuc;
if (ScheduleMessageService.this.enableAsyncDeliver) {
deliverSuc = this.asyncDeliver(msgInner, msgExt.getMsgId(), nextOffset, offsetPy, sizePy);
} else {
deliverSuc = this.syncDeliver(msgInner, msgExt.getMsgId(), nextOffset, offsetPy, sizePy);
}
//转储失败就等一等。
if (!deliverSuc) {
this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
return;
}
}
//计算下一次扫描时的offSet起点。
nextOffset = this.offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
源码中处理转存的核心代码:

如果计算的延迟时间比当前时候靠后就继续创建定时任务,继续等待; 如果已经达到了,就进行下面的消息转存逻辑,将消息捞出后重新存入业务指定的Topic中。
至此,rocketMq4.x源码中的延迟消息的实现流程就大致清楚了,第一版任意时间的延迟消息的实现也就是基于此来实现的。
任意时间延迟实现
- 在messageDelayLevel中再加入一个级别,当时设定的是3h(只是为了新增一个messageQueue用于处理任意时间的延时消息,3h不会起到任何作用)
- 任意时间的消息,需要指定delayLevel为3h,同时在Meesage中的properties中设置expireTime键值对,value是到期时间戳。
- 修改executeOnTimeup中的代码实现,在遍历每一条索引信息时增加判断处理:

这样就实现了任意时间的延时消息。但是这种实现方案性能上面并不出色,不过能够满足公司当前业务的需要。如果你的系统也需要使用rocketMq的任意时间延迟消息的功能并且对性能要求没有那么高的话,相信这种方式可以给你提供帮助。当然也可以使用rocketMq5.0,它基于时间轮的方式已经实现了任意时间的延迟消息。