聊聊 Java 訊息
訊息在開發過程中多次用到,在分散式系統中也是高頻使用的一項技術,特寫該片文章以作總結。
一、訊息使用場景
1.1 常見的使用場景
系統解耦
在單機系統中解耦可能無關痛癢,但是在分散式環境下,系統間的相互依賴,最終會會導致整個依賴關係混亂,對後期的拆分和優化都帶來負擔。

非同步處理
在邏輯請求鏈路裡,為了提高介面RT,都會對邏輯進行優化,對於非主鏈路上的分支邏輯,都會選擇非同步處理。其中比較經典的當屬日誌的記錄,一般會選擇傳送訊息日誌,然後由日誌系統非同步消費並進行儲存等後續處理。
資料最終一致性,通過訊息來實現最終的資料同步。
非同步處理也可以引來 並行處理 的使用姿勢。在工作中,我們基於訊息開發了一個簡單的分散式任務處理元件。該元件簡單分為三塊分別是 切分、載入、執行三個階段

每個階段都是以作為消費者,然後處理完畢後再作為生產者傳送訊息。訊息消費無狀態,可以按需無限拓容。
流量削峰
由於使用訊息,我們的鏈路變成了生產者傳送訊息,訊息中介軟體儲存訊息,最後消費者從訊息中介軟體拉取訊息的一個過程。而訊息中介軟體的儲存能力能夠有效的幫助消費者進行緩衝。試想下,正常流量下消費者能夠愉快的進行消費,瞬時高峰流量來的時候,消費者消費能力跟不上,剛好阻塞在訊息中介軟體,等峰值過後,消費者又能很快的將阻塞的訊息進行消費。
1.2 訊息使用的先決條件
根據上面的三個常見場景,我們可以發現,訊息的介入,一般都是在非核心鏈路較多或者核心鏈路但是能容忍延遲的情況下(比如資料的最終一致性)較為合適。
二、訊息相關的概念
在JMS標準中,有兩種訊息模型P2P(Point toPoint)和Publish/Sub(Pub/Sub)。
P2P

點對點,一個發,一個消費。涉及到的角色 釋出者(Publisher)、消費者(Consumer)、訊息佇列(Queue)
特點:
- 一個訊息只能被一個消費者消費,消費後會從佇列裡移除
- 釋出者和消費者無關係,釋出者傳送訊息的行為不會隨消費者而改變
- 消費者消費完成訊息,需要向佇列Ack,訊息佇列發現訊息消費成功即做訊息移除
Pub/Sub

釋出訂閱模式,一個釋出,多方訂閱。涉及到的角色有 釋出者(Publisher)、主題(Topic)、訂閱者(Subscriber)。
特點
- 每個訊息可以有多個消費者
- 針對某個主題(Topic)的訂閱者,必須建立一個訂閱者之後,才能消費釋出者的訊息
- 為了消費訊息,訂閱者必須保持執行的狀態
Kafka 就是該模式的典型代表。
三、常規訊息問題及解決方案
2.1 訊息延遲
一般要麼是大流量進入,要麼是消費者延遲,基本思路就是排除業務邏輯,然後考慮是否可拓容。
案例
工作中有這樣一個場景 商品根據商品變更訊息來進行ES資料的新增或修改,另外一個商品分組的訊息對ES的資料進行修改,由於業務上先有商品後有分組,所以分組訊息的消費者只做ES更新。
不巧的時當時一個哥們在刷全網商品資料,也沒做限速導致有大量的商品變更訊息產生,商品訊息阻塞導致ES對應的商品文件沒有建立,然後分組訊息消費者更新失敗。當時用的還是順序訊息,所以拓容無望。
由於每次更新ES都是從db獲取最新的資料並更新到ES,並不依賴訊息的資料。另外更新商品最多是資料同步延遲問題,而新增的商品直接會導致ES查不到,阻塞的訊息大部分都是修改的訊息,所以當時我們新建訊息例項,把阻塞的那批訊息轉移到新的例項。然後原例項訊息清空繼續處理新的訊息。保證新訊息能夠不阻塞。相當於變相進行了消費者和訊息 Patition兩個拓容

當然使用者也是有感知的,只是我們儘可能的將故障降到最低。
2.2 訊息阻塞
訊息阻塞一般就是突然訊息增多,超過了消費者消費的能力。或者消費者出現邏輯問題,導致不斷的重試或長時間等待。
該類問題一般首先要看下業務日誌,缺消費是否正常。如果是訊息峰值,可考慮是否拓容。當然消費邏輯問題,只能緊急修補併發布了,在修補後為了快速消費阻塞的訊息佇列,可考慮臨時對消費者進行拓容。
2.3 重複消費
重複消費一般發生下消費端,比如消費者處理完畢,在準備進行ack的時候出現了問題,應用重啟後,訊息中介軟體以為該訊息還未處理又推給了消費者,或者消費者拉取的時候重複。
一般的做法就是消費端做冪等,業務上設定一個冪等表或者用redis等用來儲存消費記錄,每次消費訊息都先去冪等儲存進行驗證,如果沒有消費則存入冪等記錄,冪等一般會根據一個業務唯一鍵進行確認。
2.4 訊息丟失
訊息丟失一般分為生產者傳送失敗、訊息中介軟體丟失、消費丟失。
生產者丟失
生產者傳送訊息,可能以為網路問題或者訊息中間處理失敗導致,訊息遺漏。針對生產者如果傳送是同步的自然可以進行異常捕捉並重試。如果是非同步請求,一般通過傳送訊息的非同步回撥方法來實現。kafka裡這兩種case都能滿足。
訊息中間的丟失
訊息中間對我們來說一般都是相對可信的,但是有時候訊息中間可能為了吞吐量啥的不能保證某些特性,所以這隻能根據中間的文件進行相應的配置。
案例
kafka某個broker宕機,然後重新選舉partiton的leader時,要是此時其他的follower剛好還有些資料沒有同步,結果此時leader掛了,然後選舉某個follower成leader之後,待同步的資料就徹底丟了。
給這個topic設定replication.factor引數:這個值必須大於1,要求每個partition必須有至少2個副本
在producer端設定acks=all:這個是要求每條資料,必須是寫入所有replica之後,才能認為是寫成功了
消費丟失
有的訊息中介軟體支援自動ack,當消費者消費到訊息,訊息中介軟體也不管是否消費成功自動ack。這時候一般選擇消費者主動ack比較合適。
2.5 訊息順序性
訊息順序性保證,一般來通過訊息中間實現。訊息中介軟體的順序訊息其實根本上就是區域性有序,一般會對訊息的主體,比如使用者id、商品id這些進行分割槽,每個partition保證自己的佇列有序,兩個Partition之間無順序限制。
然後在消費的過程中,一個Partition同一時刻只能被一個消費者消費變相的同步。所以可以看出需要注意的是,為了保證業務響應需要提取估好Partition的數量,保證系統最夠的併發數量。

image.png
順序訊息的消費特性,導致一旦某個訊息消費阻塞或者異常會導致它所在的Partition的阻塞。