RabbitMQ 基礎概念進階

上一篇 RabbitMQ 入門之基礎概念 介紹了 RabbitMQ 的一些基礎概念,本文再來介紹其中的一些細節和其它的進階的概念。

一、消息生產者發送的消息不可達時如何處理

RabbitMQ 提供了消息在傳遞過程中無法發送到一個隊列(比如根據自己的類型和路由鍵沒有找到匹配的隊列)時將消息回傳給消息發送方的功能,使用 RabbitMQ 的客戶端提供 channel.basicPublish 方法的兩個參數 mandatory 和 immediate (RabbitMQ 3.0 以下版本),除此之外還提供了一個備份交換器可以將無法發送的消息存儲起來處理,不用重新傳回給發送方。

1.1 mandatory 參數

mandatory 被定義在 RabbitMQ 提供的客戶端的 channel.basicPublish 方法中,如下所示:

image.png

當把方法的 mandatory 參數設置為 true 時,那麼會在交換器無法根據自身的類型和路由鍵找到一個符合要求的隊列時,RabbitMQ 會自動調用 Basic.Return 把該消息回傳給發送方也就是我們的消息生產者。反之,如果設置為 false 的話,消息就會被直接丟棄掉。那麼問題來了,我們要如何去獲取這些沒有被發送出去的消息呢?RabbitMQ 給我們提供了事件監聽機制來獲取這種消息,可以通過 addReturnListener 方法添加一個 ReturnListener 來獲取這種未發送到隊列的消息,如下所示:

image.png

通過查看 ReturnListener 介面的源碼可以看到,該介面只有一個方法,如果是 JDK8+ 的版本的話可以使用 Lambda 表達式來簡化一些程式碼。

可以看出,當設置了 mandatory 參數時,還必須為生產者同時添加 ReturnListener 監聽器的編程邏輯,這樣就會使得生產者的程式碼變得更加複雜了,為了處理這種情況,RabbitMQ 提供了 `備份交換器` 來將沒有成功路由出去的消息存儲起來,當我們需要的時候再去處理即可。

1.2 immediate 參數

該的參數同樣也是在channel.basicPublish 方法中定義的,其官方描述如下:

This flag tells the server how to react if the message cannot be routed to a queue consumer immediately. If this flag is set, the server will return an undeliverable message with a Return method. If this flag is zero, the server will queue the message, but with no guarantee that it will ever be consumed.

當把 immediate 參數設置為 true 時,如果交換器根據其類型和路由鍵找到符合要求的隊列時,發現所有隊列上沒有任何消費者,則該消息並不會存入到隊列中,會通過 Basic.Return 命令把消息回傳給生產者。簡而言之也就是說,當設置了 immediate 參數時,該消息關聯的隊列上存在消費者時,會立即發送消息到該隊列中,反之如果匹配的隊列上不存在任何消費者,則直接把消息回傳給生產者。這裡有一點需要注意的是:從 RabbitMQ 3.0 + 已經去除了該參數。

二、如何對消息和隊列設置過期時間 (TTL)

TTL 是 time to live 首字母的簡稱,RabbitMQ 中可以設置消息和隊列的過期時間,我們先來看看要如何設置消息的過期時間。

1.1 消息 TTL 設置

RabbitMQ 提供了兩種設置消息的過期時間,第一種是通過隊列的屬性設置,該方式的特點就是隊列中所有消息的過期時間都一致。還有一種是更小粒度的設置,就是對每條消息單獨設置過期時間,這種方式更加靈活,每條消息的過期時間都可以不一樣。這是你可能會問,如果同時設置了隊列的過期屬性和消息本身的過期屬性,最終以哪個為準呢?結果是 RabbitMQ 會比較這兩個 TTL 的值大小,以較小的那個為準。很容易想到,通過隊列的屬性的方式設置過期時間的話是在聲明隊列的時候指定,對應到客戶端就是其提供的 channel.queueDeclare 方法的參數 arguments 指定,示例程式碼如下:

image.png

需要注意的是 x-message-ttl 參數的單位是毫秒。如果不設置 TLL,則表示該消息不會過期,如果將 TTL 設置為 0,表示除非此時可以把消息直接發送投遞到消費者端去,否則就會直接丟棄該消息。

准對每條消息設置 TTL 的方法是在發送消息的時候設置的,對應到客戶端方法是 channel.basicPublish 的 expiration 屬性參數,具體設置程式碼如下:

image.png

這種設置方式,即使隊列過期也不會立即從隊列中移除,因為每條消息是否過期的判定是在發送到消費者是才進行的,如果此時發現已經過期才會刪除消息。而對於第一種方式則會把已經過期的消息移到隊列頭部,然後 RabbitMQ 只要定期的從頭開始掃描是否存在過期的消息即可。

1.2 隊列 TTL 設置

設置隊列的過期時間使用的是客戶端的 channel.queueDeclare 方法參數中的 x-expires 參數,其單位同樣也是毫秒,不過需要注意的是它不能設置為 0。設置隊列過期的程式碼如下所示:

image.png

上面程式碼創建了一個過期時間為 15 分鐘的隊列。

