RocketMQ 定時消息

概述

? ???消息在發送到消息隊列RocketMQ版服務端后并不會立馬投遞,而是根據消息中的屬性延遲固定時間后才投遞給消費者。比如:常見的場景電商交易中超時未支付關閉訂單的場景,通過延時消息在30分鐘后投遞給消息端進行關單。
? ???開源 RocketMQ 針對目前只支持固定精度的定時消息。生產端發送消息,通過設delayTimeLevel時間級別后,可實現消息不立馬被消費者消費到,而是按照18個級別 ("1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";)。比如支持5秒、10秒的Level,那么用戶只能發送5秒延遲或者10秒延遲,不能發送8秒延遲的消息。

實現原理

Message轉化

  1. Producer端設置定時級別,發送延時消息后。
  2. Broker端收到延時消息時,會提前將延遲消息的 topic 進行轉化為進入 Topic 為 SCHEDULE_TOPIC_XXXX的消息,同時 延遲級別 與 消息隊列編號 做固定映射:QueueId = DelayLevel - 1。
 if (msg.getDelayTimeLevel() > 0) {
                if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                    msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
                }
                topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
                queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
                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);
            }

任務檢測

  1. 生成 ConsumeQueue 時,每條消息的 tagsCode 使用【消息計劃消費時間】。ScheduleMessageService 在輪詢 ConsumeQueue 時,可以使用 tagsCode 進行過濾。
  2. 對 SCHEDULE_TOPIC_XXXX 每條消費隊列(一共18個) broker端都會啟動了一個 timer和 timerTask的任務,默認1s執行一次 拉取數據,拉取過程 首先進行系統topic+delayLevel 查詢ConsumeQueue ,然后對比每條消息的延時時間和當前時間對比,發送 到達投遞時間【計劃消費時間】 的消息。將消息進行轉化并按照業務topic封裝好Message寫入到 commit_log 中。
  DeliverDelayedMessageTimerTask#run
  public void executeOnTimeup() {
            ConsumeQueue cq =
                ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
                    delayLevel2QueueId(delayLevel));

            long failScheduleOffset = offset;

            if (cq != null) {
                SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
                if (bufferCQ != null) {
                    try {
                        long nextOffset = offset;
                        int i = 0;
                        ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
                        for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                            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();
                                }  
                            }

                            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) {   // 消息到達可發送時間
                                MessageExt msgExt =
                                    ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
                                        offsetPy, sizePy);

                                if (msgExt != null) { // 將Message消息轉化并將其寫入
                                    try {
                                        MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
                                         
                                        PutMessageResult putMessageResult =
                                            ScheduleMessageService.this.writeMessageStore
                                                .putMessage(msgInner);

                                        if (putMessageResult != null
                                            && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
                                            continue;
                                        }  
                                    } catch (Exception e) {
                                        
                                    }
                                }
                            } else {  // 安排下一次任務
                                ScheduleMessageService.this.timer.schedule(
                                    new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
                                    countdown);
                                ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                                return;
                            }
                        } 

                        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();
                    }
                }  
            ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
                failScheduleOffset), DELAY_FOR_A_WHILE);
        }
  1. 當延時消息寫成功后,先更新延時消息消費進度(內存中),同時定時消息發送進度存儲在文件(../config/delayOffset.json)里,每 10s 定時持久化發送進度。
  2. 若發送延時消息失敗后,MQ會安排下一次任務繼續發送直到成功。

優缺點

優點:設計簡單,把所有相同延遲時間的消息都先放到一個隊列中,定時掃描,可以保證消息消費的有序性

缺點:定時器采用了timer,timer是單線程運行,如果延遲消息數量很大的情況下,可能單線程處理不過來,造成消息到期后也沒有發送出去的情況

總結

圖片.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容