RocketMQ進擊(四)定時消息(延時隊列)


楔子:大字半邊牀,口水枕邊流。早上七點的鬧鐘響起,啊,起牀上班;可惡,大牀把我抱住了,起不來,再讓我睡10分鐘吧。嗯,好吧,原來是個夢,我在高速服務區睡着了。前者是定時指令/消息,定好鬧鐘後,每天早上到點就會叫你起牀;後者是延時指令/消息,它會延遲當前的事情到相對於現在之後的某個時間點再做。但不管是定時還是延時,他們都有一個共同點:大腦到了這個時間點,它就是觸發並工作,讓你起牀去上班。因爲其本質都是一種相對的延遲再做。

像這樣的定時消息和延時消息經常會出現的我們的生活中:

  • 週一早上10點項目早會;週五同事們約好晚上六點去外面喫一頓;等等類似,是定時消息
  • 週二下午2點的會議推遲半小時再開;週四晚上六點的上線要延遲到22點;等等類似,是延時消息

 

1. 似而不同

  • 定時消息:Producer 將消息發送到消息隊列 MQ 服務端,但並不期望這條消息立馬投遞,而是推遲到在當前時間點之後的某一個時間投遞到 Consumer 進行消費,該消息即定時消息。
  • 延時消息:Producer 將消息發送到消息隊列 MQ 服務端,但並不期望這條消息立馬投遞,而是延遲一定時間後才投遞到 Consumer 進行消費,該消息即延時消息。

定時消息與延時消息雖然在理解角度有一些差異,但是最終達到的效果相同:消息在發送到消息隊列 MQ 服務端後並不會立馬投遞,而是根據消息中的屬性延遲固定時間後才投遞給消費者。因爲其本質都是一種相對的延遲再消費。

 

2. 到點出場

定時消息/延時消息適用於以下一些場景:

  • 消息生產和消費有時間窗口要求:比如在電商交易中超時未支付關閉訂單的場景,用戶提交了一個訂單就可以發送一個延時消息,1h後去檢查這個訂單的狀態,如果還是未付款就取消訂單釋放庫存。
  • 通過消息觸發一些定時任務,比如在某一固定時間點向用戶發送提醒消息。

 

2.2.1 源碼示例

定時/延時消息生產者(Producer )

package com.meiwei.service.mq.tcp.producer;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.util.Date;

/**
 * 定時消息(延遲隊列) - 生產者
 */
public class DelayTimeMqProducer {

    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";

    // Tag 爲 Message 所屬的二級分類,比如初中可分爲初一、初二、初三;高中可分爲高一、高二、高三
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_DELAY_TIME";

    public static void main(String[] args) throws Exception {
        // 聲明並實例化一個 producer 生產者來產生消息
        // 需要一個 producer group 名字作爲構造方法的參數
        DefaultMQProducer producer = new DefaultMQProducer("meiwei-producer-delay-time");

        // 指定 NameServer 地址列表,多個nameServer地址用半角分號隔開。此處應改爲實際 NameServer 地址
        // NameServer 的地址必須有,但也可以通過啓動參數指定、環境變量指定的方式設置,不一定要寫死在代碼裏
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 在發送MQ消息前,必須調用 start 方法來啓動 Producer,只需調用一次即可
        producer.start();

        // 循環發送MQ測試消息
        String content = "";
        for (int i = 0; i < 3; i++) {
            // 配置容災機制,防止當前消息異常時阻斷髮送流程
            try {
                content = "【MQ測試消息】延時消息發送 " + i + ",時間 " + new Date();

                // Message Body 可以是任何二進制形式的數據,消息隊列不做任何干預,需要 Producer 與 Consumer 協商好一致的序列化和反序列化方式
                Message message = new Message(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH, "KEY" + i, content.getBytes(RemotingHelper.DEFAULT_CHARSET));

                // 默認值爲“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18個level
                // 設置延時等級3,這個消息將在 10s 之後發送
                message.setDelayTimeLevel(3);
                // 發送消息
                SendResult sendResult = producer.send(message);

                // 日誌打印
                System.out.printf("Send MQ message success! Topic: %s,Tag: %s, msgId: %s, Message: %s %n",
                        message.getTopic(), message.getTags(), sendResult.getMsgId(), new String(message.getBody()));
            } catch (Exception e) {
                // 消息發送失敗
                System.out.printf("%-10d Exception %s %n", i, e);
                e.printStackTrace();
            }
        }

        // 在發送完消息之後,銷燬 Producer 對象。如果不銷燬也沒有問題
        producer.shutdown();
    }
}

