我覺得這個(gè)功能不用實(shí)現(xiàn)
我們?cè)谑褂?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">mq的時(shí)候,就會(huì)很自然思考一個(gè)問題:怎么保證數(shù)據(jù)不丟失?
現(xiàn)在austin接入層是把消息發(fā)到mq,下發(fā)邏輯層從mq消費(fèi)數(shù)據(jù),隨后調(diào)用對(duì)應(yīng)渠道接口來下發(fā)消息。

消息丟棄一般我們考慮的是消費(fèi)端,于是重點(diǎn)看的是下發(fā)邏輯層。
(因?yàn)閷?duì)于mq使用方來說:生產(chǎn)端只要配置mq相關(guān)的參數(shù),在調(diào)用下發(fā)時(shí)有回調(diào)重試機(jī)制。那就足夠了,生產(chǎn)端能做的東西確實(shí)不多)
目前為止,下發(fā)邏輯層(消費(fèi)端)使用的是自動(dòng)提交offset策略。只要消費(fèi)端存在系統(tǒng)重啟或者進(jìn)程被kill掉,那就會(huì)有丟消息的情況。
spring.kafka.consumer.enable-auto-commit=true
當(dāng)前下發(fā)邏輯層(消費(fèi)端)有可能放大了這個(gè)丟棄消息的問題,因?yàn)楝F(xiàn)在是消費(fèi)到mq數(shù)據(jù)后,會(huì)把消息給到線程池去處理。線程池會(huì)指定一個(gè)阻塞隊(duì)列,那隊(duì)列數(shù)量越大,可能由重啟所丟棄的消息就越多。

這里我的策略是:當(dāng)應(yīng)用重啟的時(shí)候,系統(tǒng)里的線程池是優(yōu)雅關(guān)閉的(盡可能等待一段時(shí)間,等阻塞隊(duì)列里沒有消息了,再關(guān)閉線程池)。
但回到問題的本質(zhì)上,只要消費(fèi)端是自動(dòng)提交offset策略,就一定會(huì)有丟消息的問題。所以要做到消費(fèi)端的消息不丟,我們就要設(shè)置為手動(dòng)提交offset,這個(gè)是必要條件。
有沒有必要保證不丟
在探討具體的技術(shù)實(shí)現(xiàn)方案之前,我們來看看在業(yè)務(wù)上有沒有必要保證消息不丟。我剛接觸到消息推送平臺(tái)的時(shí)候,當(dāng)時(shí)那個(gè)交接的哥們告訴我和我學(xué)長(zhǎng):消息少發(fā)比多發(fā)要好。
1、重要的消息用戶很可能會(huì)手動(dòng)重試觸發(fā)。
austin是一個(gè)發(fā)送各類渠道消息的平臺(tái),從我的經(jīng)驗(yàn)來說,這里面最重要的是短信渠道。經(jīng)過austin下發(fā)很可能是登陸驗(yàn)證碼,銀行卡提現(xiàn)驗(yàn)證碼,這類消息從全局上看是最重要的。
而其他渠道,例如push通知欄的通知消息,微信渠道的營(yíng)銷消息,這種消息即便用戶沒收到,也不會(huì)對(duì)用戶帶來很大的使用體驗(yàn)問題。這種消息或許對(duì)絕大數(shù)用戶都是無感知的(少發(fā)幾條,用戶可能更樂意)。
我們先假設(shè)用戶的某一次銀行卡提現(xiàn)的驗(yàn)證碼恰好因?yàn)槲覀冎貑⑾到y(tǒng)而丟棄。這時(shí)候,絕大數(shù)用戶可能懷疑自己的信號(hào)問題,會(huì)繼續(xù)操作,重新發(fā)送一次。
(因?yàn)榭头?jīng)常找我排查這種問題,每次都能看到有好幾條下發(fā)記錄。當(dāng)然了,能到技術(shù)的,99%的問題都不是由系統(tǒng)重啟丟失消息導(dǎo)致的,更多可能是用戶的客戶端本身確實(shí)就存在問題)
2、消息是有時(shí)效性的。比如驗(yàn)證碼這種短信一般就5min的時(shí)效性,由于系統(tǒng)的問題,你超過這個(gè)時(shí)間給用戶發(fā)送,對(duì)用戶的體驗(yàn)是非常差的。
3、消息推送平臺(tái)是有全鏈路追蹤的,是可以知道下發(fā)的消息有沒有到達(dá)到用戶手上,至少都可以知道在我們的系統(tǒng)內(nèi)部執(zhí)行過程中有沒有丟。如果這條消息真的那么重要,那可以單獨(dú)為丟棄的消息單獨(dú)做重發(fā)處理,這些功能在消息推送平臺(tái)都是支持的。

