RocketMQ 延遲消息

概述

RocketMQ 支持發送延遲消息,但不支持任意時間的延遲消息的設置,僅支持內置預設值的延遲時間間隔的延遲消息。

預設值的延遲時間間隔爲:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h

在消息創建的時候,調用 setDelayTimeLevel(int level) 方法設置延遲時間。broker在接收到延遲消息的時候會把對應延遲級別的消息先存儲到對應的延遲隊列中,等延遲消息時間到達時,會把消息重新存儲到對應的topic的queue裏面。

broker 處理延遲消息

CommitLog.putMessage()

public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    // 設置消息的存儲時間
    msg.setStoreTimestamp(System.currentTimeMillis());
    // 設置消息體的校驗位
    msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    
    AppendMessageResult result = null;

    StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();

    String topic = msg.getTopic();
    int queueId = msg.getQueueId();

    // 獲取消息的 SysFlag 
    final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    // 1、非事務消息 或 已commit事物消息
    if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
        || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
        // 2、判斷消息是否設置延遲
        if (msg.getDelayTimeLevel() > 0) {
            // 3、判斷設置的延遲等級是否大於最大級別,如果大於最大值,則設置最大值(默認最大級別爲18)
            if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
            }
            // 4、延遲消息的 Topic 名稱爲 “SCHEDULE_TOPIC_XXXX”
            topic = ScheduleMessageService.SCHEDULE_TOPIC;
            // 5、根據延遲級別獲取對應的 Queue 。一個延遲級別對應一個 Queue
            queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

            // 6、消息原始的 Topic 名稱和 QueueId 備份保存到 property 中
            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()));
            // 7、修改消息的 topic 和 queueId,讓該消息先投遞到延遲消息隊列中
            msg.setTopic(topic);
            msg.setQueueId(queueId);
        }
    }
    // 省略代碼
    ........
}

1、判斷該消息類型,如果是非事物消息或事物已commit消息,才能處理延遲消息。
2、判斷該消息是否設置延遲,如果延遲級別大於零,則說明該消息時延遲消息。
3、判斷設置的延遲等級是否大於最大級別,如果大於最大值,則設置最大值(默認最大級別爲18)
4、延遲消息的 Topic 名稱爲 “SCHEDULE_TOPIC_XXXX”
5、根據延遲級別獲取對應的 Queue 。一個延遲級別對應一個 Queue
6、消息原始的 Topic 名稱和 QueueId 備份保存到 property 中
7、修改消息的 topic 和 queueId,讓該消息先投遞到延遲消息隊列中

延遲消息級別

MessageStoreConfig.java

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

解析初始化延遲級別

// 存儲消息級別對應的延遲時間
private final ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable =
        new ConcurrentHashMap<Integer, Long>(32);

// 解析並初始化消息延遲級別
public boolean parseDelayLevel() {
    // 時間單位
    HashMap<String, Long> timeUnitTable = new HashMap<String, Long>();
    timeUnitTable.put("s", 1000L);
    timeUnitTable.put("m", 1000L * 60);
    timeUnitTable.put("h", 1000L * 60 * 60);
    timeUnitTable.put("d", 1000L * 60 * 60 * 24);
    // 獲取 messageDelayLevel 定義的延遲消息信息
    String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
    try {
        String[] levelArray = levelString.split(" ");
        for (int i = 0; i < levelArray.length; i++) {
            String value = levelArray[i];
            String ch = value.substring(value.length() - 1);
            Long tu = timeUnitTable.get(ch);

            int level = i + 1;
            if (level > this.maxDelayLevel) {
                this.maxDelayLevel = level;
            }
            long num = Long.parseLong(value.substring(0, value.length() - 1));
            long delayTimeMillis = tu * num;
            this.delayLevelTable.put(level, delayTimeMillis);
        }
    } catch (Exception e) {
        log.error("parseDelayLevel exception", e);
        log.info("levelString String = {}", levelString);
        return false;
    }

    return true;
}

解析messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; 字符串,並每一個延遲時間對應一個延遲級別,存儲到 delayLevelTable 中。

用戶只需要設置延遲級別,然後通過 delayLevelTable 就知道該級別對應的延遲時間是多少。

處理延遲消息

public void start() {
    // 爲每一個延遲級別設置一個定時任務處理消息的投遞
    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) {
            this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
        }
    }
    // 定時持久化 每個消息級別處理對應queue的offset信息
    this.timer.scheduleAtFixedRate(new TimerTask() {

        @Override
        public void run() {
            try {
                ScheduleMessageService.this.persist();
            } catch (Throwable e) {
                log.error("scheduleAtFixedRate flush exception", e);
            }
        }
    }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}

1、爲每一個延遲隊列創建一個定時任務,定時處理延遲隊列中的數據,把該數據從延遲隊列中取出,然後投遞到實際發送的消息隊列(queue)中。

2、定時持久化每個消息級別處理對應queue的offset信息。(啓動後延遲10秒開始持久化,以後每間隔10秒保存一次)

延遲消息投遞

在 DeliverDelayedMessageTimerTask 中處理延遲消息的投遞,代碼如下:

