隨著分布式服務(wù)架構(gòu)的流行與普及,原來(lái)在單體應(yīng)用中執(zhí)行的多個(gè)邏輯操作,現(xiàn)在被拆分成了多個(gè)服務(wù)之間的遠(yuǎn)程調(diào)用。雖然服務(wù)化為我們的系統(tǒng)帶來(lái)了水平伸縮的能力,然而隨之而來(lái)挑戰(zhàn)就是分布式事務(wù)問(wèn)題蜻牢,多個(gè)服務(wù)之間使用自己?jiǎn)为?dú)維護(hù)的數(shù)據(jù)庫(kù)烤咧,它們彼此之間不在同一個(gè)事務(wù)中,假如A執(zhí)行成功了抢呆,B執(zhí)行卻失敗了煮嫌,而A的事務(wù)此時(shí)已經(jīng)提交,無(wú)法回滾抱虐,那么最終就會(huì)導(dǎo)致兩邊數(shù)據(jù)不一致性的問(wèn)題昌阿;盡管很早之前就有基于兩階段提交的XA分布式事務(wù),但是這類(lèi)方案因?yàn)樾枰Y源的全局鎖定恳邀,導(dǎo)致性能極差懦冰;因此后面就逐漸衍生出了消息最終一致性、TCC等柔性事務(wù)
的分布式事務(wù)方案谣沸,本文主要分析的是基于消息的最終一致性方案刷钢。
普通消息的處理流程
- 消息生成者發(fā)送消息
- MQ收到消息,將消息進(jìn)行持久化乳附,在存儲(chǔ)中新增一條記錄
- 返回ACK給生產(chǎn)者
- MQ push 消息給對(duì)應(yīng)的消費(fèi)者内地,然后等待消費(fèi)者返回ACK
- 如果消息消費(fèi)者在指定時(shí)間內(nèi)成功返回ack,那么MQ認(rèn)為消息消費(fèi)成功赋除,在存儲(chǔ)中刪除消息阱缓,即執(zhí)行第6步;如果MQ在指定時(shí)間內(nèi)沒(méi)有收到ACK举农,則認(rèn)為消息消費(fèi)失敗荆针,會(huì)嘗試重新push消息,重復(fù)執(zhí)行4、5并蝗、6步驟
- MQ刪除消息
普通消息處理存在的一致性問(wèn)題
我們以訂單創(chuàng)建為例祭犯,訂單系統(tǒng)先創(chuàng)建訂單(本地事務(wù))秸妥,再發(fā)送消息給下游處理滚停;如果訂單創(chuàng)建成功,然而消息沒(méi)有發(fā)送出去粥惧,那么下游所有系統(tǒng)都無(wú)法感知到這個(gè)事件键畴,會(huì)出現(xiàn)臟數(shù)據(jù);
public void processOrder() {
// 訂單處理(業(yè)務(wù)操作)
orderService.process();
// 發(fā)送訂單處理成功消息(發(fā)送消息)
sendBizMsg ();
}
如果先發(fā)送訂單消息突雪,再創(chuàng)建訂單起惕;那么就有可能消息發(fā)送成功,但是在訂單創(chuàng)建的時(shí)候卻失敗了咏删,此時(shí)下游系統(tǒng)卻認(rèn)為這個(gè)訂單已經(jīng)創(chuàng)建惹想,也會(huì)出現(xiàn)臟數(shù)據(jù)。
public void processOrder() {
// 發(fā)送訂單處理成功消息(發(fā)送消息)
sendBizMsg ();
// 訂單處理(業(yè)務(wù)操作)
orderService.process();
}
一個(gè)錯(cuò)誤的想法
此時(shí)可能有同學(xué)會(huì)想督函,我們可否將消息發(fā)送和業(yè)務(wù)處理放在同一個(gè)本地事務(wù)中來(lái)進(jìn)行處理嘀粱,如果業(yè)務(wù)消息發(fā)送失敗激挪,那么本地事務(wù)就回滾,這樣是不是就能解決消息發(fā)送的一致性問(wèn)題呢?
@Transactionnal
public void processOrder() {
try{
// 訂單處理(業(yè)務(wù)操作)
orderService.process();
// 發(fā)送訂單處理成功消息(發(fā)送消息)
sendBizMsg ();
}catch(Exception e){
事務(wù)回滾;
}
}
消息發(fā)送的異常情況分析
可能的情況 | 一致性 |
---|---|
訂單處理成功锋叨,然后突然宕機(jī)编矾,事務(wù)未提交扮授,消息沒(méi)有發(fā)送出去 | 一致 |
訂單處理成功,由于網(wǎng)絡(luò)原因或者M(jìn)Q宕機(jī),消息沒(méi)有發(fā)送出去履植,事務(wù)回滾 | 一致 |
訂單處理成功,消息發(fā)送成功熔吗,但是MQ由于其他原因姆钉,導(dǎo)致消息存儲(chǔ)失敗,事務(wù)回滾 | 一致 |
訂單處理成功听诸,消息存儲(chǔ)成功炉奴,但是MQ處理超時(shí),從而ACK確認(rèn)失敗蛇更,導(dǎo)致發(fā)送方本地事務(wù)回滾 | 不一致 |
從上面的情況分析瞻赶,我們可以看到,使用普通的處理方式派任,無(wú)論如何砸逊,都無(wú)法保證業(yè)務(wù)處理與消息發(fā)送兩邊的一致性,其根本的原因就在于:遠(yuǎn)程調(diào)用掌逛,結(jié)果最終可能為成功师逸、失敗、超時(shí)豆混;而對(duì)于超時(shí)的情況篓像,處理方最終的結(jié)果可能是成功,也可能是失敗皿伺,調(diào)用方是無(wú)法知曉的员辩。 筆者就曾經(jīng)在項(xiàng)目中出現(xiàn)類(lèi)似的情況,調(diào)用方先在本地寫(xiě)數(shù)據(jù)鸵鸥,然后發(fā)起RPC服務(wù)調(diào)用奠滑,但是處理方由于DB數(shù)據(jù)量比較大,導(dǎo)致處理超時(shí)妒穴,調(diào)用方在出現(xiàn)超時(shí)異常后宋税,直接回滾本地事務(wù),從而導(dǎo)致調(diào)用方這邊沒(méi)數(shù)據(jù)讼油,而處理方那邊數(shù)據(jù)卻已經(jīng)寫(xiě)入了杰赛,最終導(dǎo)致兩邊業(yè)務(wù)數(shù)據(jù)的不一致。為了保證兩邊數(shù)據(jù)的一致性矮台,我們只能從其他地方尋找新的突破口乏屯。
事務(wù)消息
由于傳統(tǒng)的處理方式無(wú)法解決消息生成者本地事務(wù)處理成功
與消息發(fā)送成功
兩者的一致性問(wèn)題阔墩,因此事務(wù)消息就誕生了,它實(shí)現(xiàn)了消息生成者本地事務(wù)與消息發(fā)送的原子性瓶珊,保證了消息生成者本地事務(wù)處理成功與消息發(fā)送成功的最終一致性
問(wèn)題啸箫。
事務(wù)消息處理的流程
事務(wù)消息與普通消息的區(qū)別就在于消息生產(chǎn)環(huán)節(jié),生產(chǎn)者首先預(yù)發(fā)送一條消息到MQ(這也被稱(chēng)為發(fā)送half消息)
MQ接受到消息后伞芹,先進(jìn)行持久化忘苛,則存儲(chǔ)中會(huì)新增一條狀態(tài)為
待發(fā)送
的消息然后返回ACK給消息生產(chǎn)者,此時(shí)MQ不會(huì)觸發(fā)消息推送事件
生產(chǎn)者預(yù)發(fā)送消息成功后唱较,執(zhí)行本地事務(wù)
執(zhí)行本地事務(wù)扎唾,執(zhí)行完成后,發(fā)送執(zhí)行結(jié)果給MQ
MQ會(huì)根據(jù)結(jié)果刪除或者更新消息狀態(tài)為
可發(fā)送
如果消息狀態(tài)更新為
可發(fā)送
南缓,則MQ會(huì)push消息給消費(fèi)者胸遇,后面消息的消費(fèi)和普通消息是一樣的
注意點(diǎn):由于MQ通常都會(huì)保證消息能夠投遞成功,因此汉形,如果業(yè)務(wù)沒(méi)有及時(shí)返回ACK結(jié)果纸镊,那么就有可能造成MQ的重復(fù)消息投遞問(wèn)題。因此概疆,對(duì)于消息最終一致性的方案逗威,消息的消費(fèi)者必須要對(duì)消息的消費(fèi)支持冪等,不能造成同一條消息的重復(fù)消費(fèi)的情況岔冀。
事務(wù)消息異常情況分析
異常情況 | 一致性 | 處理異常方法 |
---|---|---|
消息未存儲(chǔ)凯旭,業(yè)務(wù)操作未執(zhí)行 | 一致 | 無(wú) |
存儲(chǔ)待發(fā)送 消息成功,但是ACK失敗使套,導(dǎo)致業(yè)務(wù)未執(zhí)行(可能是MQ處理超時(shí)罐呼、網(wǎng)絡(luò)抖動(dòng)等原因) |
不一致 | MQ確認(rèn)業(yè)務(wù)操作結(jié)果,處理消息(刪除消息) |
存儲(chǔ)待發(fā)送 消息成功侦高,ACK成功嫉柴,業(yè)務(wù)執(zhí)行(可能成功也可能失敗),但是MQ沒(méi)有收到生產(chǎn)者業(yè)務(wù)處理的最終結(jié)果 |
不一致 | MQ確認(rèn)業(yè)務(wù)操作結(jié)果矫膨,處理消息(根據(jù)就業(yè)務(wù)處理結(jié)果差凹,更新消息狀態(tài)期奔,如果業(yè)務(wù)執(zhí)行成功侧馅,則投遞消息,失敗則刪除消息) |
業(yè)務(wù)處理成功呐萌,并且發(fā)送結(jié)果給MQ馁痴,但是MQ更新消息失敗,導(dǎo)致消息狀態(tài)依舊為待發(fā)送
|
不一致 | 同上 |
支持事務(wù)消息的MQ
現(xiàn)在目前較為主流的MQ肺孤,比如ActiveMQ罗晕、RabbitMQ济欢、Kafka、RocketMQ等小渊,只有RocketMQ支持事務(wù)消息法褥。據(jù)筆者了解,早年阿里對(duì)MQ增加事務(wù)消息也是因?yàn)橹Ц秾毮沁呉驗(yàn)闃I(yè)務(wù)上的需求而產(chǎn)生的酬屉。因此半等,如果我們希望強(qiáng)依賴(lài)一個(gè)MQ的事務(wù)消息來(lái)做到消息最終一致性的話(huà),在目前的情況下呐萨,技術(shù)選型上只能去選擇RocketMQ來(lái)解決杀饵。上面我們也分析了事務(wù)消息所存在的異常情況,即MQ存儲(chǔ)了待發(fā)送
的消息谬擦,但是MQ無(wú)法感知到上游處理的最終結(jié)果切距。對(duì)于RocketMQ而言,它的解決方案非常的簡(jiǎn)單惨远,就是其內(nèi)部實(shí)現(xiàn)會(huì)有一個(gè)定時(shí)任務(wù)谜悟,去輪訓(xùn)狀態(tài)為待發(fā)送
的消息,然后給producer發(fā)送check請(qǐng)求北秽,而producer必須實(shí)現(xiàn)一個(gè)check監(jiān)聽(tīng)器赌躺,監(jiān)聽(tīng)器的內(nèi)容通常就是去檢查與之對(duì)應(yīng)的本地事務(wù)是否成功(一般就是查詢(xún)DB),如果成功了羡儿,則MQ會(huì)將消息設(shè)置為可發(fā)送
礼患,否則就刪除消息。
常見(jiàn)的問(wèn)題
-
問(wèn):如果預(yù)發(fā)送消息失敗掠归,是不是業(yè)務(wù)就不執(zhí)行了缅叠?
答:是的,對(duì)于基于消息最終一致性的方案虏冻,一般都會(huì)強(qiáng)依賴(lài)這步肤粱,如果這個(gè)步驟無(wú)法得到保證,那么最終也 就不可能做到最終一致性了厨相。
-
問(wèn):為什么要增加一個(gè)消息
預(yù)發(fā)送
機(jī)制领曼,增加兩次發(fā)布出去消息的重試機(jī)制,為什么不在業(yè)務(wù)成功之后蛮穿,發(fā)送失敗的話(huà)使用一次重試機(jī)制庶骄?答:如果業(yè)務(wù)執(zhí)行成功,再去發(fā)消息践磅,此時(shí)如果還沒(méi)來(lái)得及發(fā)消息单刁,業(yè)務(wù)系統(tǒng)就已經(jīng)宕機(jī)了,系統(tǒng)重啟后府适,根本沒(méi)有記錄之前是否發(fā)送過(guò)消息羔飞,這樣就會(huì)導(dǎo)致業(yè)務(wù)執(zhí)行成功肺樟,消息最終沒(méi)發(fā)出去的情況。
-
如果consumer消費(fèi)失敗逻淌,是否需要producer做回滾呢么伯?
答:這里的事務(wù)消息,producer不會(huì)因?yàn)閏onsumer消費(fèi)失敗而做回滾卡儒,采用事務(wù)消息的應(yīng)用蹦狂,其所追求的是高可用和最終一致性,消息消費(fèi)失敗的話(huà)朋贬,MQ自己會(huì)負(fù)責(zé)重推消息凯楔,直到消費(fèi)成功。因此锦募,事務(wù)消息是針對(duì)生產(chǎn)端而言的摆屯,而消費(fèi)端,消費(fèi)端的一致性是通過(guò)MQ的重試機(jī)制來(lái)完成的糠亩。
-
如果consumer端因?yàn)?strong>業(yè)務(wù)異常而導(dǎo)致回滾虐骑,那么豈不是兩邊最終無(wú)法保證一致性?
答:基于消息的最終一致性方案必須保證消費(fèi)端在業(yè)務(wù)上的操作沒(méi)障礙,它只允許系統(tǒng)異常的失敗赎线,不允許業(yè)務(wù)上的失敗廷没,比如在你業(yè)務(wù)上拋出個(gè)NPE之類(lèi)的問(wèn)題,導(dǎo)致你消費(fèi)端執(zhí)行事務(wù)失敗垂寥,那就很難做到一致了颠黎。
由于并非所有的MQ都支持事務(wù)消息,假如我們不選擇RocketMQ來(lái)作為系統(tǒng)的MQ滞项,是否能夠做到消息的最終一致性呢狭归?答案是可以的。
基于本地消息的最終一致性
基于本地消息的最終一致性
方案的最核心做法就是在執(zhí)行業(yè)務(wù)操作的時(shí)候文判,記錄一條消息數(shù)據(jù)到DB过椎,并且消息數(shù)據(jù)的記錄與業(yè)務(wù)數(shù)據(jù)的記錄必須在同一個(gè)事務(wù)內(nèi)完成,這是該方案的前提核心保障戏仓。在記錄完成后消息數(shù)據(jù)后疚宇,后面我們就可以通過(guò)一個(gè)定時(shí)任務(wù)到DB中去輪訓(xùn)狀態(tài)為待發(fā)送
的消息,然后將消息投遞給MQ赏殃。這個(gè)過(guò)程中可能存在消息投遞失敗的可能敷待,此時(shí)就依靠重試機(jī)制
來(lái)保證,直到成功收到MQ的ACK確認(rèn)之后嗓奢,再將消息狀態(tài)更新或者消息清除讼撒;而后面消息的消費(fèi)失敗的話(huà),則依賴(lài)MQ本身的重試來(lái)完成股耽,其最后做到兩邊系統(tǒng)數(shù)據(jù)的最終一致性根盒。基于本地消息服務(wù)
的方案雖然可以做到消息的最終一致性,但是它有一個(gè)比較嚴(yán)重的弊端物蝙,每個(gè)業(yè)務(wù)系統(tǒng)在使用該方案時(shí)炎滞,都需要在對(duì)應(yīng)的業(yè)務(wù)庫(kù)創(chuàng)建一張消息表來(lái)存儲(chǔ)消息。針對(duì)這個(gè)問(wèn)題诬乞,我們可以將該功能單獨(dú)提取出來(lái)册赛,做成一個(gè)消息服務(wù)來(lái)統(tǒng)一處理,因而就衍生出了我們下面將要討論的方案震嫉。
獨(dú)立消息服務(wù)的最終一致性
獨(dú)立消息服務(wù)最終一致性
與本地消息服務(wù)最終一致性
最大的差異就在于將消息的存儲(chǔ)單獨(dú)地做成了一個(gè)RPC的服務(wù)森瘪,這個(gè)過(guò)程其實(shí)就是模擬了事務(wù)消息的消息預(yù)發(fā)送過(guò)程,如果預(yù)發(fā)送消息失敗票堵,那么生產(chǎn)者業(yè)務(wù)就不會(huì)去執(zhí)行扼睬,因此對(duì)于生產(chǎn)者的業(yè)務(wù)而言,它是強(qiáng)依賴(lài)于該消息服務(wù)的悴势。不過(guò)好在獨(dú)立消息服務(wù)支持水平擴(kuò)容窗宇,因此只要部署多臺(tái),做成HA的集群模式特纤,就能夠保證其可靠性军俊。在消息服務(wù)中,還有一個(gè)單獨(dú)地定時(shí)任務(wù)捧存,它會(huì)定期輪訓(xùn)長(zhǎng)時(shí)間處于待發(fā)送
狀態(tài)的消息粪躬,通過(guò)一個(gè)check補(bǔ)償機(jī)制來(lái)確認(rèn)該消息對(duì)應(yīng)的業(yè)務(wù)是否成功,如果對(duì)應(yīng)的業(yè)務(wù)處理成功昔穴,則將消息修改為可發(fā)送
短蜕,然后將其投遞給MQ;如果業(yè)務(wù)處理失敗傻咖,則將對(duì)應(yīng)的消息更新或者刪除即可朋魔。因此在使用該方案時(shí),消息生產(chǎn)者必須同時(shí)實(shí)現(xiàn)一個(gè)check服務(wù)卿操,來(lái)供消息服務(wù)做消息的確認(rèn)警检。對(duì)于消息的消費(fèi),該方案與上面的處理是一樣害淤,都是通過(guò)MQ自身的重發(fā)機(jī)制來(lái)保證消息被消費(fèi)扇雕。
總結(jié):上游事務(wù)提交之后,在基于MQ的場(chǎng)景下就不考慮回滾了窥摄。失敗的可能是由于網(wǎng)絡(luò)镶奉、服務(wù)宕機(jī)所導(dǎo)致,文章中提到說(shuō)業(yè)務(wù)上執(zhí)行是無(wú)障礙的。如果下游服務(wù)長(zhǎng)時(shí)間沒(méi)有恢復(fù)哨苛,那么就應(yīng)該設(shè)置告警鸽凶,在這里有幾種機(jī)制來(lái)解決一些牛皮癬類(lèi)型的問(wèn)題,假如上游消息始終發(fā)送失斀ㄇ汀(這種可能性基本不存在除非代碼是假的)這種情況我們可以設(shè)置報(bào)警機(jī)制比如發(fā)生異常時(shí)可以打印日志玻侥,發(fā)送短信,發(fā)送郵件亿蒸,將異常訂單保存到數(shù)據(jù)庫(kù)凑兰,這些措施可以同時(shí)用于下游一些異常訂單,同時(shí)也可以在發(fā)生異常的時(shí)候新建一個(gè)異常Topic的消息提示边锁,讓人工來(lái)介入數(shù)據(jù)訂正姑食。
如果本篇文章對(duì)你有幫助的話(huà)請(qǐng)點(diǎn)個(gè)贊加關(guān)注吧