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