楔子:大字半邊牀,口水枕邊流。早上七點的鬧鐘響起,啊,起牀上班;可惡,大牀把我抱住了,起不來,再讓我睡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'