http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
在OLTP系統(tǒng)領(lǐng)域,我們?cè)诤芏鄻I(yè)務(wù)場(chǎng)景下都會(huì)面臨事務(wù)一致性方面的需求堕伪,例如最經(jīng)典的Bob給Smith轉(zhuǎn)賬的案例界斜。傳統(tǒng)的企業(yè)開(kāi)發(fā),系統(tǒng)往往是以單體應(yīng)用形式存在的,也沒(méi)有橫跨多個(gè)數(shù)據(jù)庫(kù)祭芦。我們通常只需借助開(kāi)發(fā)平臺(tái)中特有數(shù)據(jù)訪問(wèn)技術(shù)和框架(例如Spring、JDBC、ADO.NET)蒂窒,結(jié)合關(guān)系型數(shù)據(jù)庫(kù)自帶的事務(wù)管理機(jī)制來(lái)實(shí)現(xiàn)事務(wù)性的需求。關(guān)系型數(shù)據(jù)庫(kù)通常具有ACID特性:原子性(Atomicity)荞怒、一致性(Consistency)洒琢、隔離性(Isolation)、持久性(Durability)褐桌。
而大型互聯(lián)網(wǎng)平臺(tái)往往是由一系列分布式系統(tǒng)構(gòu)成的衰抑,開(kāi)發(fā)語(yǔ)言平臺(tái)和技術(shù)棧也相對(duì)比較雜,尤其是在SOA和微服務(wù)架構(gòu)盛行的今天荧嵌,一個(gè)看起來(lái)簡(jiǎn)單的功能呛踊,內(nèi)部可能需要調(diào)用多個(gè)“服務(wù)”并操作多個(gè)數(shù)據(jù)庫(kù)或分片來(lái)實(shí)現(xiàn),情況往往會(huì)復(fù)雜很多完丽。單一的技術(shù)手段和解決方案恋技,已經(jīng)無(wú)法應(yīng)對(duì)和滿足這些復(fù)雜的場(chǎng)景了。
分布式系統(tǒng)的特性
對(duì)分布式系統(tǒng)有過(guò)研究的讀者逻族,可能聽(tīng)說(shuō)過(guò)“CAP定律”蜻底、“Base理論”等,非常巧的是聘鳞,化學(xué)理論中ACID是酸薄辅、Base恰好是堿。這里筆者不對(duì)這些概念做過(guò)多的解釋抠璃,有興趣的讀者可以查看相關(guān)參考資料站楚。CAP定律如下圖:
在分布式系統(tǒng)中,同時(shí)滿足“CAP定律”中的“一致性”搏嗡、“可用性”和“分區(qū)容錯(cuò)性”三者是不可能的窿春,這比現(xiàn)實(shí)中找對(duì)象需同時(shí)滿足“高、富采盒、帥”或“白旧乞、富、美”更加困難磅氨。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場(chǎng)景尺栖,都需要犧牲強(qiáng)一致性來(lái)?yè)Q取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”烦租,只要這個(gè)最終時(shí)間是在用戶可以接受的范圍內(nèi)即可延赌。
分布式事務(wù)
提到分布式系統(tǒng)除盏,必然要提到分布式事務(wù)。要想理解分布式事務(wù)挫以,不得不先介紹一下兩階段提交協(xié)議者蠕。先舉個(gè)簡(jiǎn)單但不精準(zhǔn)的例子來(lái)說(shuō)明:
第一階段,張老師作為“協(xié)調(diào)者”屡贺,給小強(qiáng)和小明(參與者蠢棱、節(jié)點(diǎn))發(fā)微信,組織他們倆明天8點(diǎn)在學(xué)校門(mén)口集合甩栈,一起去爬山泻仙,然后開(kāi)始等待小強(qiáng)和小明答復(fù)。
第二階段量没,如果小強(qiáng)和小明都回答沒(méi)問(wèn)題玉转,那么大家如約而至。如果小強(qiáng)或者小明其中一人回答說(shuō)“明天沒(méi)空殴蹄,不行”究抓,那么張老師會(huì)立即通知小強(qiáng)和小明“爬山活動(dòng)取消”。
細(xì)心的讀者會(huì)發(fā)現(xiàn)袭灯,這個(gè)過(guò)程中可能有很多問(wèn)題的刺下。如果小強(qiáng)沒(méi)看手機(jī),那么張老師會(huì)一直等著答復(fù)稽荧,小明可能在家里把爬山裝備都準(zhǔn)備好了卻一直等著張老師確認(rèn)信息橘茉。更嚴(yán)重的是,如果到明天8點(diǎn)小強(qiáng)還沒(méi)有答復(fù)姨丈,那么就算“超時(shí)”了畅卓,那小明到底去還是不去集合爬山呢?
這就是兩階段提交協(xié)議的弊病蟋恬,所以后來(lái)業(yè)界又引入了三階段提交協(xié)議來(lái)解決該類問(wèn)題翁潘。
兩階段提交協(xié)議在主流開(kāi)發(fā)語(yǔ)言平臺(tái),數(shù)據(jù)庫(kù)產(chǎn)品中都有廣泛應(yīng)用和實(shí)現(xiàn)的歼争,下面來(lái)介紹一下XOpen組織提供的DTP模型圖:
XA協(xié)議指的是TM(事務(wù)管理器)和RM(資源管理器)之間的接口拜马。目前主流的關(guān)系型數(shù)據(jù)庫(kù)產(chǎn)品都是實(shí)現(xiàn)了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的沐绒,事務(wù)管理器和資源管理器之間也使用了XA協(xié)議俩莽。 本質(zhì)上也是借助兩階段提交協(xié)議來(lái)實(shí)現(xiàn)分布式事務(wù)的,下面分別來(lái)看看XA事務(wù)成功和失敗的模型圖:
在JavaEE平臺(tái)下洒沦,WebLogic豹绪、Webshare等主流商用的應(yīng)用服務(wù)器提供了JTA的實(shí)現(xiàn)和支持价淌。而在Tomcat下是沒(méi)有實(shí)現(xiàn)的(其實(shí)筆者并不認(rèn)為T(mén)omcat能算是JavaEE應(yīng)用服務(wù)器)申眼,這就需要借助第三方的框架Jotm****瞒津、Automikos等來(lái)實(shí)現(xiàn),兩者均支持spring事務(wù)整合括尸。
而在Windows .NET平臺(tái)中巷蚪,則可以借助ado.net中的*TransactionScop *API來(lái)編程實(shí)現(xiàn),還必須配置和借助Windows操作系統(tǒng)中的MSDTC服務(wù)濒翻。如果你的數(shù)據(jù)庫(kù)使用的mysql屁柏,并且mysql是部署在Linux平臺(tái)上的,那么是無(wú)法支持分布式事務(wù)的有送。 由于篇幅關(guān)系淌喻,這里不展開(kāi),感興趣的讀者可以自行查閱相關(guān)資料并實(shí)踐雀摘。
總結(jié):這種方式實(shí)現(xiàn)難度不算太高裸删,比較適合傳統(tǒng)的單體應(yīng)用,在同一個(gè)方法中存在跨庫(kù)操作的情況阵赠。但分布式事務(wù)對(duì)性能的影響會(huì)比較大涯塔,不適合高并發(fā)和高性能要求的場(chǎng)景。
提供回滾接口
在服務(wù)化架構(gòu)中清蚀,功能X匕荸,需要去協(xié)調(diào)后端的A、B甚至更多的原子服務(wù)枷邪。那么問(wèn)題來(lái)了榛搔,假如A和B其中一個(gè)調(diào)用失敗了,那可怎么辦呢齿风?
在筆者的工作中經(jīng)常遇到這類問(wèn)題药薯,往往提供了一個(gè)BFF層來(lái)協(xié)調(diào)調(diào)用A、B服務(wù)救斑。如果有些是需要同步返回結(jié)果的童本,我會(huì)盡量按照“串行”的方式去調(diào)用。如果調(diào)用A失敗脸候,則不會(huì)盲目去調(diào)用B穷娱。如果調(diào)用A成功,而調(diào)用B失敗运沦,會(huì)嘗試去回滾剛剛對(duì)A的調(diào)用操作泵额。
當(dāng)然,有些時(shí)候我們不必嚴(yán)格提供單獨(dú)對(duì)應(yīng)的回滾接口携添,可以通過(guò)傳遞參數(shù)巧妙的實(shí)現(xiàn)嫁盲。
這樣的情況,我們會(huì)盡量把可提供回滾接口的服務(wù)放在前面烈掠。舉個(gè)例子說(shuō)明:
我們的某個(gè)論壇網(wǎng)站羞秤,每天登錄成功后會(huì)獎(jiǎng)勵(lì)用戶5個(gè)積分缸托,但是積分和用戶又是兩套獨(dú)立的子系統(tǒng)服務(wù),對(duì)應(yīng)不同的DB瘾蛋,這控制起來(lái)就比較麻煩了俐镐。解決思路:
把登錄和加積分的服務(wù)調(diào)用放在BFF層一個(gè)本地方法中。
當(dāng)用戶請(qǐng)求登錄接口時(shí)哺哼,先執(zhí)行加積分操作佩抹,加分成功后再執(zhí)行登錄操作
如果登錄成功,那當(dāng)然最好了取董,積分也加成功了棍苹。如果登錄失敗,則調(diào)用加積分對(duì)應(yīng)的回滾接口(執(zhí)行減積分的操作)茵汰。
總結(jié):這種方式缺點(diǎn)比較多廊勃,通常在復(fù)雜場(chǎng)景下是不推薦使用的,除非是非常簡(jiǎn)單的場(chǎng)景经窖,非常容易提供回滾坡垫,而且依賴的服務(wù)也非常少的情況。
這種實(shí)現(xiàn)方式會(huì)造成代碼量龐大画侣,耦合性高冰悠。而且非常有局限性,因?yàn)橛泻芏嗟臉I(yè)務(wù)是無(wú)法很簡(jiǎn)單的實(shí)現(xiàn)回滾的配乱,如果串行的服務(wù)很多,回滾的成本實(shí)在太高搬泥。
本地消息表
這種實(shí)現(xiàn)方式的思路桑寨,其實(shí)是源于ebay,后來(lái)通過(guò)支付寶等公司的布道忿檩,在業(yè)內(nèi)廣泛使用尉尾。其基本的設(shè)計(jì)思想是將遠(yuǎn)程分布式事務(wù)拆分成一系列的本地事務(wù)。如果不考慮性能及設(shè)計(jì)優(yōu)雅燥透,借助關(guān)系型數(shù)據(jù)庫(kù)中的表即可實(shí)現(xiàn)沙咏。
舉個(gè)經(jīng)典的跨行轉(zhuǎn)賬的例子來(lái)描述。
第一步偽代碼如下班套,扣款1W肢藐,通過(guò)本地事務(wù)保證了憑證消息插入到消息表中。
第二步吱韭,通知對(duì)方銀行賬戶上加1W了吆豹。那問(wèn)題來(lái)了,如何通知到對(duì)方呢?
通常采用兩種方式:
采用時(shí)效性高的MQ痘煤,由對(duì)方訂閱消息并監(jiān)聽(tīng)鸳吸,有消息時(shí)自動(dòng)觸發(fā)事件
采用定時(shí)輪詢掃描的方式,去檢查消息表的數(shù)據(jù)速勇。
兩種方式其實(shí)各有利弊,僅僅依靠MQ坎拐,可能會(huì)出現(xiàn)通知失敗的問(wèn)題烦磁。而過(guò)于頻繁的定時(shí)輪詢,效率也不是最佳的(90%是無(wú)用功)哼勇。所以都伪,我們一般會(huì)把兩種方式結(jié)合起來(lái)使用。
解決了通知的問(wèn)題积担,又有新的問(wèn)題了陨晶。萬(wàn)一這消息有重復(fù)被消費(fèi),往用戶帳號(hào)上多加了錢(qián)帝璧,那豈不是后果很嚴(yán)重先誉?
仔細(xì)思考,其實(shí)我們可以消息消費(fèi)方的烁,也通過(guò)一個(gè)“消費(fèi)狀態(tài)表”來(lái)記錄消費(fèi)狀態(tài)褐耳。在執(zhí)行“加款”操作之前,檢測(cè)下該消息(提供標(biāo)識(shí))是否已經(jīng)消費(fèi)過(guò)渴庆,消費(fèi)完成后铃芦,通過(guò)本地事務(wù)控制來(lái)更新這個(gè)“消費(fèi)狀態(tài)表”。這樣子就避免重復(fù)消費(fèi)的問(wèn)題襟雷。
總結(jié):上訴的方式是一種非常經(jīng)典的實(shí)現(xiàn)刃滓,基本避免了分布式事務(wù),實(shí)現(xiàn)了“最終一致性”耸弄。但是咧虎,關(guān)系型數(shù)據(jù)庫(kù)的吞吐量和性能方面存在瓶頸,頻繁的讀寫(xiě)消息會(huì)給數(shù)據(jù)庫(kù)造成壓力计呈。所以老客,在真正的高并發(fā)場(chǎng)景下,該方案也會(huì)有瓶頸和限制的震叮。
MQ(非事務(wù)消息)
通常情況下胧砰,在使用非事務(wù)消息支持的MQ產(chǎn)品時(shí),我們很難將業(yè)務(wù)操作與對(duì)MQ的操作放在一個(gè)本地事務(wù)域中管理苇瓣。通俗點(diǎn)描述尉间,還是以上述提到的“跨行轉(zhuǎn)賬”為例,我們很難保證在扣款完成之后對(duì)MQ投遞消息的操作就一定能成功。這樣一致性似乎很難保證哲嘲。
先從消息生產(chǎn)者這端來(lái)分析贪薪,請(qǐng)看偽代碼:
根據(jù)上述代碼及注釋,我們來(lái)分析下可能的情況:
操作數(shù)據(jù)庫(kù)成功眠副,向MQ中投遞消息也成功画切,皆大歡喜
操作數(shù)據(jù)庫(kù)失敗,不會(huì)向MQ中投遞消息了
操作數(shù)據(jù)庫(kù)成功囱怕,但是向MQ中投遞消息時(shí)失敗霍弹,向外拋出了異常,剛剛執(zhí)行的更新數(shù)據(jù)庫(kù)的操作將被回滾
從上面分析的幾種情況來(lái)看娃弓,貌似問(wèn)題都不大的典格。那么我們來(lái)分析下消費(fèi)者端面臨的問(wèn)題:
消息出列后,消費(fèi)者對(duì)應(yīng)的業(yè)務(wù)操作要執(zhí)行成功台丛。如果業(yè)務(wù)執(zhí)行失敗耍缴,消息不能失效或者丟失。需要保證消息與業(yè)務(wù)操作一致
盡量避免消息重復(fù)消費(fèi)挽霉。如果重復(fù)消費(fèi)防嗡,也不能因此影響業(yè)務(wù)結(jié)果
如何保證消息與業(yè)務(wù)操作一致,不丟失侠坎?
主流的MQ產(chǎn)品都具有持久化消息的功能本鸣。如果消費(fèi)者宕機(jī)或者消費(fèi)失敗,都可以執(zhí)行重試機(jī)制的(有些MQ可以自定義重試次數(shù))硅蹦。
如何避免消息被重復(fù)消費(fèi)造成的問(wèn)題荣德?
保證消費(fèi)者調(diào)用業(yè)務(wù)的服務(wù)接口的冪等性
通過(guò)消費(fèi)日志或者類似狀態(tài)表來(lái)記錄消費(fèi)狀態(tài),便于判斷(建議在業(yè)務(wù)上自行實(shí)現(xiàn)童芹,而不依賴MQ產(chǎn)品提供該特性)
總結(jié):這種方式比較常見(jiàn)涮瞻,性能和吞吐量是優(yōu)于使用關(guān)系型數(shù)據(jù)庫(kù)消息表的方案。如果MQ****自身和業(yè)務(wù)都具有高可用性假褪,理論上是可以滿足大部分的業(yè)務(wù)場(chǎng)景的署咽。不過(guò)在沒(méi)有充分測(cè)試的情況下,不建議在交易業(yè)務(wù)中直接使用生音。
MQ(事務(wù)消息)
舉個(gè)例子宁否,Bob向Smith轉(zhuǎn)賬,那我們到底是先發(fā)送消息缀遍,還是先執(zhí)行扣款操作慕匠?
好像都可能會(huì)出問(wèn)題。如果先發(fā)消息域醇,扣款操作失敗台谊,那么Smith的賬戶里面會(huì)多出一筆錢(qián)蓉媳。反過(guò)來(lái),如果先執(zhí)行扣款操作锅铅,后發(fā)送消息酪呻,那有可能扣款成功了但是消息沒(méi)發(fā)出去,Smith收不到錢(qián)盐须。除了上面介紹的通過(guò)異常捕獲和回滾的方式外玩荠,還有沒(méi)有其他的思路呢?
下面以阿里巴巴的RocketMQ中間件為例贼邓,分析下其設(shè)計(jì)和實(shí)現(xiàn)思路阶冈。
RocketMQ第一階段發(fā)送Prepared消息時(shí),會(huì)拿到消息的地址立帖,第二階段執(zhí)行本地事物,第三階段通過(guò)第一階段拿到的地址去訪問(wèn)消息悠砚,并修改狀態(tài)晓勇。細(xì)心的讀者可能又發(fā)現(xiàn)問(wèn)題了,如果確認(rèn)消息發(fā)送失敗了怎么辦灌旧?RocketMQ會(huì)定期掃描消息集群中的事物消息绑咱,這時(shí)候發(fā)現(xiàn)了Prepared消息,它會(huì)向消息發(fā)送者確認(rèn)枢泰,Bob的錢(qián)到底是減了還是沒(méi)減呢描融?如果減了是回滾還是繼續(xù)發(fā)送確認(rèn)消息呢?RocketMQ會(huì)根據(jù)發(fā)送端設(shè)置的策略來(lái)決定是回滾還是繼續(xù)發(fā)送確認(rèn)消息衡蚂。這樣就保證了消息發(fā)送與本地事務(wù)同時(shí)成功或同時(shí)失敗窿克。如下圖:
總結(jié):據(jù)筆者的了解,各大知名的電商平臺(tái)和互聯(lián)網(wǎng)公司毛甲,幾乎都是采用類似的設(shè)計(jì)思路來(lái)實(shí)現(xiàn)“最終一致性”的年叮。這種方式適合的業(yè)務(wù)場(chǎng)景廣泛,而且比較可靠玻募。不過(guò)這種方式技術(shù)實(shí)現(xiàn)的難度比較大只损。目前主流的開(kāi)源MQ(ActiveMQ、RabbitMQ七咧、Kafka)均未實(shí)現(xiàn)對(duì)事務(wù)消息的支持跃惫,所以需二次開(kāi)發(fā)或者新造輪子。比較遺憾的是艾栋,RocketMQ事務(wù)消息部分的代碼也并未開(kāi)源爆存,需要自己去實(shí)現(xiàn)。
其他補(bǔ)償方式
做過(guò)支付寶交易接口的同學(xué)都知道蝗砾,我們一般會(huì)在支付寶的回調(diào)頁(yè)面和接口里终蒂,解密參數(shù)蜂林,然后調(diào)用系統(tǒng)中更新交易狀態(tài)相關(guān)的服務(wù),將訂單更新為付款成功拇泣。同時(shí)噪叙,只有當(dāng)我們回調(diào)頁(yè)面中輸出了success字樣或者標(biāo)識(shí)業(yè)務(wù)處理成功相應(yīng)狀態(tài)碼時(shí),支付寶才會(huì)停止回調(diào)請(qǐng)求霉翔。否則睁蕾,支付寶會(huì)每間隔一段時(shí)間后,再向客戶方發(fā)起回調(diào)請(qǐng)求债朵,直到輸出成功標(biāo)識(shí)為止子眶。
其實(shí)這就是一個(gè)很典型的補(bǔ)償例子,跟一些MQ重試補(bǔ)償機(jī)制很類似序芦。
一般成熟的系統(tǒng)中臭杰,對(duì)于級(jí)別較高的服務(wù)和接口,整體的可用性通常都會(huì)很高谚中。如果有些業(yè)務(wù)由于瞬時(shí)的網(wǎng)絡(luò)故障或調(diào)用超時(shí)等問(wèn)題渴杆,那么這種重試機(jī)制其實(shí)是非常有效的。
當(dāng)然宪塔,考慮個(gè)比較極端的場(chǎng)景磁奖,假如系統(tǒng)自身有bug或者程序邏輯有問(wèn)題,那么重試1W次那也是無(wú)濟(jì)于事的某筐。那豈不是就發(fā)生了“明明已經(jīng)付款比搭,卻顯示未付款不發(fā)貨”類似的悲劇南誊?
其實(shí)為了交易系統(tǒng)更可靠身诺,我們一般會(huì)在類似交易這種高級(jí)別的服務(wù)代碼中,加入詳細(xì)日志記錄的抄囚,一旦系統(tǒng)內(nèi)部引發(fā)類似致命異常戚长,會(huì)有郵件通知。同時(shí)怠苔,后臺(tái)會(huì)有定時(shí)任務(wù)掃描和分析此類日志同廉,檢查出這種特殊的情況,會(huì)嘗試通過(guò)程序來(lái)補(bǔ)償并郵件通知相關(guān)人員柑司。
在某些特殊的情況下迫肖,還會(huì)有“人工補(bǔ)償”的,這也是最后一道屏障攒驰。
小結(jié)
上訴的幾種方案中蟆湖,筆者也大致總結(jié)了其設(shè)計(jì)思路,優(yōu)勢(shì)玻粪,劣勢(shì)等隅津,相信讀者已經(jīng)有了一定的理解诬垂。其實(shí)分布式系統(tǒng)的事務(wù)一致性本身是一個(gè)技術(shù)難題,目前沒(méi)有一種很簡(jiǎn)單很完美的方案能夠應(yīng)對(duì)所有場(chǎng)景伦仍。具體還是要使用者根據(jù)不同的業(yè)務(wù)場(chǎng)景去抉擇结窘。