這個(gè)問題我以前的同事也跟我探討過,就是把上面的內(nèi)容給我隔壁的老哥聽的,他說:你就盡扯淡吧,到面試的時(shí)候人家可不認(rèn)你,丟了就是丟了,其他都是借口。
我說:沒事,要是不認(rèn)的話,就把我們處理訂單那一套給他講講嘛,反正處理的思路都是一樣的。
不過啊,廣告訂單邏輯處理又相對(duì)沒那么復(fù)雜,廣告訂單最后是以入數(shù)據(jù)庫作為標(biāo)準(zhǔn)的,又可以接受一定的延遲,只要能保證處理完就行了。
要想client端消費(fèi)數(shù)據(jù)不能丟,肯定是不能使用autoCommit的,所以必須是手動(dòng)提交的。
候選者:我們這邊是這樣實(shí)現(xiàn)的:
候選者:一、從Kafka拉取消息(一次批量拉取500條,這里主要看配置)時(shí)
候選者:二、為每條拉取的消息分配一個(gè)msgId(遞增)
候選者:三、將msgId存入內(nèi)存隊(duì)列(sortSet)中
候選者:四、使用Map存儲(chǔ)msgId與msg(有offset相關(guān)的信息)的映射關(guān)系,通過msgId用來獲取相關(guān)元信息
候選者:五、當(dāng)業(yè)務(wù)處理完消息后,ack時(shí),獲取當(dāng)前處理的消息msgId,然后從sortSet刪除該msgId(此時(shí)代表已經(jīng)處理過了)
候選者:六、接著與sortSet隊(duì)列(本地內(nèi)存隊(duì)列)的首部第一個(gè)Id比較(其實(shí)就是最小的msgId),如果當(dāng)前msgId<=sort Set第一個(gè)ID,則提交當(dāng)前offset
候選者:七、系統(tǒng)即便掛了,在下次重啟時(shí)就會(huì)從sortSet隊(duì)首的消息開始拉取,實(shí)現(xiàn)至少處理一次語義
候選者:八、會(huì)有少量的消息重復(fù),但只要下游做好冪等就OK了。
面試官:嗯,你也提到了冪等,你們這業(yè)務(wù)怎么實(shí)現(xiàn)冪等性的呢?
候選者:嗯,還是以處理訂單消息為例好了。
候選者:冪等Key我們由訂單編號(hào)+訂單狀態(tài)所組成(一筆訂單的狀態(tài)只會(huì)處理一次)
候選者:在處理之前,我們首先會(huì)去查Redis是否存在該Key,如果存在,則說明我們已經(jīng)處理過了,直接丟掉
候選者:如果Redis沒處理過,則繼續(xù)往下處理,最終的邏輯是將處理過的數(shù)據(jù)插入到業(yè)務(wù)DB上,再到最后把冪等Key插入到Redis上
候選者:顯然,單純通過Redis是無法保證冪等的(:
候選者:所以,Redis其實(shí)只是一個(gè)「前置」處理,最終的冪等性是依賴數(shù)據(jù)庫的唯一Key來保證的(唯一Key實(shí)際上也是訂單編號(hào)+狀態(tài))
候選者:總的來說,就是通過Redis做前置處理,DB唯一索引做最終保證來實(shí)現(xiàn)冪等性的
保證austin數(shù)據(jù)不丟需要做什么?
保證數(shù)據(jù)不丟簡(jiǎn)單來說,就是我們要在消費(fèi)端手動(dòng)ack offset,不能再用自動(dòng)提交策略了。這樣當(dāng)我們系統(tǒng)重啟時(shí),kafka會(huì)自動(dòng)從未ack的offset中拉取。
如果要實(shí)現(xiàn)消息推送平臺(tái)不丟消息的話,有幾個(gè)問題是需要考慮的:
1、消息少發(fā)比多發(fā)要好,那么要實(shí)現(xiàn)消息不丟,就必須要在系統(tǒng)內(nèi)實(shí)現(xiàn)冪等。因?yàn)楝F(xiàn)在的消息不丟,一般都是基于【至少一次]消費(fèi)語義去做的。
2、那實(shí)現(xiàn)冪等的邏輯是在調(diào)用渠道下發(fā)接口前,還是渠道下發(fā)接口后?
如果做在下發(fā)接口前,那是不是會(huì)有可能第一次下發(fā)記錄寫入了,但實(shí)際調(diào)用下發(fā)接口卻失敗了,后面的重試都被冪等處理掉了。
如果做在下發(fā)接口后,那是不是會(huì)有可能調(diào)用調(diào)用下發(fā)接口成功了,但寫入冪等處理的消息失敗了,后面的重試就會(huì)導(dǎo)致消息多發(fā)
3、消息是有時(shí)效性的,那如果重試的處理時(shí)間過長(zhǎng),那是不是要考慮把這條消息給丟棄掉,不再重試了。
4、重試的消息不應(yīng)該影響到正常消息的下發(fā),他得作為一種補(bǔ)償?shù)臋C(jī)制,而非主流程。
稍微細(xì)想下技術(shù)實(shí)現(xiàn),應(yīng)該不太好搞,還有很多細(xì)節(jié)的地方得關(guān)注到。比如業(yè)務(wù)上的:應(yīng)該是不需要所有的渠道的所有類型消息都得實(shí)現(xiàn)消息不丟吧?現(xiàn)在的設(shè)計(jì)是追求高性能的,能在短時(shí)間內(nèi)下發(fā)批量的消息。而如果做到所有消息不丟,肯定會(huì)影響到下發(fā)的速率。
什么時(shí)候動(dòng)手?
1、對(duì)于這個(gè)功能吧,有用肯定是有用,但這功能又沒那么急。
2、我估摸對(duì)現(xiàn)有代碼改動(dòng)還是蠻大的,現(xiàn)在我還沒想好該怎么實(shí)現(xiàn)比較好,也一直沒下手。
3、最近工作的事挺多的,沒那么有空。
結(jié)論:先看看想要這個(gè)功能的人多不多,不多就鴿一會(huì)。
都看到這了,如果按上面的理由,我不實(shí)現(xiàn)這個(gè)功能,你認(rèn)不認(rèn)可?
推薦項(xiàng)目
如果想學(xué)Java項(xiàng)目的,我還是
強(qiáng)烈推薦
我的開源項(xiàng)目消息推送平臺(tái)Austin,可以用作
畢業(yè)設(shè)計(jì)
,可以用作
校招
,可以看看
生產(chǎn)環(huán)境是怎么推送消息
的。
倉庫地址(可點(diǎn)擊閱讀原文跳轉(zhuǎn)):https://gitee.com/zhongfucheng/austin
我開通了 股東服務(wù) 內(nèi)容,感興趣可以點(diǎn)擊下方看看,主要針對(duì)的是項(xiàng)目喲