public void executeOnTimeup() {
    // 根據 topic 和 queueId 獲取延遲隊列對應的 ConsumeQueue
    ConsumeQueue cq =
            ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
                    delayLevel2QueueId(delayLevel));

    long failScheduleOffset = offset;

    if (cq != null) {
        // 通過偏移量獲取延遲隊列 MappedFile (MappedFile 對應的 Buffer)
        // ConsumerQueue 中每個消息存儲的長度爲20位,而 offset 是消息的個數,實際的偏移量爲 offset * 20
        SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
        if (bufferCQ != null) {
            try {
                long nextOffset = offset;
                int i = 0;
                ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
                // bufferCQ.getSize() 爲延遲隊列中可以讀取到的延遲消息長度(包括已到時間和未到實際的數據)
                // ConsumeQueue.CQ_STORE_UNIT_SIZE 爲20。 ConsumerQueue 中每個消息固定的長度。
                for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                    // 從ConsumerQueue 中獲取一條消息。
                    // 消息包括3部分:物理偏移量、消息大小、Tag的HashCode
                    // 這裏的tagsCode在延遲消息隊列中存儲是存儲在 【延遲隊列中的時間 + 延遲的時間】(通過這個時間來確定消息是否達到延遲的時間)
                    long offsetPy = bufferCQ.getByteBuffer().getLong();
                    int sizePy = bufferCQ.getByteBuffer().getInt();
                    long tagsCode = bufferCQ.getByteBuffer().getLong();

                    // 通過 tagsCode 來判斷是否存儲的是延時時間
                    // 如果是 Tag 的 hashcode ,那麼最大值爲 Integer.Max
                    // 如果是 延遲時間,時間爲long類型,肯定大於 Integer.Max
                    if (cq.isExtAddr(tagsCode)) {
                        //獲取延遲發送時間
                        if (cq.getExt(tagsCode, cqExtUnit)) {
                            tagsCode = cqExtUnit.getTagsCode();
                        }
                        // 從commitLog中獲取存儲時間,然後從新計算延遲發送時間。延遲時發送時間=消息發送到延遲隊列存儲時間+延遲時間
                        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;
                    // countdown <=0 是需要馬上投遞的延遲消息
                    if (countdown <= 0) {
                        // 從 CommitLog 中獲取當前消息的信息
                        MessageExt msgExt =
                                ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
                                        offsetPy, sizePy);

                        if (msgExt != null) {
                            try {
                                // 這裏從 property 中解析出正真的 Topic、QueueId、TagCode 信息,存儲到 msgInner 中。
                                MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
                                // 消息投遞,跟 producer 發送消息處理流程一樣。
                                PutMessageResult putMessageResult =
                                        ScheduleMessageService.this.defaultMessageStore
                                                .putMessage(msgInner);
                                // 如果處理成功,則繼續下一條處理
                                if (putMessageResult != null
                                        && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
                                    continue;
                                } 
                                // 如果處理失敗
                                else {
                                    // 打印失敗信息
                                    log.error("ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
                                            msgExt.getTopic(), msgExt.getMsgId());
                                    // 則從新創建一個定時任務
                                    ScheduleMessageService.this.timer.schedule(
                                            new DeliverDelayedMessageTimerTask(this.delayLevel,
                                                    nextOffset), DELAY_FOR_A_PERIOD);
                                    //並記錄下處理延遲隊列的 offset
                                    ScheduleMessageService.this.updateOffset(this.delayLevel,
                                            nextOffset);
                                    return;
                                }
                            } catch (Exception e) {
                                log.error(
                                        "ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
                                                + msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
                                                + offsetPy + ",sizePy=" + sizePy, e);
                            }
                        }
                    }
                    // 未到投遞時間
                    else {
                        // 重新創建一個定時任務,延遲 countdown 長時間在執行
                        ScheduleMessageService.this.timer.schedule(
                                new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
                                countdown);
                        // 更新延遲隊列待處理消息的 offset
                        ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                        return;
                    }
                } // end of for

                nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
                ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
                        this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
                ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                return;
            } finally {

                bufferCQ.release();
            }
        } // end of if (bufferCQ != null)
        else {

            long cqMinOffset = cq.getMinOffsetInQueue();
            if (offset < cqMinOffset) {
                failScheduleOffset = cqMinOffset;
                log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
                        + cqMinOffset + ", queueId=" + cq.getQueueId());
            }
        }
    } 
    // 如果出現異常,則創建一個100毫秒延遲的定時任務
    ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
            failScheduleOffset), DELAY_FOR_A_WHILE);
}

這裏的註釋已經寫的很清楚了,就不解釋了。

延遲消息 TagCode 值

public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, final boolean checkCRC,
        final boolean readBody) {
        // 省略代碼
        ......

       // Timing message processing
        {
            String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
            if (ScheduleMessageService.SCHEDULE_TOPIC.equals(topic) && t != null) {
                int delayLevel = Integer.parseInt(t);

                if (delayLevel > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                    delayLevel = this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel();
                }
                // 如果是延遲消息隊列,則ConsumerQueue中的 tagsCode 存儲的是要投遞的時間(存儲時間+延遲時間)
                if (delayLevel > 0) {
                    tagsCode = this.defaultMessageStore.getScheduleMessageService().computeDeliverTimestamp(delayLevel,
                                storeTimestamp);
                }
            }
        //省略代碼
        ......
}

從這裏看出,如果是延遲消息,則 TagCode 中存儲的是消息需要投遞到正在消息隊列的時間。而不是 Tag 的 hashcode 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章