Java如何實現消費資料隔離?

Java3y發表於2022-02-17

我是3y,一年CRUD經驗用十年的markdown程式設計師??‍?常年被譽為優質八股文選手

今天繼續更新austin專案,如果還沒看過該系列的同學可以點開我的歷史文章回顧下,在看的過程中不要忘記了點贊喲!建議不要漏了或者跳著看,不然這篇就看不懂了,之前寫過的知識點和業務我就不再贅述啦。

今天要實現的是handler模組的消費資料隔離。在聊這個之前,先看下之前的實現是怎麼樣的。

austin-api接收到了請求之後,將請求發往Kafka,topicName為austin。而在austin-handler起了一個groupName名為austinGroup監聽austin這個topic的資料,進而實現訊息傳送。

從系統架構來說,austin專案是可以傳送多種型別訊息的:簡訊、微信小程式、郵件等等等

那如果是單個topic單個group的話,有沒有想過一個問題:如果某個傳送渠道介面存在異常,超時了,此時會怎麼樣

沒錯,訊息都會堵住,因為它們消費同一個topic,用的是同一個消費者。

01、資料隔離

要破局?很簡單。多topic多group就行啦

上面這種能解決所有問題嗎?並不。即便是同一個渠道,但不同型別的訊息傳送特性是不一樣的。比如我要發push營銷訊息,有可能在某個時刻就要推送4000W的人群。

那這4000W人在短時間內完全傳送出去,不太現實。這很可能意味著會影響到通知類的push訊息

還要破局?很簡單。 畢竟我們在設計訊息模板的時候就已經考慮到這點了。訊息模板有msgType欄位來標識當前的模板屬於哪種型別,那我們可以根據不同的訊息型別再劃分對應的group。

從理論上來說,我們可以為每種渠道的每種訊息型別單獨區分一個topic和group。因為topic間的資料是隔離的,不同的group間消費也是隔離的,那我們消費時肯定是資料隔離的。

不過,我目前的做法是:單topic多group。消費是隔離的,但生產的topic是共享的。我認為這樣程式碼會更加清晰和易懂些,後期如果存在瓶頸了我們可以繼續改。

02、消費端設計

從上面已經定了通過單topic多group來實現資料隔離。比如,我目前定義了6個渠道(im/push/郵件/簡訊/小程式/微信服務號)和3種訊息型別(通知/營銷/驗證碼),那相當於起了18個消費者。

從kafka獲取得到訊息以後,我暫定規劃是走幾個步驟:訊息丟棄->去重->真正傳送

從本質上看去重傳送訊息都是網路IO密集型。於是,為了提高吞吐量,我這邊決定消費Kafka後存入快取,做一層緩衝區

做一層緩衝區可提高吞吐量,但同樣會帶來別的問題。如:當應用重啟時,緩衝區的資料還沒消費完,那是不是就會丟失?

這個我們可以後面再看看怎麼把帶來的問題給搞掂(持續關注,專案優化後面多著呢)。現在還是認為緩衝區的利大於弊,所以回到緩衝區上。

緩衝區給我的第一反應是實現生產者消費者模式

要實現這種模式,我初想了下挺簡單的:消費Kafka的訊息作為生產者,然後把資料扔進阻塞佇列上,開多個執行緒去消費阻塞佇列的資料就完事了。

後來又想了下,直接執行緒池不就完事了嗎?執行緒池不就是生產者和消費者的實現嗎。

於是乎,架構就變成了下圖:

03、程式碼設計

在消費端首先看Receiver的程式碼,該類看起來看簡單,就只有一個@KafkaListener註解修飾方法,從Kafka消費出來隨後交給pending做處理

我用的是@KafkaListener註解從Kafka拉取訊息,而沒有用低階的Kafka api,原因無他:在專案前期無需做到完美,等有瓶頸的時候再想辦法就好了。雖說如此,但我寫的時候還是給我帶來了不少的麻煩。

第一個問題@KafkaListener是一個註解,從原始碼註釋看它的傳值只能夠用Spring EL表示式和讀取某個配置。但要知道的是,我的目的是想有多個group消費同一個topic。而我不可能說給每個group都定義一個消費的方法吧?(寫這種破程式碼,我都睡不著覺

翻了一個晚上技術部落格我都沒找到方案,甚至還發了個朋友圈吐槽下有沒有人遇到過。第二天我仔細翻了下Spring的官方文件,終於給我找到了方案。

還是官方文件實在

有了解決辦法了以後,那事情就好辦了。既然我是每種訊息渠道的每種訊息型別都要隔離,那我把這給列舉出來就完事啦!

我的Receiver是多例的,那麼只要我遍歷這個List就好了(初始化消費者在ReceiverStart類上)。

解決了用@KafkaListener註解動態傳入groupId 進而建立多個消費者了之後。

我又遇到了第二個問題:Spring有@Aysnc註解來優雅實現執行緒池的方法呼叫。我之前是沒用過@Aysnc註解的,但我看了下原理和使用姿勢。我感覺這樣挺優雅的(優雅永不過時)。但是用@Aysnc是肯定要自己建立執行緒池,並且我要給每個消費者都建立自己獨有的執行緒池。而我不可能說給每個group都定義一個建立執行緒池的方法吧?(寫這種破程式碼,我都睡不著覺

這次翻了官網和各種技術部落格,都沒能解決掉我的問題:在Spring環境下@Async註解上動態傳入執行緒池例項,以及建立執行緒池例項時可支援根據條件傳參。

最後只能放棄掉@Aysnc註解了,以程式設計的方式去實現:

下面是TaskPendingHolder的實現(無非就是給每個消費者建立對應的執行緒池),後面會考慮是否做成動態的:

而Task實現目前就比較簡單啦,直接呼叫對應的Handler進而下發訊息就好:

04、總結

程式碼看似簡單,業務看似容易理解,但是要知道的是即便是很多小公司的生產專案都沒有這種設計。一把梭可真的是太常見了(功能又不是不能實現,程式碼又不是不能跑,最主要的:人也不是不能跑)

這篇文章主要講述了一個思路:在消費MQ的時候,多group是可以實現資料隔離的,想要提高消費的吞吐量,可以再做一層緩衝區(前提是消費是IO密集型的)

關注我的微信公眾號【Java3y】除了技術我還會聊點日常,有些話只能悄悄說~ 【對線面試官+從零編寫Java專案】 持續高強度更新中!求star!!原創不易!!求三連!!

原始碼Gitee連結:gitee.com/austin

原始碼GitHub連結:github.com/austin

相關文章