關於 Kafka 訊息丟失、重複消費和順序消費的問題
訊息丟失,訊息重複消費,訊息順序消費等問題是我們使用 MQ 時不得不考慮的一個問題,下面我結合實際的業務來和你分享一下解決方案。
訊息丟失問題
比如我們使用 Kakfa 時,以下場景都會發生訊息丟失:
- producer -> broker (生產者生產訊息)
- broker -> broker (叢集環境,broker 同步給其他 broker)
- broker -> consumer (消費者消費訊息)
解決方案也很簡單,設定 acks(訊息確認機制)retries(重試機制)factor(設定 partition 數量)...
一般來說,最常見的訊息丟失場景就是:consumer 消費訊息。
要保證 consumer 消費訊息時不丟失訊息,必須使用手動提交 ack
我們業務是這樣實現的:
- 從 Kafka 拉取訊息(一次批量拉取 100條)
- 為每條訊息分配一個 msgId(遞增)
- 將 msgId 存入記憶體佇列(sortSet)
- 使用 Map 儲存 msgId 與 msg (包含 offset)的對映關係
- 當業務處理完訊息後,獲取當前訊息的 msgId,然後從 sortSet 中刪除該 msgId(表示該訊息已經處理過了)
- ack 時,如果當前 msgId <= sortSet(msgId 在 sortSet 中是從小到大排列) ,就提交當前 offset
- 就算 consumer 在處理訊息時掛了,下次重啟時就會從 sortSet 隊首的訊息開始拉取,實現至少處理一次語義。
- 步驟 7 存在一個問題:當訊息處理完後,還沒從 sortSet 中刪除該 msgId,系統就掛了,當系統重啟時,又會重新處理一次剛剛已處理過的訊息,這就引出訊息重複消費的問題了。
訊息重複消費
要解決訊息重複消費,也就是要實現冪等(冪等就是:多次請求,但結果保持不變,舉一個例子你就明白了:在 http 中,你傳送同一個 get 請求,無論傳送多少次,返回結果都是一樣的
)
回到我們的業務場景上,我以處理訂單訊息為例:
-
冪等Key 由我們的訂單Id + 訂單狀態組成(一筆訂單的狀態只會處理一次)
-
在處理之前,我們首先會去 Redis 查詢是否存在這個 Key
如果存在,說明我們已經處理過了,直接丟掉;
如果不存在,說明沒處理過,繼續往下處理;
-
最終的邏輯是:將處理過的資料存到DB上,再把 冪等Key 存到 Redis 上
顯然一般場景下 Redis 是無法保證冪等的
所以Redis只是一個前置處理,最終的冪等性依賴 DB 的唯一Key(訂單Id+訂單狀態)
總的來說就是:通過 Redis 做前置處理,DB 唯一索引做最終保證實現冪等性
訊息順序消費
訊息的順序性很好理解,還是以訂單處理為例
訂單的狀態有:支付、確認收貨、完成等等,而訂單下還有計費、退款的訊息報
理論上來說:支付的訊息肯定要比退款的訊息先到。
但是程式處理的過程就不一定了,所以我們處理訊息順序消費的流程如下:
- 寬表:建立一張寬表,唯一索引是 訂單Id,將訂單的每個狀態拆分為一個列,當訊息來了,只更新對應的欄位就好,訊息只會存在短暫的狀態不一致問題,但是最終狀態是一致的
- 訊息補償機制
- 把相同的 userID/orderId 傳送到相同的 partition(因為一個 consumer 消費一個 partition)