基于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,它基于时间轮的方式已经实现了任意时间的延迟消息。

相关推荐
金銀銅鐵9 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路9 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x10 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn12 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈12 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805113 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234513 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒13 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记13 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk5987813 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端