探索RocketMQ的重複消費和亂序問題

王子發表於2020-11-13

 

前言

在之前的MQ專題中,我們已經解決了訊息中介軟體的一大難題,訊息丟失問題。

但MQ在實際應用中不是說保證訊息不丟失就萬無一失了,它還有兩個令人頭疼的問題:重複消費和亂序。

今天我們就來聊一聊這兩個常見的問題,看看RocketMQ是如何解決這兩個問題的。

 

為什麼會重複消費

首先我們來聊一聊重複消費的問題,要解決一個問題最開始的一步當然是去查詢問題發生的原因了。

那出現重複消費的原因到底是什麼呢?

我們先來思考一下生產者傳送訊息這一過程中是不是有可能重複傳送訊息到MQ呢?

答案是肯定的,比如生產者傳送訊息的時候使用了重試機制,傳送訊息後由於網路原因沒有收到MQ的響應資訊,報了個超時異常,然後又去重新傳送了一次訊息。

但其實MQ已經接到了訊息,並返回了響應,只是因為網路原因超時了。

這種情況下,一條訊息就會被髮送兩次。

 

當然,這只是列舉了一種情況,實際有很多情況會造成訊息的重新傳送。

那麼假如生產者沒有重複傳送訊息,消費者就能保證不重複消費了嗎?

當然不能保證,我們知道,在消費者處理了一條訊息後會返回一個offset給MQ,證明這條訊息被處理過了。

但是,假如這條訊息已經處理過了,在返回offset給MQ的時候服務當機了,MQ就沒有接收到這條offset,那麼服務重啟後會再次消費這條訊息。

 

 

如何解決重複消費

解決重複消費的關鍵就是引入冪等性機制,什麼是冪等性機制呢?我們可以把它理解成,假如一個介面被重複呼叫,依然可以保證資料的準確性。

對於生產者重複傳送訊息到MQ這一過程,其實我們沒有必要去保證冪等性,只要在消費者處理訊息時保證冪等性就可以了。

這塊其實就比較簡單了,只要處理訊息之前先根據業務判斷一下本次操作是否已經執行過了,如果已經執行過了,那就不再執行了,這樣就可以保證消費者的冪等性。

舉個例子,比如每條訊息都會有一條唯一的訊息ID,消費者接收到訊息會儲存訊息日誌,如果日誌中存在相同ID的訊息,就證明這條訊息已經被處理過了。

 

訊息重試、延時訊息、死信佇列

解決完重複消費問題,我們來思考一種極端情況,比如某一時刻,消費者操作的資料庫當機了,這個時候消費者會發生異常,當然不能返回給MQ一個CONSUME_SUCCESS了,我們可以返回RECONSUME_LATER,他的意思是我現在沒法處理這些訊息,一會再來試試能不能處理。

簡單來說,RocketMQ會有一個針對當前Consumer Group的重試佇列,如果你返回了RECONSUME_LATER,MQ會把你的這批消費放到當前消費組的重試佇列中,然後過一段時間重試佇列中的訊息會再次傳送給消費者,預設可以重試16次,每次重試的間隔是不同的,這個時間間隔是可以配置的,預設配置如下:

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

細心的小夥伴會發現,這個配置一共有18個時間,為什麼最多重試16次,配置中卻有18個時間呢,這裡就要說到延時訊息了。

上邊的配置其實不是針對重試佇列的,而是針對延時訊息的,18個時間分別代表延遲level1-level18,延時訊息大概流程如下:

1 所有的延遲訊息到達broker後,會存放到SCHEDULE_TOPIC_XXX的topic下(這個topic比較特殊,對客戶端是不可見的,包括使用rocketmq-console,也查不到這個topic)

2 SCHEDULE_TOPIC_XXX這個topic下存在18個佇列,每個佇列中存放的訊息都是同一個延遲級別訊息

3 broker端啟動了一個timer和timerTask的任務,定時從此topic下拉取資料,如果延遲時間到了,就會把此訊息傳送到指定的topic下,完成延遲訊息的傳送

剛才我們說如果你返回了RECONSUME_LATER,訊息就會進入重試佇列,其實不完全準確。

當MQ接收到RECONSUME_LATER後,首先會完成訊息的轉換,把訊息存到延時佇列中,然後再根據訊息的延時時間儲存到重試佇列中。

如果重試了16次之後依然無法處理,就會把這些消費放入死信佇列。死信佇列中的訊息RocketMQ不會再做處理,這部分資料要怎麼處理就要看我們的業務場景了,我們可以做一個後臺執行緒去訂閱這個死信佇列,完成後續訊息的處理。

 

訊息亂序

接下來我們聊一聊訊息亂序問題,為什麼會出現這個問題呢,這個其實不難理解。

我們都學過,每個Topic可以有多個MessageQueue,寫入訊息的時候實際上會平均分配給不同的MessageQueue。

然後假如我們有一個Consume Group,這個消費組中的每臺機器都會負責一部分MessageQueue,那麼就會導致訊息的順序亂序問題。

舉個例子,生產者傳送了兩條順序訊息,先是insert,後是update,分別分配到兩個MessageQueue中,消費者組中的兩臺機器分別處理兩個佇列的訊息,這個時候是無法保證順序性的,有可能會先執行update,後執行insert,導致資料發生錯誤。

那麼如何解決訊息亂序問題呢?

其實道理也很簡單,把需要保持順序的訊息都放入到同一個MessageQueue中,讓同一臺機器處理不就可以了嗎。

我們完全可以根據唯一ID與佇列的數量進行hash運算,保證這些訊息進入到同一個佇列中,最簡單的演算法就是取餘運算了。

現在我們能保證這批訊息進入到同一個佇列中了,似乎這樣就能保證訊息不會亂序了,但真的是這樣嗎?

上文我們說到如果消費者資料庫出現問題,使用重試佇列重試訊息,那麼對於需要保證順序的訊息也可以使用這套方案嗎?

肯定是不能的,如果使用重試機制是無法保證順序性的。

RocketMQ提供了另一個狀態,SUSPEND_CURRENT_QUEUE_A_MOMENT,意思是先等一會,再接著處理這批訊息,而不是把這批訊息放入重試佇列裡去處理其他訊息。

所以我們只要返回這個狀態就可以了。

 

總結

好了,到這裡關於RocketMQ重複消費和亂序問題的產生原因和解決方案我們就介紹完了,同時也介紹了RocketMQ的重試機制、延時訊息和死信佇列。

有些地方可能比較複雜,可能需要小夥伴們重複閱讀幾次才能理解,如果哪裡有想不清楚的,或者有疑問的可以聯絡王子共同探討。

 

往期文章推薦:

深入研究Broker是如何持久化的

Dledger是如何實現主從自動切換的

深入研究RocketMQ消費者是如何獲取訊息的

RocketMQ的訊息是怎麼丟失的

RocketMQ訊息丟失解決方案:事務訊息

RocketMQ訊息丟失解決方案:同步刷盤+手動提交

 

相關文章