《RabbitMQ》| 解決訊息延遲和堆積問題

蔡不菜丶發表於2021-11-08

大家好,我是小菜。
一個希望能夠成為 吹著牛X談架構 的男人!如果你也想成為我想成為的人,不然點個關注做個伴,讓小菜不再孤單!

本文主要介紹 RabbitMQ的常見問題

如有需要,可以參考

如有幫助,不忘 點贊

微信公眾號已開啟,小菜良記,沒關注的同學們記得關注哦!

  • 訊息可靠性問題:如何確保傳送的訊息至少被消費一次?
  • 延遲訊息問題:如何實現訊息的延遲投遞?
  • 訊息堆積問題:如何解決數百萬級以上訊息堆積,無法及時消費問題?

我們在上篇已經說明了如何解決訊息丟失的問題,也就是保證了訊息的可靠性,那麼其餘兩個問題同樣重要,這篇我們將講述其餘兩個問題的解決方式~!

訊息丟失解決方案:《RabbitMQ》 | 訊息丟失也就這麼回事

一、延遲訊息

延遲訊息 字面意思就是讓延遲接收訊息,那麼如何能讓訊息延遲到達?這就是我們要思考解決的問題,在瞭解延遲佇列之前我們需要先明白 RabbitMQ 中的兩個概念

  • 死信交換機
  • TTL

1)死信交換機

死信(dead letter),也就是廢棄已死亡的訊息,那什麼情況下一個普通的訊息能夠成為死信?需要符合以下三個條件:

  1. 消費者使用 basic.rejectbasic.nack 宣告消費失敗,並將訊息的 requeue 引數設定為 false
  2. 訊息是一個過期訊息,超時後無人消費
  3. 要投遞的佇列訊息堆積滿了,最早的訊息就會成為死信

死信交換機 便是 死信 的歸屬。

如果一個佇列配置了 dead-letter-exchange 屬性,指定了一個交換機,那麼佇列中的死信就會投遞到這個交換機中,而這個交換機就稱為 死信交換機 - DLXDead Letter Exchange

步驟:當生產者正常投遞到佇列(simple.queue)中,如果消費者從佇列(simple.queue) 消費訊息卻宣告瞭 reject,那並且佇列繫結了死信交換機(dl.queue),那麼這個時候成為死信的訊息就會投遞到這個死信佇列(dl.queue)中。

死信投遞過程

正常佇列 --> 死信佇列 的過程,我們必須宣告兩個關鍵資訊

  • 死信交換機的名稱
  • 死信交換機與死信佇列繫結的路由key

而這兩個資訊也是我們投遞訊息的基礎配置。

接下來我們簡單模擬一下 條件1 所產生的場景

1、首先宣告一個死信交換機和死信佇列

我們這邊是使用簡單的註解方式直接生成

生成死信交換機和死信佇列

通過 RabbitMQ 控制檯介面可以看出已經成功生成

2、宣告正常使用交換機與佇列

然後這個時候我們就可以建立一個正常使用的交換機與佇列,並指明死信交換機

同樣可以通過控制檯檢視建立狀態

其中是否有宣告死信交換機我們可以通過佇列的 DLXDLK 標誌判斷

3、模擬拒收

然後我們現在通過程式碼模擬客戶端拒絕訊息的場景

1)訊息傳送

2)訊息接收

檢視控制檯,結果如下:

2021-11-06 23:56:52.095  INFO 2112 --- [ntContainer#0-1] c.l.m.c.listener.SpringRabbitListener    : 正常業務交換機 | 接收到的訊息 : [hello]
2021-11-06 23:56:52.118  INFO 2112 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener    : 死信交換機 | 接收到的訊息 : hello

這說明我們死信交換機已經成功發揮作用

2)TTL

以上我們已經成功認識到了 死信交換機 的使用,但是這與我們一開始說的 延遲佇列 似乎並沒有太大關係,莫急~接下來說到的 TTL(Time-To-Live) 就是用來處理延遲訊息的~!

在 TTL 的概念中,如果一個佇列中的訊息 TTL 結束後仍未被消費,那麼這個訊息就會自動變為死信,而 TTL 超時情況分為兩種:

  1. 訊息所在的佇列設定了存活時間
  2. 訊息本身設定了存活時間

我們同樣進行上述 條件2 的模擬場景

1、宣告死信交換機與死信佇列(上述已完成)
2、宣告延遲佇列並指定死信交換機

同樣控制檯檢視建立結果,並且我們發現不止有 DLXDLK 標誌,還多了個 TTL ,說明該佇列是延遲佇列

3、模擬消費超時情況

我們往延遲佇列中傳送一條訊息,並且沒有消費者進行消費,等待 1 分鐘後檢視是否能進入 死信佇列

我們已經傳送了一條訊息到延遲佇列並且一分鐘後也成功在控制檯發現了這條資訊已經進入到了死信交換機

2021-11-07 00:01:30.854  INFO 32752 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener    : 死信交換機 | 接收到的訊息 : test ttl-message

以上是配置了佇列超時時間,訊息本身自然也能配置超時時間,當 訊息佇列 都存在超時時間時,那麼就以最短的 TTL 為準,訊息的超時配置如下:

如上圖所示,我們可以利用 Message 這個類來傳遞訊息資訊,並設定上超時時間,我們設定的是 5000 ms,等待傳送成功後,控制檯過5000 ms 也成功列印了死信交換機消費的訊息:

