RocketMQ-延时消息实现原理

RocketMQ支持18个延时等级的消息发送,消息首先存储在内部的SCHEDULE_TOPIC_XXXX队列中,由ScheduleMessageService定时任务根据delayLevel拉取并判断消息是否到期,到期则投递到目标Topic。该服务在启动时加载消费进度,并定期持久化。每个延迟等级都有一个定时任务负责处理其队列中的消息。
摘要由CSDN通过智能技术生成

源码版本号:版本号:4.9.4

RocketMQ可以发送延时消息,一共有18个延时等级,例如延时等级 1 代表延时 1s,延时等级 2 代表延时 5s

通过 Message#setDelayTimeLevel 方法设置消息的延时等级。

所有延时时间如下所示

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

Broker进行消息存储的时候会判断消息是否设置了延时等级,如果设置了延时等级则会按照延时消息的逻辑进行处理

public class CommitLog {
    /**
     * 版本号:4.9.4
     * 找到607行
     */
    public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
        // 省略......
        /**
         * 重要处理代码如下
         * 找到624行
         */
        // Delay Delivery
        if (msg.getDelayTimeLevel() > 0) {
            // 判断延时等级是否超过最大, 默认为18
            if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
            }
            // 重新指定topic = SCHEDULE_TOPIC_XXXX
            topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
            // 计算新的 queueId = 延时等级 - 1
            int queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
            // 将真正的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()));

            msg.setTopic(topic);
            msg.setQueueId(queueId);
        }
        // 省略......
    }
}

可以发现,延时消息的存储,并不会直接保存到指定的Topic队列中,而是先保存到内部Topic = SCHEDULE_TOPIC_XXXX的队列里面,
SCHEDULE_TOPIC_XXXX一共有18个队列(MessageQueue),对应着延迟消息的18个等级,
RocektMQ会根据指定的DelayTimeLevel来决定选择哪个MessageQueue,这块的计算逻辑很简单:queueId = DelayTimeLevel - 1

看到这里,可以猜测,肯定会有个定时任务将SCHEDULE_TOPIC_XXXX队列里面满足条件的消息投递到指定的Topic

查看Broker的代码,定时任务在 org.apache.rocketmq.store.schedule.ScheduleMessageService

查看它的启动方法

public class ScheduleMessageService extends ConfigManager {
    // 存放了每个延迟等级对应的延迟时间
    private final ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable =
            new ConcurrentHashMap<Integer, Long>(32);
    // 存放每个延迟等级对应的消费进度
    private final ConcurrentMap<Integer /* level */, Long/* offset */> offsetTable =
            new ConcurrentHashMap<Integer, Long>(32);
    /**
     * 版本号:4.9.4
     * 找到128行
     * 查看启动方法
     */
    public void start() {
        if (started.compareAndSet(false, true)) {
            // 加载一些必要信息
            this.load();
            // 遍历 delayLevelTable , 找到对应的消费进度, 然后进行消费
            for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
                Integer level = entry.getKey();
                Long timeDelay = entry.getValue();
                Long offset = this.offsetTable.get(level);
                if (null == offset) {
                    offset = 0L;
                }

                if (timeDelay != null) {
                    // ......
                    /**
                     * 147行, 生成一个延迟执行的定时任务, 去拉取延时队列里面的消息
                     * level可以定位到要消费的队列
                     * offset可以知道从哪里开始消费
                     */
                    this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
                }
            }
            /**
             * 省略......
             * 后面会生成一个定时任务, 默认每隔10s就会将消费进度 offsetTable 的数据持久化到 delayOffset.json
             */
        }
    }

    @Override
    public boolean load() {
        boolean result = super.load();
        /**
         * 解析延迟等级, 初始化 delayLevelTable 字段
         * 1. 生成一个Map<String, Long>
         *     初始值 "s" -> 1000, "m" -> 1000*60, "h" -> 1000*60*60, "d" -> 1000*60*60*24
         * 2.拿到字符串: "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
         * 2.根据空格分割, 然后遍历
         * 3.比如拿到了"10s", 最后解析成 10 和 s, s 对应 1000, 计算出延时时间为: 10 * 1000
         */
        result = result && this.parseDelayLevel();
        // 加载延时队列的消费进度, 取的是 delayOffset.json, 初始化 offsetTable 字段
        result = result && this.correctDelayOffset();
        return result;
    }
}

ScheduleMessageService启动时会去加载SCHEDULE_TOPIC_XXXX的消费进度,在文件delayOffset.json
内容如下所示,记录了每个队列被消费到哪里

{
	"offsetTable":{1:2,2:1,3:1,4:1,5:1,6:1,7:1}
}

每一个延迟等级都提交了一个 延迟执行的DeliverDelayedMessageTimerTask 任务,上面提交的是一次性的任务,查看它的 run 方法,
可以发现每次拉取完消息后都会再次提交一个延迟执行的DeliverDelayedMessageTimerTask 任务

DeliverDelayedMessageTimerTask里面有两个重要的属性:

delayLevel: 延迟等级,delayLevel - 1得到对应的队列的queueId

offset: 消费进度,可以知道应该从哪里开始消费

里面的执行逻辑大概如下:

  1. 拉取Topic为SCHEDULE_TOPIC_XXXX,queueId = (delayLevel - 1),偏移量为offset 的消息
  2. 遍历拉取到的消息,通过delayLeveldelayLevelTable找到对应的延迟时间间隔,根据消息的存储时间msgStoreTime可以判断当前消息是否应该被投递到指定的Topic
  3. 如果没有到时间,则延迟执行,再生成一个 DeliverDelayedMessageTimerTask 任务。
    由于同一个队列里面的延迟时间是一样的,先进来的消息肯定先到期,所以当前遇到未到期的消息,那么后面的消息肯定也还没有到处理时间。
  4. 处理完拉取到的消息,再次提交一个延迟执行的DeliverDelayedMessageTimerTask 任务。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值