目錄
- 一、哪些場景需要消息保序?
- 二、如何實現消息保序?
- 三、保序消息的常見問題和應對策略
- 3.1、重復消息
- 3.2、節點故障
- 3.3、分區擴容
- 四、小結
本文來源:極客時間vip課程筆記
一、哪些場景需要消息保序?
- 消息保序問題指的是,在通過消息中間件傳遞消息過程中,我們希望消費者收到消息的順序,和發送者發送消息的順序保持一致。或者說,消息中間件在傳遞消息時,不要改變消息之間的順序。
- 過在工程實踐中,我們面臨的保序問題不一定只是局限在消息保序這一個環節,更常見的場景是,事件經過包含消息隊列等多個環節的處理和傳遞后,在某個服務內能夠按照事件發生的順序來逐個處理這些事件,不發生亂序。比如:
(1)、在證券、股票交易撮合場景中,對于出價相同的交易單,需要堅持按照先出價先交易的原則,下游處理訂單的系統需要嚴格按照出價順序來處理訂單。
(2)、在數據庫變更增量同步場景中,上游源端數據庫按需執行增刪改操作,將 BINLOG 作為消息,通過消息隊列傳輸到下游系統,下游系統按順序還原消息數據,實現狀態數據有序更新。
(3)、在電商系統中,訂單創建、支付、退款、物流等消息需要按照順序處理,才能保證訂單狀態的正確更新。
二、如何實現消息保序?
-
有些消息隊列其設計就是基于隊列來實現的,比如 RabbitMQ。
-
我們知道隊列的特性就是先進先出,天然就是有序的,使用這種“基于隊列來實現的消息隊列”傳遞消息,自然就可以實現消息保序。
-
但是為了實現高吞吐,更多的消息隊列在設計上采用的是多分區或多隊列的實現,比如 Kafka 或 RocketMQ 。這些消息隊列在處理消息時,會將消息按照一定的策略分發到多個分區上并行處理,也就無法保證消息的有序性了。
如果我們希望在多分區的消息隊列上實現消息保序,可以將分區數量配置為 1,這樣就可以實現和單隊列消息中間件一樣的保序效果了。
-
此外,為了保證在消費者端消息的有序性,消費者也只能單實例單線程來消費消息,才能保證全流程消息是嚴格有序的。
-
以上就是全局消息保序的實現方法,這種方法的實現思路其實就是全局串行處理。我們知道,串行處理有一個很大的弊端,那就是消息吞吐量有限,也沒法通過水平擴展來提升消息吞吐量。在規模比較大的場景下就顯得力不從心了。
-
我們會發現,大多數需要消息保序的場景并不需要消息“全局有序”。多數場景下,我們只需要保證有業務關系的消息之間的順序就可以了,沒有業務關系的消息之間的先后順序,其實是無所謂的。
-
只有數據庫 BINLOG 同步這個場景,因為數據庫事務的存在,我們沒有辦法把 BINLOG 歸并到數據庫表上,也就無法判斷 BINLOG 消息的關聯性,這種情況下必須實現全局保序,才能保證數據同步的正確性。
將保序要求從全局放寬到局部,就可以充分利用多分區消息隊列的并發能力來提升消息吞吐量了。
-
實現的思路是這樣的,既然我們只需要保證有關聯的消息之間的順序,那么只要把關聯的消息都發送給同一個分區處理,而分區內天然就可以保證消息的處理順序,這樣就實現了消息局部保序。下面舉個例子來說明。
-
比如,我們用一個 4 分區的主題來處理訂單消息。可以采用哈希算法將訂單消息按照訂單號的后兩位均勻地分配到 4 個分區上。
-
也就是,將訂單尾號 00-24 的訂單發給 0 號分區,25-49 尾號的訂單分給 1 號分區,50-74 尾號的訂單發給 2 號分區,剩余的發給 3 號分區。這樣就可以保證同一個訂單的消息總是分配相同的分區,也就實現了訂單內消息保序。
-
多數消息中間件都內置了相應的功能來幫助我們快速實現局部消息保序。
-
以 Kafka 為例,Kafka 的 API 支持在發送消息的時候指定一個消息的 Key,并且內置了默認的哈希算法將 Key 映射到主題的分區上,保證具有相同 Key 的消息總是會被發送到同一分區,從而實現了消息局部保序。在上面例子中,可以直接使用訂單號作為消息的 Key,示例代碼如下:
// 訂單消息 OrderMessage orderMessage = createOrderMessage(...); // 訂單號,作為Key String orderId = orderMessage.getOrderId();