2021-11-07 00:03:09.048  INFO 39996 --- [ntContainer#1-1] c.l.m.c.listener.SpringRabbitListener    : 死信交換機 | 接收到的訊息 : this is a ttl message

3)延遲佇列

我們上述是使用 死信交換機 來間接實現 延遲佇列 的效果,但實際在 RabbitMQ 不必如此麻煩,RabbitMQ 已經為我們封裝好了外掛,我們只需要下載安裝即可~

RabbitMQ 外掛下載地址

我們進入地址可以發現有許多外掛,搜尋 delay 關鍵字找到我們需要的外掛進行下載

下載完後直接上傳到 RabbitMQ 的外掛目錄 - plugins,小菜這邊是使用 docker 臨時安裝測試的,所以已經將該外掛目錄掛載出來了:

docker run -itd --name rabbitmq -v plugins:/plugins -p 15672:15672 -p 5672:5672 rabbitmq:management

因此我這邊直接將外掛上傳到容器中的 plugins 目錄即可~

然後進入到容器中執行以下命令進行外掛開啟

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

並且我們在控制檯建立交換機的時候可以看到 type 型別多了個選項

成功執行到這步就說明已經開啟了 RabbitMQ 的延遲佇列功能

那接下來我們就可以來使用 DelayExchange,首先我們需要了解程式碼的方式建立延遲交換機:

方式1

方式2

當我們萬事具備之後就可以來傳送訊息了

在傳送訊息的時候,訊息頭中一定要攜帶上 x-delay 引數,指定上延遲時間

通過這樣配置之後,我們可以在控制檯看到,經過10秒後 delay.queue 才收到對應訊息,然後被對應消費者消費

3)總結

我們上面從 死信交換機TTL延遲佇列,一步步認識瞭如何實現延遲訊息的功能,然後我們進行一個小小的總結:

問題1:什麼樣的訊息會成為死信?
  1. 訊息被消費者 reject 或返回 nack
  2. 訊息超時未及時消費
  3. 訊息佇列滿了
問題2:訊息超時的方式
  1. 給佇列設定 TTL 屬性
  2. 給訊息設定 TTL 屬性
問題3:如何使用延遲佇列
  1. 下載並啟用 RabbitMQ 延遲佇列外掛
  2. 宣告一個交換機,並將 delayed 屬性設定為 true
  3. 傳送訊息時,新增 x-delay 頭,值為超時時間
問題4:延遲佇列的使用場景
  1. 延遲傳送簡訊通知
  2. 訂單自動取消
  3. 庫存自動回滾

二、惰性佇列

講完延遲佇列,我們繼續來認識惰性佇列

惰性佇列之前,我們先丟擲一個問題~

RabbitMQ 如何解決訊息堆積問題

什麼情況下會出現訊息堆積問題?

  1. 當生產者生產速度遠遠消費者消費速度
  2. 當消費者當機沒有及時重啟

那麼如何解決這個問題?通常思路如下:

  1. 在消費者機器重啟後,增加更多的消費者進行處理
  2. 在消費者處理邏輯內部開闢執行緒池,利用多執行緒的方式提高處理速度
  3. 擴大佇列的容量,提高堆積上限

這幾個方式從理論上來說解決訊息堆積問題也是沒有問題的,但是處理方式不夠優雅甚至不夠靈活~ 那麼除了以上的幾種解決方式,我們可以利用 RabbitMQ 中自帶的一種佇列型別 -- 惰性佇列

什麼是惰性佇列?我們認識一下惰性佇列的幾個特性:

  • 接收到訊息後直接存入磁碟而非記憶體
  • 消費者要消費訊息時才會從磁碟中讀取並載入到記憶體中
  • 它支援百萬級訊息的儲存

說到底,就是利用磁碟的緩衝機制,而這種機制的缺點就是訊息的時效性會降低,效能受限於磁碟的IO,認識特性和缺點之後,我們便來看看如何建立惰性佇列

方式1

方式2

方式3

該方式是直接基於命令列修改將一個正在執行中的佇列修改為惰性佇列

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

其中幾個命令引數含義如下:

  • rabbitmqctl:命令列工具
  • set_policy:新增一個策略
  • Lazy:策略名稱,可以自定義
  • ^lazy-queue$:用正規表示式匹配佇列的名稱
  • '{"queue-mode":"lazy"}':設定佇列為 lazy 模式
  • --apply-to queues:策略的作用物件,是所有的佇列

這種惰性佇列的方式儘管缺點是訊息時效性會降低,但是在某些場景下也不是不能接受,何況它的優點同樣明顯:

  • 基於磁碟儲存,訊息上限高
  • 沒有間歇性的 page-out,效能穩定

到這裡,我們就已經講述了 RabbitMQ 的常見問題,對於我們來說,普通的開發場景可能比較少遇到這些問題,但是沒遇到不等於沒有,所以我們還是需要多認識來防患於未然!

不要空談,不要貪懶,和小菜一起做個吹著牛X做架構的程式猿吧~點個關注做個伴,讓小菜不再孤單。我們們下文見!

看完不讚,都是壞蛋

今天的你多努力一點,明天的你就能少說一句求人的話!
我是小菜,一個和你一起變強的男人。 ?
微信公眾號已開啟,小菜良記,沒關注的同學們記得關注哦!

相關文章