前言
在之前的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的重試機制、延時消息和死信隊列。
有些地方可能比較複雜,可能需要小夥伴們重複閱讀幾次才能理解,如果哪裏有想不清楚的,或者有疑問的可以聯繫王子共同探討。
往期文章推薦: