《RabbitMQ》 | 訊息丟失也就這麼回事

蔡不菜丶發表於2021-10-26

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

本文主要介紹 RabbitMQ的訊息丟失問題

如有需要,可以參考

如有幫助,不忘 點贊

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

是的,最終是對 RabbitMQ 下手了!

面試中常見的RabbitMQ面試題也是多了去了,常見的如下:

  • 訊息可靠性問題:如何確保傳送的訊息至少被消費一次?
  • 延遲訊息問題:如何實現訊息的延遲投遞?
  • 高可用問題:如何避免單點的MQ故障而導致的不可用問題?
  • 訊息堆積問題:如何解決數百萬級以上訊息堆積,無法及時消費問題?

這幾個問題又得讓你腦殼疼一陣子,是不是也在網上看了挺多博文介紹這方面的解決方案,但是卻看了又忘,實際便是因為缺少實操,這篇小菜便重點講述下 RabbitMQ 如何解決訊息丟失問題~

一、訊息可靠性問題

訊息可靠性問題我們又可能將其理解為如何防止訊息丟失?那為什麼訊息會丟失呢?我們可以先看看訊息投遞的整個過程:

我們從圖中可以從三個階段分析可能造成訊息丟失:

  • publisher 傳送訊息到 exchange
  • exchange 分發到 queue
  • queue 投遞到 customer

既然我們知道了哪些階段可能造成資料丟失,那我們就可以從源頭防範於未然~!

工程結構

工程結構很簡單,就是一個簡單的 Spring Boot 專案,裡面有個 消費者生產者 兩個模組

1、生產者傳送丟失

RabbitMQ 中提供了 publisher confirm 機制來避免訊息傳送到 MQ 的過程中丟失的問題。訊息傳送到 MQ 以後,會返回一個確認結果給生產者,用於表示訊息是否確認成功。該確認結果存在兩種請求:

  • publisher-confirm

該型別是 傳送者確認 ,存在兩種情況

  1. 訊息成功投遞到交換機,返回 ack
  2. 訊息未投遞到交換機,返回 nack
  • publisher-return

該型別是 傳送者回執 ,存在兩種情況

  1. 訊息投遞到交換機,且成功分發到佇列,返回 ack
  2. 訊息投遞到交換機,但未成功分發到佇列,返回 nack

注意:確認機制傳送訊息時,需要給每個訊息設定一個全域性唯一ID,以區分不同訊息,避免ack衝突

接下來我們用程式碼來說明具體的操作方式

1)配置檔案

我們首先看下 生產者 的配置檔案

前面幾個配置 RabbitMQ 的連線資訊沒啥好講的,我們來看幾個比較陌生的配置

  • publisher-confirm-type

開啟傳送確認,這裡可以支援兩種型別

  1. simple:同步等待 confirm 結果,直到超時
  2. correlated:非同步回撥,定義 ConfirmCallback,MQ返回結果時會回撥這個 ConfirmCallback
  • publisher-returns

開啟 public-return,同樣是基於 CallBack 機制,不過是定義 ReturnCallback

  • template.mandatory

定義路由失敗時的策略。

  • true:呼叫 ReturnCallback
  • false:直接丟棄訊息
2)定義回撥事件

每個 RabbitTemplate 只能配置一個 ReturnCallback

image-20211024171022583

3)傳送訊息

執行傳送程式碼之前,我們確保已經建立了(一個直連交換機direct-exchange,一個佇列direct-queue,且繫結的 key 為direct

正常情況下,我們執行程式碼肯定是傳送成功的,可以看到控制檯綠色輸出

且我們在訊息佇列中也成功接收到了訊息:

到這步是沒有任何問題的,那我們就需要手動給它製造點問題~ 我們可以修改 交換機名稱,這個時候傳送訊息的時候找不到交換機,那麼交換機肯定就會返回 nack ,再看是否可以進入到我們程式碼中的判斷:

程式碼執行雖然是綠色的,但因為rabbitMQ找不到正確的交換機,而導致訊息傳送失敗,也就是下圖的這個過程:

這一個是 publish -> exchange 失敗我們順利的捕獲到了,那麼 exchange -> queue 這步的失敗是我們是否能夠正常捕獲?我們可以通過修改 路由 key 使交換機路由不到對應的 queue

可以發現當交換機沒有路由到相對應的 queue 時,也成功觸發了我們自定義的回撥函式,然後看 rabbitMQ 控制檯是可以發現訊息已經成功投遞到交換機

到這裡,我們通過兩種簡單的錯誤模擬,使程式都能順利的進入到我們預先定義的回撥中,如果遇到傳送失敗的情況,我們可以在失敗的回撥中自定義訊息重發機制,最大程度上避免訊息丟失的問題

4)總結

我們可以通過 publisher-confirmpublisher-return 兩種錯誤捕獲機制,來避免 生產者 -> exchange -> queue 這條鏈路的訊息丟失

  • publisher-confirm

    1. 訊息成功傳送到 exchange,返回 ack
    2. 訊息未能成功傳送到 exchange,返回 nack
    3. 訊息傳送過程中出現異常,沒有收到回執,則進入 failureCallback 回撥
  • publisher-return

    1. 訊息成功傳送到 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-confirmpublisher-return

當未送達到 交換機 我們可以通過 publisher-confirm 返回的 acknack 來確認

交換機 未成功路由到 佇列,我們可以通過 publisher-return 自定義的回撥函式來確認,每個 RabbitTemplate 只能配置一個 ReturnCallback

  • 開啟持久化功能,確保訊息未消費前在佇列中不會丟失

持久化功能分為 交換機持久化佇列持久化訊息持久化,我們都需要將 durable 設定為 true

  • 開啟消費者確認機制最低為 auto 級別

消費者確認機制有三種型別:manual (手動確認)auto (自動確認)none (關閉 ack)

  • 失敗重試機制

我們手動設定 MessageResovererRepublishMessageRecoverer 方式,將投遞失敗的訊息轉到異常佇列中,交由人工處理


這一套組合拳回答下來,面試官還不得默默承認你有點東西?

當然這只是 RabbitMQ 的問題之一,我們下篇繼續其他幾個問題的解決方式~

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

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

相關文章