基于RocketMQ4.x版本实现任意时间的延迟消息(一)

由于公司之前新的产品需求中希望企业管理员可以自定义通知推送时间,业务线目前使用的主要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源码中的延迟消息的实现流程就大致清楚了,第一版任意时间的延迟消息的实现也就是基于此来实现的。

任意时间延迟实现

  1. 在messageDelayLevel中再加入一个级别,当时设定的是3h(只是为了新增一个messageQueue用于处理任意时间的延时消息,3h不会起到任何作用
  2. 任意时间的消息,需要指定delayLevel为3h,同时在Meesage中的properties中设置expireTime键值对,value是到期时间戳。
  3. 修改executeOnTimeup中的代码实现,在遍历每一条索引信息时增加判断处理:

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

相关推荐
大学生资源网20 小时前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记20 小时前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记21 小时前
windows系统搭建kafka环境
后端
爬山算法21 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai21 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌21 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量21 小时前
AQS抽象队列同步器原理与应用
后端
9号达人1 天前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337981 天前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso1 天前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net