前序
在編程武俠世界中,有一個(gè)門(mén)派“天機(jī)樓”辫樱,連接并協(xié)調(diào)各大門(mén)派之間的關(guān)系峭拘,確保整個(gè)江湖的運(yùn)作流暢無(wú)阻。天機(jī)樓住要的業(yè)務(wù)范圍主要如下:
- 信息傳遞的信使:
天機(jī)樓就像是江湖中的飛鴿傳書(shū)狮暑,確保各門(mén)派之間的信息能夠快速鸡挠、準(zhǔn)確地傳遞。無(wú)論是戰(zhàn)斗指令搬男、情報(bào)交換還是緊急求援拣展,天機(jī)樓都能可靠地完成任務(wù)。 - 系統(tǒng)穩(wěn)定的守護(hù)者:
它如同一位隱形的護(hù)法缔逛,時(shí)刻監(jiān)控著江湖的運(yùn)作备埃,確保各門(mén)派的系統(tǒng)穩(wěn)定運(yùn)行姓惑,避免因系統(tǒng)故障而引發(fā)的江湖動(dòng)亂。 - 性能優(yōu)化的高手:
天機(jī)樓精通各種優(yōu)化技巧按脚,能夠在復(fù)雜的江湖環(huán)境中找到最佳的解決方案于毙,提升系統(tǒng)的性能,讓各系統(tǒng)的操作更加高效與流暢辅搬。 - 負(fù)載均衡的調(diào)度者:
如同武林盟主一般唯沮,天機(jī)樓可以合理調(diào)配各門(mén)派的資源,確保每個(gè)門(mén)派都能均衡發(fā)展堪遂,避免資源過(guò)度集中或分配不均烂翰。 - 問(wèn)題解決的醫(yī)者:
當(dāng)江湖中出現(xiàn)問(wèn)題時(shí),天機(jī)樓能夠迅速診斷并修復(fù)問(wèn)題蚤氏,像神醫(yī)華佗一樣,確保江湖的和平與穩(wěn)定踊兜。
天機(jī)樓的武器:
- 消息隊(duì)列:
如同傳遞消息的飛鴿竿滨,確保信息快速且準(zhǔn)確地到達(dá)目的地。 - 緩存系統(tǒng):
猶如藏在暗處的密庫(kù)捏境,能快速提供所需的資源于游。 - 負(fù)載均衡:
如同武林盟主,能夠調(diào)配各系統(tǒng)的資源垫言,確保每個(gè)系統(tǒng)都能均衡發(fā)展贰剥。
阿強(qiáng)所在的世界中,天機(jī)樓一直是基石般的存在筷频,這個(gè)神秘的組織中每個(gè)人的技術(shù)功力都非常強(qiáng)悍蚌成。以至于天機(jī)樓雖然人數(shù)不多,但是地位與實(shí)力卻一直很高凛捏。此組織成員一般散落在世界各地担忧,統(tǒng)一由天機(jī)閣管理。而天機(jī)閣中的成員所接的任務(wù)難度跟積分跟平常門(mén)派所發(fā)布的任務(wù)都要高出不少坯癣,也正是因?yàn)槿绱似渴ⅲ芏嘤X(jué)得自己實(shí)力還不錯(cuò)的人都想加入其中,但很多人都低估 了其考核難度示罗,而此組織如果你實(shí)力沒(méi)有達(dá)到他的考核標(biāo)準(zhǔn)是沒(méi)有其他的途徑進(jìn)入的惩猫。而加入天機(jī)樓除了能夠獲取高積分的任務(wù)之外,還有的好處是蚜点,此組織跟自己的門(mén)派不沖突轧房,也就是說(shuō),天機(jī)樓的身份不影響你可以在任何門(mén)派中擔(dān)任職位绍绘。而阿強(qiáng)就是除了是目前所在代碼劍宗的武林高手身份外锯厢,也在天機(jī)閣中擔(dān)任中級(jí)俠客一職皮官。而所謂的天機(jī)閣內(nèi)部也有著自己的一套等級(jí)劃分:
- 初級(jí)俠客(新手):
職責(zé):負(fù)責(zé)基礎(chǔ)的中間件配置和維護(hù)工作,處理常見(jiàn)的小問(wèn)題和簡(jiǎn)單的優(yōu)化任務(wù)实辑。
能力:掌握基礎(chǔ)的中間件技術(shù)捺氢,能夠進(jìn)行基本的安裝、配置和常見(jiàn)問(wèn)題排查剪撬。 - 中級(jí)俠客(高手):
職責(zé):負(fù)責(zé)復(fù)雜的中間件部署和優(yōu)化任務(wù)摄乒,能夠獨(dú)立解決較為復(fù)雜的問(wèn)題。
能力:精通常見(jiàn)的中間件技術(shù)残黑,具備較強(qiáng)的系統(tǒng)優(yōu)化和問(wèn)題解決能力馍佑,能夠進(jìn)行性能調(diào)優(yōu)和負(fù)載均衡配置。 - 高級(jí)俠客(大俠):
職責(zé):負(fù)責(zé)整個(gè)系統(tǒng)的中間件架構(gòu)設(shè)計(jì)和優(yōu)化梨水,能夠處理高難度的技術(shù)問(wèn)題和進(jìn)行系統(tǒng)級(jí)的優(yōu)化拭荤。
能力:深諳各種中間件技術(shù),具備系統(tǒng)架構(gòu)設(shè)計(jì)能力疫诽,能夠進(jìn)行復(fù)雜的系統(tǒng)集成和全面的性能優(yōu)化舅世。 - 宗師(頂級(jí)專(zhuān)家):
職責(zé):負(fù)責(zé)中間件技術(shù)的戰(zhàn)略規(guī)劃和創(chuàng)新,領(lǐng)導(dǎo)團(tuán)隊(duì)進(jìn)行技術(shù)攻關(guān)和前沿技術(shù)探索奇徒。
能力:擁有深厚的技術(shù)積累和豐富的實(shí)戰(zhàn)經(jīng)驗(yàn)雏亚,能夠在技術(shù)上引領(lǐng)團(tuán)隊(duì),推動(dòng)中間件技術(shù)的發(fā)展和創(chuàng)新摩钙。
阿強(qiáng)處理完上次門(mén)派中的pagehelper的緊急任務(wù)后就一直沉浸在進(jìn)行門(mén)派中的另一個(gè)開(kāi)發(fā)任務(wù)中罢低,不知道過(guò)了多久,阿強(qiáng)滿(mǎn)臉興奮地從自己的洞府走出胖笛,他經(jīng)過(guò)這段時(shí)間的閉關(guān)网持,終于將那個(gè)開(kāi)發(fā)任務(wù)給開(kāi)發(fā)完并交給斷點(diǎn)神教小美測(cè)試,此時(shí)的他一身輕松地望向天空不禁有些恍惚长踊,大約過(guò)了2刻鐘翎碑,天機(jī)閣突然發(fā)布了一條任務(wù)“L服務(wù)出現(xiàn)mq重新消費(fèi),請(qǐng)及時(shí)處理”之斯,本來(lái)此時(shí)的阿強(qiáng)就是無(wú)事一身輕日杈,正想著去天機(jī)閣找個(gè)任務(wù),因此看到任務(wù)這么合時(shí)宜地發(fā)布佑刷,阿強(qiáng)毫不猶豫就認(rèn)領(lǐng)了下來(lái)莉擒,不多時(shí),阿強(qiáng)就全身心地投入到此任務(wù)中.......瘫絮。
第四章 什么涨冀?mq重復(fù)消費(fèi)了?
15分鐘過(guò)后麦萤,阿強(qiáng)了解到此次的問(wèn)題暴露是由于L服務(wù)側(cè)下游F服務(wù)(沒(méi)錯(cuò)鹿鳖,就是上次pagehelper 分頁(yè)sql問(wèn)題服務(wù))生成了兩條一樣的由L服務(wù)調(diào)用產(chǎn)生的訂單扁眯。而L調(diào)用F服務(wù)生成訂單的時(shí)序圖如下:
[圖片上傳失敗...(image-91a2ca-1721209265184)]
從時(shí)序圖不難看出,這個(gè)生成訂單的過(guò)程是一個(gè)異步過(guò)程翅帜,而L服務(wù)的異步處理是通過(guò)MQ來(lái)實(shí)現(xiàn)姻檀。知道了整體交互邏輯的阿強(qiáng)打開(kāi)idea查看L服務(wù)中MQ的消費(fèi)邏輯:
//下面是L服務(wù)消息消費(fèi)的偽代碼
public MsgStatus onMessage(ConsumeMessage msg){
log.info("【xxx異步處理】接收MQ消息:{}", message);
if (StringUtils.isBlank(message)) {
return MsgStatus.SUCCEED;
}
Entity entity = dataConversion();//數(shù)據(jù)轉(zhuǎn)換
LOrderEntity LOrderEntity = LOrderRepository.selectByLOrderId(entity.getLOrderId());
//冪等校驗(yàn),但是在某些場(chǎng)景下沒(méi)有用涝滴,如本案例中
if(StringUtils.isNotEmpty(LOrderEntity.getTaskId()))){
log.warn("【xxx異步處理】mq重復(fù)發(fā)送消息");
return MsgStatus.SUCCEED;
}
//構(gòu)建請(qǐng)求入?yún)? FOrderReq req = buildReq(LOrderEntity,msg);
RpcResponse res = FRpc.createOrder(req);
//構(gòu)建更新字段實(shí)體
LOrderEntity LOrderEntity1 = buildLOrderField(res.getTaskId(),res.getStates());
LOrderRepository.updateOrder(LOrderEntity1);
return MsgStatus.SUCCEED;
}
阿強(qiáng)梳理完L服務(wù)的異步消息處理邏輯绣版,隨即就開(kāi)始通過(guò)天書(shū)法器查看L服務(wù)的日志,不多時(shí)歼疮,阿強(qiáng)就通過(guò)“【xxx異步處理】接收MQ消息”關(guān)鍵字在天書(shū)上看到了兩條很奇怪的日志杂抽,這兩條日志都是mq消費(fèi)邏輯打印出來(lái),且后面輸出的mq消息體字段全都是一樣韩脏。
[圖片上傳失敗...(image-ac2f94-1721209265184)]
看到兩條一樣的消息體缩麸,阿強(qiáng)陷入了沉思,隨即他又通過(guò)“發(fā)送mq”關(guān)鍵字赡矢,在這條鏈路上搜索杭朱,發(fā)現(xiàn)只出現(xiàn)了一條日志,也就是說(shuō)济竹,mq生產(chǎn)者只發(fā)送了一條消息,卻消費(fèi)了兩次霎槐。
[圖片上傳失敗...(image-16ce1c-1721209265184)]
看到這種情況送浊,阿強(qiáng)腦子里面浮現(xiàn)幾種猜想,第一種是:天機(jī)樓的消息處理發(fā)生抖動(dòng)導(dǎo)致的重復(fù)消費(fèi)丘跌;第二種是:L服務(wù)的消費(fèi)者沒(méi)有去ack袭景。此時(shí)的阿強(qiáng)也沒(méi)辦法確定是那種情況導(dǎo)致的重復(fù)消費(fèi),此時(shí)最好的辦法就是使用排除法闭树。同身為天機(jī)閣成員耸棒,阿強(qiáng)有權(quán)限去查看某個(gè)消息的一個(gè)消費(fèi)情況跟整個(gè)消息處理服務(wù)的監(jiān)控。當(dāng)阿強(qiáng)打開(kāi)天眼系統(tǒng)查看消息處理服務(wù)的監(jiān)控报辱,發(fā)現(xiàn)整個(gè)服務(wù)的指標(biāo)都很正常与殃,并沒(méi)有什么抖動(dòng)。此時(shí)第一種猜想已經(jīng)驗(yàn)證是沒(méi)有問(wèn)題的碍现,阿強(qiáng)快馬加鞭地開(kāi)始第二種猜想驗(yàn)證幅疼。只見(jiàn)他打開(kāi)天書(shū)系統(tǒng)的鏈路耗時(shí)
[圖片上傳失敗...(image-977d-1721209265184)]
發(fā)現(xiàn)整個(gè)消費(fèi)的耗時(shí)遠(yuǎn)遠(yuǎn)沒(méi)有達(dá)到消息ack處理超時(shí)時(shí)間3分鐘的。此時(shí)的阿強(qiáng)雙眉緊湊昼接,腦中已經(jīng)開(kāi)始了頭腦風(fēng)暴爽篷。如果沒(méi)有達(dá)到消息ack處理超時(shí)時(shí)間,那么還會(huì)有什么場(chǎng)景會(huì)讓mq消息ack失效呢慢睡?阿強(qiáng)在腦中重新回顧了一下RocketMQ的ack機(jī)制(因?yàn)長(zhǎng)系統(tǒng)使用的RocketMQ消息中間件):
- 消息消費(fèi)確認(rèn)機(jī)制
RocketMQ 采用了“消費(fèi)者主動(dòng)確認(rèn)”的機(jī)制逐工,即消費(fèi)者在成功處理完消息后铡溪,主動(dòng)向 Broker 發(fā)送 ACK 確認(rèn)。這與一些消息隊(duì)列系統(tǒng)的自動(dòng)確認(rèn)機(jī)制不同泪喊,能夠確保消息被成功處理后才被確認(rèn)棕硫。 - 消息消費(fèi)過(guò)程
消息發(fā)送:生產(chǎn)者將消息發(fā)送到 RocketMQ Broker。
消息存儲(chǔ):Broker 接收到消息后窘俺,將消息存儲(chǔ)在磁盤(pán)中饲帅,并將消息寫(xiě)入 CommitLog。
消息拉攘隼帷:消費(fèi)者從 Broker 拉取消息進(jìn)行消費(fèi)灶泵。
消息處理:消費(fèi)者接收消息并進(jìn)行處理。
確認(rèn)消息:消息處理成功后对途,消費(fèi)者向 Broker 發(fā)送 ACK 確認(rèn)赦邻。 - 消息重試機(jī)制
如果消費(fèi)者在處理消息過(guò)程中出現(xiàn)異常或失敗实檀,沒(méi)有發(fā)送 ACK 確認(rèn)惶洲,RocketMQ 會(huì)認(rèn)為該消息未被成功消費(fèi),并會(huì)進(jìn)行重試膳犹。重試機(jī)制確保消息不會(huì)丟失恬吕,但可能會(huì)出現(xiàn)消息重復(fù)消費(fèi)的情況。為此须床,消費(fèi)者需要實(shí)現(xiàn)冪等性處理铐料。
此時(shí)的阿強(qiáng)已經(jīng)沒(méi)有一開(kāi)始接收這個(gè)任務(wù)時(shí)輕松的心態(tài),他的腦海不斷浮現(xiàn)各種知識(shí)內(nèi)容豺旬,去思考可能出現(xiàn)場(chǎng)景钠惩。大約過(guò)了2小時(shí),阿強(qiáng)的眼睛閃過(guò)一絲光亮族阅,他連忙打開(kāi)了天劍部署系統(tǒng)去查看最后一次L服務(wù)部署時(shí)間篓跛,不多時(shí),阿強(qiáng)深沉的眼神中閃過(guò)一絲興奮坦刀,嘴角露出了一絲笑容愧沟。但是為了確認(rèn)自己的猜想,他回顧了一下rocketmq消費(fèi)的存儲(chǔ)方式鲤遥、消費(fèi)模式:
- 消費(fèi)進(jìn)度的存儲(chǔ)方式
RocketMQ 支持兩種消費(fèi)進(jìn)度(offset)的存儲(chǔ)方式:
Broker 端存儲(chǔ):消費(fèi)進(jìn)度保存在 Broker 上央渣,消費(fèi)者重啟后會(huì)從 Broker 獲取最新的消費(fèi)進(jìn)度。
消費(fèi)者本地存儲(chǔ):消費(fèi)進(jìn)度保存在本地磁盤(pán)渴频,消費(fèi)者重啟后會(huì)從本地磁盤(pán)讀取消費(fèi)進(jìn)度芽丹。
如果消費(fèi)進(jìn)度存儲(chǔ)在 Broker 上,那么即使消費(fèi)者重新部署卜朗,重新啟動(dòng)后也會(huì)從 Broker 獲取最新的消費(fèi)進(jìn)度拔第,避免消息重復(fù)消費(fèi)的情況咕村。 - 消費(fèi)模式
RocketMQ 支持兩種消費(fèi)模式:
集群消費(fèi)(Clustering):同一個(gè)消費(fèi)組內(nèi)的多個(gè)消費(fèi)者實(shí)例會(huì)均攤消息,每條消息只會(huì)被其中一個(gè)消費(fèi)者實(shí)例消費(fèi)蚊俺。當(dāng)某個(gè)消費(fèi)者實(shí)例重啟時(shí)懈涛,其他實(shí)例會(huì)接管它的消費(fèi)任務(wù)。
廣播消費(fèi)(Broadcasting):每個(gè)消費(fèi)者實(shí)例都會(huì)消費(fèi)所有的消息泳猬。當(dāng)某個(gè)消費(fèi)者實(shí)例重啟時(shí)批钠,重新啟動(dòng)后會(huì)重新消費(fèi)所有未確認(rèn)的消息。
再結(jié)合L項(xiàng)目的消費(fèi)模式得封,真相大白了埋心,L項(xiàng)目當(dāng)時(shí)由于有人在重啟項(xiàng)目,消息還未ack忙上,機(jī)器重啟導(dǎo)致的消息重復(fù)消費(fèi)拷呆。知道原因的阿強(qiáng)呼出一大口濁氣,臉上的眉目也放松下來(lái)疫粥。接下來(lái)茬斧,就只需怎么處理這種特殊場(chǎng)景。
[圖片上傳失敗...(image-415b2-1721209265184)]
怎么處理這種問(wèn)題梗逮,阿強(qiáng)心里已經(jīng)有了方案项秉,第一種方案是讓F服務(wù)做冪等處理;第二種方案是在L服務(wù)的mq消息消費(fèi)邏輯里面做一個(gè)冪等處理慷彤。最終阿強(qiáng)結(jié)合本次任務(wù)選擇了第二種方案娄蔼,他用idea
打開(kāi)了L系統(tǒng)的代碼,并進(jìn)行了一個(gè)改造:
//下面是L服務(wù)消息消費(fèi)的偽代碼
public MsgStatus onMessage(ConsumeMessage msg){
log.info("【xxx異步處理】接收MQ消息:{}", message);
if (StringUtils.isBlank(message)) {
return MsgStatus.SUCCEED;
}
Entity entity = dataConversion();//數(shù)據(jù)轉(zhuǎn)換
RedisLock lock = RedisClientManagement.createLock(entity.getUserId,entity.getApplyNo());
try {
if (lock.blockAcquireLock(RedisTimeOut.FIVE_MINUTE,RedisTimeOut.SECOUND)) {
LOrderEntity LOrderEntity = LOrderRepository.selectByLOrderId(entity.getLOrderId());
//冪等校驗(yàn)瞬欧,但是在某些場(chǎng)景下沒(méi)有用贷屎,如本案例中
if(StringUtils.isNotEmpty(LOrderEntity.getTaskId()))){
log.warn("【xxx異步處理】mq重復(fù)發(fā)送消息");
return MsgStatus.SUCCEED;
}
//構(gòu)建請(qǐng)求入?yún)? FOrderReq req = buildReq(LOrderEntity,msg);
RpcResponse res = FRpc.createOrder(req);
//構(gòu)建更新字段實(shí)體
LOrderEntity LOrderEntity1 = buildLOrderField(res.getTaskId(),res.getStates());
LOrderRepository.updateOrder(LOrderEntity1);
}
}else{
log.info("Existing lock key = {}",key);
}
}catch (Exception e) {
log.error("消費(fèi)異常罢防,不重新消費(fèi)",e);
return MsgStatus.SUCCEED;
} finally {
lock.releaseLock();
}
return MsgStatus.SUCCEED;
}
當(dāng)阿強(qiáng)把改完的代碼提交完艘虎,戀戀不舍地看了看美好世界,一會(huì)后又開(kāi)始進(jìn)行回到洞府內(nèi)修煉起來(lái)....