三、死信隊列介紹

死信交換器(DLX)的全稱是 Dead-Letter-Exchange ,也稱之為死信郵箱。簡單來說就是當一個消息由於 消息被拒絕 、 消息過期 、 隊列達到最大長度 時,變成死信(dead message)之後,會被重新發送到一個交換器中,這個交換器就是死信交換器,綁定在這個交換器上的隊列就稱之為死信隊列。死信交換器實際上就是平常的交換器,可以在任何隊列上指定,當在一個隊列上設置死信交換器後,如果該隊列出現死信時就會被 RabbitMQ 把死信消息重新發送到死信交換器上去,然後路由到死信隊列中,我們可以監聽這個隊列來處理那些死信消息。為一個隊列設置死信交換器是在生產者的聲明隊列的方法中設置 x-dead-letter-message 參數來實現的,如下所示:

image.png

同時也可以通過 x-dead-letter-routing-key 參數設置死信交互器的路由鍵,不設置默認使用原始度列的路由鍵。可以到 RabbitMQ 的後台管理介面,有 DLX 標誌的就是死信隊列。

image.png

RabbitMQ 提供的 DLX 是個比較實用的功能特性,它可以在我們消息不能被消費者正確消費的情況下放入到死信隊列,後續我們可以通過這個死信隊列的內容來查看異常情況來改造和優化系統。

四、延遲隊列介紹

顧名思義,延遲隊列存儲的是哪些需要等待指定時間後才能拿到的延遲消息,一個比較典型的場景就是訂單 30 分鐘後未支付取消訂單。這裡需要注意的是,在 RabbitMQ 中並沒有直接提供延遲隊列的功能,而是需要通過上面介紹的過期時間(TTL)和死信隊列一起來實現,比如超時取消訂單這個場景,我們可以讓消費者訂閱死信隊列,設置正常的那個隊列的超時時間為 30 分鐘並綁定到該死信隊列上,當消息超過 30 分鐘未被處理後消息就會把發送到死信隊列中,然後死信隊列的消費者就可以在 30 分鐘後成功的消費到該消息了。

image.png

同時當我們有其它的超時配置需求時也很方便擴展,比如可以在生產者發送消息的時候通過設置不同的路由鍵,通過路由鍵來將消息發送到與交換器綁定的不同隊列中,然後這些隊列分別設置不同的過期時間和與之相對應的死信隊列,當消息過期時就會被 RabbitMQ 轉發到相應的死信隊列中,這樣就可以去訂閱相應的死信隊列即可。

五、交換器、消息和隊列持久化

持久化可以提高可靠性,可以防止宕機或者重啟等異常下數據的丟失,RabbitMQ 的持久化從組成結構上可以分為三個部分,即交換器持久化、消息持久化和隊列持久化。

1.1 交換器持久化

交換器持久化是在聲明交換器時將 durable 參數設置為 true 來實現的。如果不設置持久化屬性的話,當 RabbitMQ 服務重啟後交換器的數據就會丟失,需要注意的是,是交換器的數據丟失,消息不會丟失,只是不能將消息發送到這個交換器中了,一般生產環境使用都會把該屬性設置為持久化。

1.2 消息持久化

交換器的持久化僅僅只是保證了交換器本身的元數據不會丟失,無法保證其存儲的消息不會丟失,如果需要其內部存儲的消息不丟失,則需要設置消息的持久化,通過將消息的投遞模式(deliveryMode)設置為 2 即可實現消息的持久化,如下所示:

image.png

需要消息持久化的前提是其所在的隊列也要設置持久化,假如僅僅只設置消息的持久化的話,RabbitMQ 重啟之後隊列消失,然後消息也會丟失。這裡有點需要注意一下,雖然持久化可以提高可靠性,但是持久化是將數據存儲到硬碟上,比直接操作記憶體要慢很多,所以對於哪些可靠性要求不高的業務不需要進行持久化。

1.3 隊列持久化

隊列的持久化的設置和交換器持久化類似,同樣也是在聲明的時候通過 durable 參數設置為 true 實現的,如果不設置,當 RabbitMQ 重啟後,相關的隊列元數據也會丟失,相應的其存儲的消息也會隨之丟失掉。

將交換器、隊列、消息都設置了持久化之後就能百分之百保證數據不丟失了嗎?其實無法保證百分之百數據不丟失。比如消費者在訂閱消費隊列時將自動應答(autoAck)參數設置為 true 的話,在接收到消息後還沒來得及處理就掛了,這時需要把自動應答設置 false,進行手動 ack 應答即可。還有一個就是由於不是實時持久化存檔,當消息存檔的過程中 RabbitMQ 宕機了,此時也會發生數據丟失,此時需要通過 RabbitMQ 的 鏡像隊列機制 來處理了。

五、總結

本文主要介紹了一些參數具體使用時的設置細節和死信隊列、延遲隊列以及持久化等,還有一些比較重要的點沒有涉及到,比如消息確認機制。「紙上得來終覺淺,絕知此事要躬行」,在了解一些基礎的概念之後還是需要通過具體編碼實踐才能對其更加理解深刻。

Tags: