如何保證消息不被重復消費?(如何保證消息消費時的冪等性)

面試題

如何保證消息不被重復消費?或者說,如何保證消息消費時的冪等性?

面試官心理分析

其實這是很常見的一個問題,這倆問題基本可以連起來問。既然是消費消息,那肯定要考慮會不會重復消費?能不能避免重復消費?或者重復消費了也別造成系統異常可以嗎?這個是 MQ 領域的基本問題,其實本質上還是問你使用消息隊列如何保證冪等性,這個是你架構里要考慮的一個問題。

面試題剖析

回答這個問題,首先你別聽到重復消息這個事兒,就一無所知吧,你先大概說一說可能會有哪些重復消費的問題

首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能會出現消息重復消費的問題,正常。因為這問題通常不是 MQ 自己保證的,是由我們開發來保證的。挑一個 Kafka 來舉個例子,說說怎么重復消費吧。

Kafka 實際上有個 offset 的概念,就是每個消息寫進去,都有一個 offset,代表消息的序號,然后 consumer 消費了數據之后,每隔一段時間(定時定期),會把自己消費過的消息的 offset 提交一下,表示“我已經消費過了,下次我要是重啟啥的,你就讓我繼續從上次消費到的 offset 來繼續消費吧”。

但是凡事總有意外,比如我們之前生產經常遇到的,就是你有時候重啟系統,看你怎么重啟了,如果碰到點著急的,直接 kill 進程了,再重啟。這會導致 consumer 有些消息處理了,但是沒來得及提交 offset,尷尬了。重啟之后,少數消息會再次消費一次。

image.png

舉個栗子。

有這么個場景。數據 1/2/3 依次進入 kafka,kafka 會給這三條數據每條分配一個 offset,代表這條數據的序號,分配的 offset 依次是 152/153/154。消費者從 kafka 去消費的時候,也是按照這個順序去消費。假如當消費者消費了 offset=153 的這條數據,剛準備去提交 offset 到 zookeeper,此時消費者進程被重啟了。那么此時消費過的數據 1/2 的 offset 并沒有提交,kafka 也就不知道你已經消費了 offset=153 這條數據。那么重啟之后,消費者會找 kafka 說,嘿,哥兒們,你給我接著把上次我消費到的那個地方后面的數據繼續給我傳遞過來。數據 1/2 再次被消費。

如果消費者干的事兒是拿一條數據就往數據庫里寫一條,會導致說,你可能就把數據 1/2 在數據庫里插入了 2 次,那么數據就錯啦。

其實重復消費不可怕,可怕的是你沒考慮到重復消費之后,怎么保證冪等性

舉個例子吧。假設你有個系統,消費一條消息就往數據庫里插入一條數據,要是你一個消息重復兩次,你不就插入了兩條,這數據不就錯了?但是你要是消費到第二次的時候,自己判斷一下是否已經消費過了,若是就直接扔了,這樣不就保留了一條數據,從而保證了數據的正確性。

一條數據重復出現兩次,數據庫里就只有一條數據,這就保證了系統的冪等性。

冪等性,通俗點說,就一個數據,或者一個請求,給你重復來多次,你得確保對應的數據是不會改變的,不能出錯

所以第二個問題來了,怎么保證消息隊列消費的冪等性?

其實還是得結合業務來思考,我這里給幾個思路:

  • 比如你拿個數據要寫庫,你先根據主鍵查一下,如果這數據都有了,你就別插入了,update 一下好吧。
  • 比如你是寫 Redis,那沒問題了,反正每次都是 set,天然冪等性。
  • 比如你不是上面兩個場景,那做的稍微復雜一點,你需要讓生產者發送每條數據的時候,里面加一個全局唯一的 id,類似訂單 id 之類的東西,然后你這里消費到了之后,先根據這個 id 去比如 Redis 里查一下,之前消費過嗎?如果沒有消費過,你就處理,然后這個 id 寫 Redis。如果消費過了,那你就別處理了,保證別重復處理相同的消息即可。
  • 比如基于數據庫的唯一鍵來保證重復數據不會重復插入多條。因為有唯一鍵約束了,重復數據插入只會報錯,不會導致數據庫中出現臟數據。
image.png

當然,如何保證 MQ 的消費是冪等性的,需要結合具體的業務來看。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容