ps:偽延時佇列先賣個關子,我們先了解下延時佇列。
[TOC]
一、什麼是延時佇列
所謂延時佇列是指訊息push到佇列後,監聽的消費者不能第一時間獲取訊息,需要等到指定時間才能消費。
一般在業務裡面需要對某些訊息做定時傳送,不想走定時任務或者是使用者下單之後多長時間自動失效類似的場景可以考慮通過延時佇列實現。
複製程式碼
二、RabbitMQ實現
MQ本身並不支援直接的延時佇列實現,但是我們可以通過RabbitMQ的訊息TTL和Dead Letter規則來實現
-
Time TO Live (TTL): RabbitMQ可以針對Queue設定x-expires 或者 針對Message設定 x-message-ttl,來控制訊息的生存時間
-
Dead Letter 死信 RabbitMQ官網這樣定義死信訊息:
- . 訊息被拒絕(basic.reject或basic.nack)並且requeue=false.
- . 訊息TTL過期
- 佇列達到最大長度(佇列滿了,無法再新增資料到mq中)
- Dead Letter Exchanges(DLX)死信交換機 MQ預設的死信訊息是丟棄的,但是我們可以通過設定以下兩個屬性讓死信訊息轉發到我們指定的佇列。
- x-dead-letter-exchange:出現dead letter之後將dead letter重新傳送到指定exchange
- x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key傳送
-
延時佇列實現: 瞭解了MQ佇列的TTL和Dead Letter之後,我們就可以通過這兩個特性來實現,首先我們通過設定訊息或者佇列的TTL來設定訊息在指定時間後成為死信,再設定死信訊息的路由轉發規則到特定佇列,消費者通過監聽這個特定佇列就能實現延時佇列的效果。
-
程式碼實現
生產者傳送訊息:ttlQueue存放過期時間的佇列,deadLetterQueue死信轉發佇列,seconds是過期時間
public static void sendTTLMsg(String ttlQueue, String deadLetterQueue, Object msg, Integer seconds) {
MqSender.getInstance().setHost(RABBIT_MQ_HOST);
// 獲取到連線以及MQ通道
Connection connection;
try {
connection = MqSender.getInstance().newConnection();
// 從連線中建立通道
Channel channel = connection.createChannel();
// 配置
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "");
args.put("x-dead-letter-routing-key", deadLetterQueue);
channel.queueDeclare(deadLetterQueue, true, false, false, null);
channel.queueDeclare(ttlQueue, true, false, false, args);
// 傳送訊息
channel.basicPublish("", ttlQueue, new AMQP.BasicProperties.Builder().expiration(String.valueOf(seconds)).build(), MAPPER.writeValueAsBytes(msg));
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
消費者通過監聽deadLetterQueue來實現延時訊息監聽
三、 延時佇列的問題
通過我們測試發現,這種方式實現的延時佇列,在佇列設定TTL的情況下是可以正常的,但是如果根據訊息設定了不同的TTL,就會有問題,因為MQ本質上還是訊息佇列中介軟體,佇列是遵循先進先出的,如果有兩個訊息先後入隊,但是後入隊的訊息TTL小於前面的訊息,它必須等待之前的訊息被消費完後才能挪到佇列頭部,這樣不同延時訊息就會出現問題。
通過RabbitMQ官網的文件也介紹了這個問題:
Only when expired messages reach the head of a queue will they actually be discarded (or dead-lettered)
複製程式碼
所以我才稱之為MQ的偽延時佇列,這種延時佇列在訊息TTL不同的情況下並不能實現真正的延時消費。
四、解決RabbitMQ的偽延時方案
既然RabbitMQ無法支援不同TTL訊息的延時消費,那麼如果我們要實現這種功能,有什麼方案呢,在實際業務開發中,我們有這樣的解決方案:
首先我們會建立多級延時消費佇列(比如兩分鐘,三十分鐘,一天三種,具體可以根據業務量和訪問量還有時間精確度來劃分,這裡的兩分鐘、三十分鐘是指佇列統一的TTL),push消費佇列的時候,會根據需要延時的時間,丟到不同的消費佇列,比如小於三十分鐘的我們push到兩分鐘佇列,三十分鐘到一天的放入三十分鐘佇列,超過一天的放入一天佇列,在死信佇列的監聽器做同樣的判斷,如果是小於等於當前時間訊息的,立馬消費,否則按照上述規則繼續迴圈到不同的延時佇列
這種方案解決了多級延時消費的問題,並且能夠較大程度地避免了訊息的重複迴圈,降低MQ的壓力,但是缺點也比較明顯,因為最低是兩分鐘的延時,理論上來說最多會有兩分鐘的誤差,如果對時間要求性比較高的,可以適當調低最低一級別的延時TTL,比如一分鐘或者三十秒
類似程式碼如下:cts是需要消費掉的時間戳
long now = System.currentTimeMillis();
long cts = Long.valueOf(feedComment.getCts());
if (cts - now <= 30 * 60 * 1000) {
MqSender.sendTTLMsg(MqConstants.FEED_COMMENT_DELAY_QUEUE_2MIN, MqConstants.FEED_COMMENT_AUTO_POST_QUEUE, feedComment, 2 * 60);
} else if (cts - now <= 24 * 60 * 60 * 1000) {
MqSender.sendTTLMsg(MqConstants.FEED_COMMENT_DELAY_QUEUE_30MIN, MqConstants.FEED_COMMENT_AUTO_POST_QUEUE, feedComment, 30 * 60);
} else {
MqSender.sendTTLMsg(MqConstants.FEED_COMMENT_DELAY_QUEUE_24HOUR, MqConstants.FEED_COMMENT_AUTO_POST_QUEUE, feedComment, 24 * 60 * 60);
}
複製程式碼