基于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 分钟前
后端视角:拆解春晚背后的高可用技术架构
后端·架构
心之语歌32 分钟前
flutter provider 使用,状态管理更新跨组件数据共享
后端·flutter
Loo国昌44 分钟前
【AI应用开发实战】05_GraphRAG:知识图谱增强检索实战
人工智能·后端·python·语言模型·自然语言处理·金融·知识图谱
颜酱1 小时前
差分数组:高效处理数组区间批量更新的核心技巧
javascript·后端·算法
用户908324602731 小时前
Spring AI 1.1.2 集成 MCP(Model Context Protocol)实战:以 Tavily 搜索为例
java·后端
玹外之音1 小时前
告别 STDIO/SSE:Spring AI Streamable HTTP MCP 实战指南
后端·spring
茶杯梦轩1 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
服务器·后端·面试
故事到这儿1 小时前
Python实现跨机器隧道打通
后端
Java后端的Ai之路2 小时前
【JDK】-JDK 11 新特性内容整理(很全面)
java·开发语言·后端·jdk
小王同学^ ^2 小时前
OpenClaw 多智能体实战:从创建Agent到飞书多通道接入完全指南
后端