Java如何實現消費數據隔離?

我是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章