大家好,我是小菜。
一個希望能夠成為 吹著牛X談架構 的男人!如果你也想成為我想成為的人,不然點個關注做個伴,讓小菜不再孤單!
本文主要介紹
RabbitMQ的訊息丟失問題
如有需要,可以參考
如有幫助,不忘 點贊 ❥
微信公眾號已開啟,小菜良記,沒關注的同學們記得關注哦!
是的,最終是對 RabbitMQ 下手了!
面試中常見的RabbitMQ面試題也是多了去了,常見的如下:
- 訊息可靠性問題:如何確保傳送的訊息至少被消費一次?
- 延遲訊息問題:如何實現訊息的延遲投遞?
- 高可用問題:如何避免單點的MQ故障而導致的不可用問題?
- 訊息堆積問題:如何解決數百萬級以上訊息堆積,無法及時消費問題?
這幾個問題又得讓你腦殼疼一陣子,是不是也在網上看了挺多博文介紹這方面的解決方案,但是卻看了又忘,實際便是因為缺少實操,這篇小菜便重點講述下 RabbitMQ 如何解決訊息丟失問題~
一、訊息可靠性問題
訊息可靠性問題我們又可能將其理解為如何防止訊息丟失?那為什麼訊息會丟失呢?我們可以先看看訊息投遞的整個過程:
我們從圖中可以從三個階段分析可能造成訊息丟失:
- publisher 傳送訊息到 exchange
- exchange 分發到 queue
- queue 投遞到 customer
既然我們知道了哪些階段可能造成資料丟失,那我們就可以從源頭防範於未然~!
工程結構
工程結構很簡單,就是一個簡單的 Spring Boot 專案,裡面有個 消費者
和 生產者
兩個模組
1、生產者傳送丟失
RabbitMQ 中提供了 publisher confirm
機制來避免訊息傳送到 MQ 的過程中丟失的問題。訊息傳送到 MQ 以後,會返回一個確認結果給生產者,用於表示訊息是否確認成功。該確認結果存在兩種請求:
publisher-confirm
該型別是 傳送者確認 ,存在兩種情況
- 訊息成功投遞到交換機,返回
ack
- 訊息未投遞到交換機,返回
nack
publisher-return
該型別是 傳送者回執 ,存在兩種情況
- 訊息投遞到交換機,且成功分發到佇列,返回
ack
- 訊息投遞到交換機,但未成功分發到佇列,返回
nack
注意:確認機制傳送訊息時,需要給每個訊息設定一個全域性唯一ID,以區分不同訊息,避免ack衝突
接下來我們用程式碼來說明具體的操作方式
1)配置檔案
我們首先看下 生產者
的配置檔案
前面幾個配置 RabbitMQ 的連線資訊沒啥好講的,我們來看幾個比較陌生的配置
publisher-confirm-type
開啟傳送確認,這裡可以支援兩種型別
- simple:同步等待 confirm 結果,直到超時
- correlated:非同步回撥,定義 ConfirmCallback,MQ返回結果時會回撥這個 ConfirmCallback
publisher-returns
開啟 public-return,同樣是基於 CallBack 機制,不過是定義 ReturnCallback
template.mandatory
定義路由失敗時的策略。
- true:呼叫 ReturnCallback
- false:直接丟棄訊息
2)定義回撥事件
每個 RabbitTemplate 只能配置一個 ReturnCallback
3)傳送訊息
執行傳送程式碼之前,我們確保已經建立了(一個直連交換機direct-exchange
,一個佇列direct-queue
,且繫結的 key 為direct
正常情況下,我們執行程式碼肯定是傳送成功的,可以看到控制檯綠色輸出
且我們在訊息佇列中也成功接收到了訊息:
到這步是沒有任何問題的,那我們就需要手動給它製造點問題~ 我們可以修改 交換機名稱
,這個時候傳送訊息的時候找不到交換機,那麼交換機肯定就會返回 nack
,再看是否可以進入到我們程式碼中的判斷:
程式碼執行雖然是綠色的,但因為rabbitMQ找不到正確的交換機,而導致訊息傳送失敗,也就是下圖的這個過程:
這一個是 publish -> exchange
失敗我們順利的捕獲到了,那麼 exchange -> queue
這步的失敗是我們是否能夠正常捕獲?我們可以通過修改 路由 key
使交換機路由不到對應的 queue
可以發現當交換機沒有路由到相對應的 queue 時,也成功觸發了我們自定義的回撥函式,然後看 rabbitMQ 控制檯是可以發現訊息已經成功投遞到交換機
到這裡,我們通過兩種簡單的錯誤模擬,使程式都能順利的進入到我們預先定義的回撥中,如果遇到傳送失敗的情況,我們可以在失敗的回撥中自定義訊息重發
機制,最大程度上避免訊息丟失的問題
4)總結
我們可以通過 publisher-confirm
和 publisher-return
兩種錯誤捕獲機制,來避免 生產者 -> exchange -> queue
這條鏈路的訊息丟失
publisher-confirm
- 訊息成功傳送到 exchange,返回 ack
- 訊息未能成功傳送到 exchange,返回 nack
- 訊息傳送過程中出現異常,沒有收到回執,則進入 failureCallback 回撥
publisher-return
- 訊息成功傳送到 exchange,但沒有路由到 queue,呼叫自定義回撥函式 returnCallback
2、訊息儲存丟失
訊息儲存丟失是啥意思?其實就是持久化
的概念,當訊息已經成功傳送到 queue 時,這個時候如果消費者沒有及時進行消費,rabbitMQ 又剛好當機重啟了,那麼這個時候就會發現訊息丟失了。
這是因為 MQ 預設是記憶體儲存訊息,我們可以通過開啟持久化的功能來確保在 MQ 中的訊息不丟失
其實我們通過 RabbitMQ 提供的 GUI 建立交換機或佇列的時候就可以發現有持久化的這個選項
如果將 durability
設為 durable 後,我們可以發現無論如何重啟 MQ,重啟後交換機和佇列依然存在。
但是很多時候我們交換機
和 佇列
的建立並非在 GUI 上建立,而是通過應用程式碼的方式建立
- 交換機持久化
- 佇列持久化
- 訊息持久化
預設情況下,AMQP 發出的訊息都是持久化的,不用特意指定
3、消費者消費丟失
RabbitMQ 採取的機制是當確認訊息被消費者消費後就會立即刪除
那麼如何確認訊息已被消費者消費?那就還得依靠回執來確認,消費者獲取訊息後,需要向 RabbitMQ 傳送 ack
回執,表明自己已經處理訊息。其中 ack
在 AMQP 中有三種確認模式:
- manual:手動 ack,需要在業務程式碼結束後,呼叫 api 傳送 ack
- auto:自動 ack,由 spring 監測 listener 程式碼是否出現異常,沒有異常則返回 ack,反之返回 nack
- none:關閉 ack,MQ 在訊息投遞後會立即刪除訊息
上述三種方式都是通過修改配置檔案:
1)manual
該方式需要使用者自己手動確認,靈活性較好
這個時候如果執行邏輯是正常的,那麼在 RabbitMQ 上就會將該訊息刪除,但是如果執行的邏輯丟擲了異常,沒有進入到手動確認的環節,RabbitMQ 將會把該訊息保留:
2)auto
該方式在沒有異常發生時會自動進行訊息確認
我們在配置檔案中將確認方式改為 auto
進行測試:
正常情況下接收訊息是沒有任何問題的,那我們同樣製造些非正常情況:
我們手動製造了點異常,發現訊息沒有被 RabbitMQ 刪除的同時,而且控制檯一直在報錯,無止境的在嘗試重新消費,這如果放線上上環境難免有些令人崩潰。
當消費者出現異常後,訊息會不斷 requeue(重新入隊)到佇列,再重新傳送給消費者,然後再次異常,再次 requeue,無限迴圈,就會導致 MQ 的訊息處理飆升
而發生這種情況的原因所在便是因為 RabbitMQ的訊息失敗重試機制
,但很多時候我們可能不想一直重試,只需要經過幾次嘗試,如果失敗就放棄處理,這個時候我們就需要在配置檔案中配置失敗重試機制:
開啟該配置後,我們重啟專案進行觀察
通過控制檯可以看到在重試 3 次後,SpringAMQP會丟擲異常AmqpRejectAndDontRequeueException
,說明本地重試機制生效了。而且我們回到 RabbitMQ 控制檯可以看到對應訊息被刪除了,說明最後 SpringAMQP 返回的是 ack,導致訊息被 MQ 刪除
但是這種處理方式並不優雅
,重試後直接刪除訊息過於 暴力,那麼有沒有更好的處理方式?答案是有的!
我們可以利用 AMQP 提供的 MessageRecovery
介面來實現,該介面有三種不同的實現方式:
- RejectAndDontRequeueRecoverer:重試耗盡後,直接 reject,丟失訊息。預設方式,以上就是採用這種方式
- ImmediateRequeueMessageRecoverer:重試耗盡後,返回 nack,訊息重新入隊
- RepublishMessageRecoverer:重試耗盡後,將失敗訊息投遞到指定的交換機
三種方式可以根據不同場景進行採用,分析一下,不難發現第三種 RepublishMessageRecoverer
是比較優雅的~ 當重試失敗後會將訊息投遞到一個指定專門存放異常訊息的佇列,後續由人工集中進行處理!具體使用方式如下:
通過自定義異常處理後,我們重啟專案檢視控制檯:
可以發現重試3次後,我們的異常訊息進入到了我們自定義的異常佇列中
3)none
該方式沒啥好講的~ 無論訊息異常與否 MQ 都會進行刪除!
4、總結
假如這個時候面試再問你,如何確保 RabbitMQ訊息的可靠性?那你可得好好嘮嗑嘮嗑
如何保證訊息不丟失?
1)首先分析丟失的場景有哪些?
訊息丟失可能發生在 傳送時丟失(未送達 exchange / 未路由到 queue)
、訊息未持久化而MQ當機
、消費者接收訊息未能正確消費
2)然後如何預防
- 開啟生產者確認機制,確保生產者的訊息能到達佇列
確認機制包括 publisher-confirm
和 publisher-return
當未送達到 交換機 我們可以通過 publisher-confirm 返回的 ack
和 nack
來確認
當 交換機 未成功路由到 佇列,我們可以通過 publisher-return
自定義的回撥函式來確認,每個 RabbitTemplate 只能配置一個 ReturnCallback
- 開啟持久化功能,確保訊息未消費前在佇列中不會丟失
持久化功能分為 交換機持久化
、佇列持久化
和 訊息持久化
,我們都需要將 durable 設定為 true
- 開啟消費者確認機制最低為
auto
級別
消費者確認機制有三種型別:manual (手動確認)
、auto (自動確認)
、none (關閉 ack)
- 失敗重試機制
我們手動設定 MessageResoverer
為 RepublishMessageRecoverer 方式,將投遞失敗的訊息轉到異常佇列中,交由人工處理
這一套組合拳回答下來,面試官還不得默默承認你有點東西?
當然這只是 RabbitMQ 的問題之一,我們下篇繼續其他幾個問題的解決方式~
不要空談,不要貪懶,和小菜一起做個吹著牛X做架構
的程式猿吧~點個關注做個伴,讓小菜不再孤單。我們們下文見!
今天的你多努力一點,明天的你就能少說一句求人的話!
我是小菜,一個和你一起變強的男人。?
微信公眾號已開啟,小菜良記,沒關注的同學們記得關注哦!