使用 RabbitMQ 和 RocketMQ 的人是幸運的,因為這兩個 MQ 自身提供了延遲佇列的實現,不像用 Kafka 的同學那麼苦逼,還要自己實現延遲佇列。當然,這都是題外話,今天咱們重點來聊聊 RabbitMQ 延遲佇列的實現原理,以及 RabbitMQ 實現延遲佇列的優缺點有哪些?
很多人知道使用 RabbitMQ 是可以實現延遲佇列的,但對於 RocketMQ 自身也提供了延遲佇列這件事卻持有不同態度,這是因為網上有些資料說 RocketMQ 和 Kafka 沒有內建延遲佇列。其實這種說法是因為,RocketMQ 在早期版本中確實沒有內建延遲佇列,但在 4.x 就內建了 18 個級別的延遲佇列了(最長支援 2 小時的延遲佇列),5.x 就支援隨機延遲時間的延遲佇列了,所以這裏需要特殊強調一下。
1.什麼是延遲佇列?
延遲佇列(Delay Queue)是一種特殊型別的佇列,它的主要特點是可以讓進入佇列的元素在指定的延遲時間之後才被取出進行處理。
延遲佇列的主要使用場景有以下這些:
訂單超時處理:在電商系統中,如果使用者下單後未在一定時間內支付,訂單可能會被自動取消。可以將訂單放入延遲佇列,在設定的延遲時間(如 30 分鐘)後取出處理取消操作。
任務重試:當某個任務執行失敗時,將其放入延遲佇列,等待一段時間(如 5 分鐘)後重新執行。
訊息延遲傳送:某些訊息不需要立即傳送,而是在指定的延遲時間後傳送,例如定時提醒訊息。
快取過期處理:快取中的資料可能有一定的有效期,將即將過期的資料放入延遲佇列,到期後進行刪除或更新操作。
2.延遲任務實現方法
那麼延遲佇列的實現方式有哪些呢?
延遲佇列的實現方式通常有以下幾種:
基於 JDK 提供的 DelayQueue 來實現:它是記憶體級別的延遲佇列,重啟應用之後訊息會丟失,並且只支援單機版延遲佇列,所以一般不用。
基於 MQ 的延遲佇列:例如使用 RabbitMQ 來實現延遲佇列,他們適合處理動態和臨時延遲任務,不像定時任務一樣,適合處理正式的、固定的延遲任務。
基於定時任務元件實現延遲任務:例如 XXLJob 或 Quartz 等框架來實現延遲任務,他們適合處理固定(執行)頻率的延遲任務。
我們通常會使用延遲佇列來儲存(和實現)延遲訊息,所以大部分時候,我們說的延遲佇列和延遲訊息其實是一回事。
3.使用RabbitMQ實現延遲佇列
使用 RabbitMQ 實現延遲佇列有以下兩種實現方式:
透過死信佇列實現延遲任務:將正常的訊息放到沒有訊息訂閱者的訊息佇列(訊息自然就會過期),等訊息過期之後會進入死信佇列,透過訂閱死信佇列消費訊息,從而實現延遲佇列,如下圖所示:
透過官方提供的延遲外掛實現延遲功能。
早期大部分公司都會採用第一種方式,而隨著 RabbitMQ 3.5.7(2015 年底釋出)的延遲外掛的釋出,因為其使用更簡單、更方便,所以它現在纔是大家普通會採用的,實現延遲佇列的方式。
3.1 實現原理分析
使用延遲外掛的實現原理是透過建立一個延遲交換機(Delay Exchange),延遲訊息首先會把訊息投遞到延遲交換機,並不是直接將訊息投遞業務佇列(所以不會立即執行),由延遲交換機控制訊息在延遲一段時間後,再將訊息投遞到真正的佇列中進行消費,從而實現延遲佇列,它的實現流程如下圖所示:
其中 Mnesia 可以理解為基於檔案儲存的資料庫。
3.2 優缺點分析
使用死信佇列實現延遲任務有個缺點,它不能實現隨機延遲任務,每個無消費者的佇列上只能設定一個 ttl(訊息過期時間),所以只能實現固定過期時間的延遲任務。
使用延遲外掛實現延遲任務有以下兩個缺點:
訊息丟失問題:訊息在真的被投遞到目標訊息佇列之前,是存放在接收到了這個訊息的服務端本地的 Mnesia 裡面。也就是說,如果這個時候還沒有重新整理磁碟,那麼訊息就會丟失;如果這個節點不可用了,那麼訊息也同樣會丟失。
高併發問題:這種實現方式不支援高併發場景,因為它只有一個延遲交換機,當高併發或資料量比較大時執行效率就會比較低。