1. 前言
上個小節中我們介紹了 RabbitMQ 中如何防止消息丟失,即保證消息發送的 At Least Once 性質,除此之外,如何防止消息被重復消費,即保證消息消費的 Exactly Once 性質,也是業務邏輯中需要考慮的問題。
2. 消息消費順序
面試官提問:業務中使用了 RabbitMQ 消息隊列,如何保證消息的順序消費?
題目解析:
保證消息的順序消費是業務場景下經常面臨的挑戰,可能在面試中會涉及到一些實戰場景,例如電商的下單邏輯,在用戶下單之后,會發送創建訂單和扣減庫存的消息,我們需要保證扣減庫存在創建訂單之后執行。
在MQ層面支持消息的順序消費是一件開銷很大的操作,例如使用事務,所以除非特定場景,一般不在 RabbitMQ 消息傳輸底層支持順序。在上層即應用層處理業務邏輯是常規操作,有兩種通用解決方案:
(1)同步發送消息:將消息發送從異步模式切換為同步模式,例如先發送創建訂單消息,當創建訂單的下游消費者發送ACK確認成功消費后,再發送扣減庫存的消息;
(2)消息實體增加冗余字段:例如增加 version(版本號)、 msg_id(消息id),保證在扣減庫存時,對應 msg_id 的訂單已經創建成功,實戰中配合Redis等緩存協助判斷。
3. 消息重復消費
面試官提問:RabbitMQ 如何保證消息不會被重復消費?
題目解析:
所有的消息隊列都要保證同一條消息不會被重復消費,RabbitMQ 重復消費消息的可能場景主要有兩種:
(1)生產者重復發送消息:生產者在往消息隊列發送消息時,發生了網絡抖動,生產者沒有收到確認信號,但是實際上消息隊列已經收到了消息,超過一定時間后生產者會重新發送消息,這時一條消息被發送了兩次;
(2)消費者重復接受消息:消費者成功消費消息后,發生了網絡抖動,消息隊列沒有收到確認信號,超過一段時間后會重新給消費者投遞相同的消息,同一條消息即存在被消費兩次的可能。
通用解決方案是在消息實體中添加全局唯一的id,例如 msg_id(消息ID),在業務邏輯層保證消息的冪等性,具體參考步驟:
(1)消費者在收到消息之后,根據 msg_id 從緩存/數據庫中查詢是否存在已有消息;
(2)如果不存在已有消息,那么消費之后,將 msg_id 對應的消息實體或者序列化對象寫入緩存/數據庫;
(3)如果存在已有消息,說明這條消息已被消費過,丟棄消息并且打一條告警日志。
并且可以根據重復消費的容忍程度以及性能要求選擇使用緩存還是使用數據庫,如果對判斷的速度要求高,可以使用 Redis 作為緩存;如果對判斷的穩定性和魯棒性要求高,使用數據庫存儲消息實體,同時將 msg_id 作為數據庫表的唯一鍵,插入重復記錄一定會拋出異常,避免數據庫因為并發問題產生臟數據,保證了消息消費的不可重復性。
4. 小結
本章節介紹了 RabbitMQ 中最常見的重復發送消息的實際場景,并且給出了添加全局唯一 ID 的通用性解決方案,候選人需要理解通過全局 ID 解決重復消息的核心邏輯,準備時間充裕的情況可以在本地環境編碼實現上述流程。