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

相关推荐
Victor3561 小时前
Redis(72)Redis分布式锁的常见使用场景有哪些?
后端
Victor3561 小时前
Redis(73)如何处理Redis分布式锁的死锁问题?
后端
程序员爱钓鱼3 小时前
Python编程实战 · 基础入门篇 | Python的缩进与代码块
后端·python
earthzhang20216 小时前
第3讲:Go垃圾回收机制与性能优化
开发语言·jvm·数据结构·后端·性能优化·golang
thinktik8 小时前
AWS EKS 集成Load Balancer Controller 对外暴露互联网可访问API [AWS 中国宁夏区]
后端·kubernetes·aws
追逐时光者9 小时前
将 EasySQLite 解决方案文件格式从 .sln 升级为更简洁的 .slnx
后端·.net
驰羽9 小时前
[GO]GORM 常用 Tag 速查手册
开发语言·后端·golang
AntBlack9 小时前
虽迟但到 :盘一盘 SpringAI 现在发展得怎么样了?
后端·spring·openai
ss27310 小时前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
舒一笑11 小时前
🚀 PandaCoder 2.0.0 - ES DSL Monitor & SQL Monitor 震撼发布!
后端·ai编程·intellij idea