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