在應用層初始化 Message 消息對象之後,調用 Message.setDelayTimeLevel(int level) 方法來設置延遲級別,按照序列取相應的延遲級別,例如 level=3,則延遲爲 10s 再發送消息。 

RocketMQ 目前只支持固定精度時間的延時消息發送(配置 Message.setDelayTimeLevel 延時精度),默認有18個時間精度的 level,分別是:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

  • 這個配置項配置了從1級開始,各級延時的時間,可以修改這個指定級別的延時時間
  • 時間單位支持:s、m、h、d,分別表示秒、分、時、天
  • 默認值就是上面聲明的,可手工調整
  • 默認值已夠用,不建議修改這個值

 

定時/延時消息消費者(Consumer )【Push模式】

package com.meiwei.service.mq.tcp.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.Calendar;
import java.util.List;

/**
 * 定時消息(延遲隊列) - 消費者
 */
public class DelayTimeMqConsumer {

    // Message 所屬的 Topic 一級分類,須要與提供者的頻道保持一致才能消費到消息內容
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_DELAY_TIME";

    public static void main(String[] args) throws Exception {

        // 聲明並初始化一個 consumer
        // 需要一個 consumer group 名字作爲構造方法的參數
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("meiwei-consumer-delay-time");

        // 同樣也要設置 NameServer 地址,須要與提供者的地址列表保持一致
        consumer.setNamesrvAddr("127.0.0.1:9876");

        // 設置 consumer 所訂閱的 Topic 和 Tag,*代表全部的 Tag
        consumer.subscribe(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH);

        // 註冊消息監聽者
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                list.forEach(mq->{
                    Calendar calendar = Calendar.getInstance();
                    String timeNow = new String(calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE) + ":" + Calendar.SECOND);
                    System.out.printf("TimeNow: %s, Thread: %s, Topic: %s, Tags: %s, Message: %s",
                            timeNow,
                            Thread.currentThread().getName(),
                            mq.getTopic(),
                            mq.getTags(),
                            new String(mq.getBody()));
                    System.out.println();
                });

                // 返回消費狀態
                // CONSUME_SUCCESS 消費成功
                // RECONSUME_LATER 消費失敗,需要稍後重新消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 調用 start() 方法啓動 consumer
        consumer.start();
        System.out.println("DelayTime Consumer Started.");
    }
}

 

2.2.2 測試結果

定時/延時消息生產者(Producer)發送結果:

定時/延時消息消費者(Consumer)消費結果:

可以看到消息的消費比存儲時間晚了10秒。 


參考資料:
RocketMQ 官網:http://rocketmq.apache.org/docs/motivation/
阿里雲消息隊列 MQ:https://help.aliyun.com/document_detail/29532.html
阿里巴巴中間件團隊:http://jm.taobao.org/2016/11/29/apache-rocketmq-incubation/


RocketMQ進擊物語:
RocketMQ進擊(零)RocketMQ這個大水池子
RocketMQ進擊(一)Windows環境下安裝部署Apache RocketMQ
RocketMQ進擊(二)一個默認生產者,兩種消費方式,三類普通消息詳解分析
RocketMQ進擊(三)順序消息與高速公路收費站
RocketMQ進擊(四)定時消息(延時隊列)
RocketMQ進擊(五)集羣消費模式與廣播消費模式
RocketMQ進擊(六)磕一磕RocketMQ的事務消息
RocketMQ進擊(七)盤一盤RocketMQ的重試機制
RocketMQ進擊(八)RocketMQ的日誌收集Logappender
RocketMQ異常:RocketMQ順序消息收不到或者只能收到一部分消息
RocketMQ異常:Unrecognized VM option 'MetaspaceSize=128m'

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