轉(zhuǎn)載:http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
開篇
在OLTP系統(tǒng)領(lǐng)域拨齐,我們在很多業(yè)務場景下都會面臨事務一致性方面的需求磺樱,例如最經(jīng)典的Bob給Smith轉(zhuǎn)賬的案例朴乖。傳統(tǒng)的企業(yè)開發(fā)团搞,系統(tǒng)往往是以單體應用形式存在的,也沒有橫跨多個數(shù)據(jù)庫风宁。我們通常只需借助開發(fā)平臺中特有數(shù)據(jù)訪問技術(shù)和框架(例如Spring翁潘、JDBC癌别、ADO.NET),結(jié)合關(guān)系型數(shù)據(jù)庫自帶的事務管理機制來實現(xiàn)事務性的需求铃慷。關(guān)系型數(shù)據(jù)庫通常具有ACID特性:原子性(Atomicity)单芜、一致性(Consistency)、隔離性(Isolation)犁柜、持久性(Durability)洲鸠。
而大型互聯(lián)網(wǎng)平臺往往是由一系列分布式系統(tǒng)構(gòu)成的,開發(fā)語言平臺和技術(shù)棧也相對比較雜馋缅,尤其是在SOA和微服務架構(gòu)盛行的今天坛怪,一個看起來簡單的功能,內(nèi)部可能需要調(diào)用多個“服務”并操作多個數(shù)據(jù)庫或分片來實現(xiàn)股囊,情況往往會復雜很多袜匿。單一的技術(shù)手段和解決方案,已經(jīng)無法應對和滿足這些復雜的場景了稚疹。
分布式系統(tǒng)的特性
對分布式系統(tǒng)有過研究的讀者居灯,可能聽說過“CAP定律”、“Base理論”等内狗,非常巧的是怪嫌,化學理論中ACID是酸、Base恰好是堿柳沙。這里筆者不對這些概念做過多的解釋岩灭,有興趣的讀者可以查看相關(guān)參考資料。CAP定律如下圖:
在分布式系統(tǒng)中赂鲤,同時滿足“CAP定律”中的“一致性”噪径、“可用性”和“分區(qū)容錯性”三者是不可能的柱恤,這比現(xiàn)實中找對象需同時滿足“高、富找爱、帥”或“白梗顺、富、美”更加困難车摄。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景寺谤,都需要犧牲強一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”吮播,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可变屁。
分布式事務
提到分布式系統(tǒng),必然要提到分布式事務意狠。要想理解分布式事務敞贡,不得不先介紹一下兩階段提交協(xié)議。先舉個簡單但不精準的例子來說明:
相關(guān)廠商內(nèi)容
超級App的實時性能監(jiān)控與性能優(yōu)化實踐
微信圖片視頻背后的EB級存儲引擎設計
漏斗模型:京東物流系統(tǒng)高并發(fā)架構(gòu)演進之路
如何開發(fā)一個商業(yè)智能推薦系統(tǒng)摄职?
2017年誊役,你應該關(guān)注這些運維技術(shù)熱點
第一階段,張老師作為“協(xié)調(diào)者”迫悠,給小強和小明(參與者鹏漆、節(jié)點)發(fā)微信,組織他們倆明天8點在學校門口集合创泄,一起去爬山艺玲,然后開始等待小強和小明答復。
第二階段鞠抑,如果小強和小明都回答沒問題饭聚,那么大家如約而至。如果小強或者小明其中一人回答說“明天沒空搁拙,不行”秒梳,那么張老師會立即通知小強和小明“爬山活動取消”。
細心的讀者會發(fā)現(xiàn)箕速,這個過程中可能有很多問題的酪碘。如果小強沒看手機,那么張老師會一直等著答復盐茎,小明可能在家里把爬山裝備都準備好了卻一直等著張老師確認信息兴垦。更嚴重的是,如果到明天8點小強還沒有答復,那么就算“超時”了探越,那小明到底去還是不去集合爬山呢狡赐?
這就是兩階段提交協(xié)議的弊病,所以后來業(yè)界又引入了三階段提交協(xié)議來解決該類問題扶关。
兩階段提交協(xié)議在主流開發(fā)語言平臺,數(shù)據(jù)庫產(chǎn)品中都有廣泛應用和實現(xiàn)的数冬,下面來介紹一下XOpen組織提供的DTP模型圖:
XA協(xié)議指的是TM(事務管理器)和RM(資源管理器)之間的接口节槐。目前主流的關(guān)系型數(shù)據(jù)庫產(chǎn)品都是實現(xiàn)了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的拐纱,事務管理器和資源管理器之間也使用了XA協(xié)議铜异。 本質(zhì)上也是借助兩階段提交協(xié)議來實現(xiàn)分布式事務的,下面分別來看看XA事務成功和失敗的模型圖:
在JavaEE平臺下秸架,WebLogic揍庄、Webshare等主流商用的應用服務器提供了JTA的實現(xiàn)和支持。而在Tomcat下是沒有實現(xiàn)的(其實筆者并不認為Tomcat能算是JavaEE應用服務器)东抹,這就需要借助第三方的框架Jotm****蚂子、Automikos等來實現(xiàn),兩者均支持spring事務整合缭黔。
而在Windows .NET平臺中食茎,則可以借助ado.net中的*TransactionScop *API來編程實現(xiàn),還必須配置和借助Windows操作系統(tǒng)中的MSDTC服務馏谨。如果你的數(shù)據(jù)庫使用的mysql别渔,并且mysql是部署在Linux平臺上的,那么是無法支持分布式事務的惧互。 由于篇幅關(guān)系哎媚,這里不展開,感興趣的讀者可以自行查閱相關(guān)資料并實踐喊儡。
總結(jié):這種方式實現(xiàn)難度不算太高拨与,比較適合傳統(tǒng)的單體應用,在同一個方法中存在跨庫操作的情況艾猜。但分布式事務對性能的影響會比較大截珍,不適合高并發(fā)和高性能要求的場景。
提供回滾接口
在服務化架構(gòu)中箩朴,功能X岗喉,需要去協(xié)調(diào)后端的A、B甚至更多的原子服務炸庞。那么問題來了钱床,假如A和B其中一個調(diào)用失敗了,那可怎么辦呢埠居?
在筆者的工作中經(jīng)常遇到這類問題查牌,往往提供了一個BFF層來協(xié)調(diào)調(diào)用A事期、B服務。如果有些是需要同步返回結(jié)果的纸颜,我會盡量按照“串行”的方式去調(diào)用兽泣。如果調(diào)用A失敗,則不會盲目去調(diào)用B胁孙。如果調(diào)用A成功唠倦,而調(diào)用B失敗,會嘗試去回滾剛剛對A的調(diào)用操作涮较。
當然稠鼻,有些時候我們不必嚴格提供單獨對應的回滾接口,可以通過傳遞參數(shù)巧妙的實現(xiàn)狂票。
這樣的情況候齿,我們會盡量把可提供回滾接口的服務放在前面。舉個例子說明:
我們的某個論壇網(wǎng)站闺属,每天登錄成功后會獎勵用戶5個積分慌盯,但是積分和用戶又是兩套獨立的子系統(tǒng)服務,對應不同的DB掂器,這控制起來就比較麻煩了润匙。解決思路:
把登錄和加積分的服務調(diào)用放在BFF層一個本地方法中。
當用戶請求登錄接口時唉匾,先執(zhí)行加積分操作孕讳,加分成功后再執(zhí)行登錄操作
如果登錄成功,那當然最好了巍膘,積分也加成功了厂财。如果登錄失敗,則調(diào)用加積分對應的回滾接口(執(zhí)行減積分的操作)峡懈。
總結(jié):這種方式缺點比較多璃饱,通常在復雜場景下是不推薦使用的,除非是非常簡單的場景肪康,非常容易提供回滾荚恶,而且依賴的服務也非常少的情況。
這種實現(xiàn)方式會造成代碼量龐大磷支,耦合性高谒撼。而且非常有局限性,因為有很多的業(yè)務是無法很簡單的實現(xiàn)回滾的雾狈,如果串行的服務很多廓潜,回滾的成本實在太高。
本地消息表
這種實現(xiàn)方式的思路,其實是源于ebay辩蛋,后來通過支付寶等公司的布道呻畸,在業(yè)內(nèi)廣泛使用。其基本的設計思想是將遠程分布式事務拆分成一系列的本地事務悼院。如果不考慮性能及設計優(yōu)雅伤为,借助關(guān)系型數(shù)據(jù)庫中的表即可實現(xiàn)。
舉個經(jīng)典的跨行轉(zhuǎn)賬的例子來描述据途。
第一步偽代碼如下绞愚,扣款1W,通過本地事務保證了憑證消息插入到消息表中昨凡。
第二步爽醋,通知對方銀行賬戶上加1W了蚁署。那問題來了便脊,如何通知到對方呢?
通常采用兩種方式:
采用時效性高的MQ光戈,由對方訂閱消息并監(jiān)聽哪痰,有消息時自動觸發(fā)事件
采用定時輪詢掃描的方式,去檢查消息表的數(shù)據(jù)久妆。
兩種方式其實各有利弊晌杰,僅僅依靠MQ,可能會出現(xiàn)通知失敗的問題筷弦。而過于頻繁的定時輪詢肋演,效率也不是最佳的(90%是無用功)。所以烂琴,我們一般會把兩種方式結(jié)合起來使用爹殊。
解決了通知的問題,又有新的問題了奸绷。萬一這消息有重復被消費梗夸,往用戶帳號上多加了錢,那豈不是后果很嚴重号醉?
仔細思考反症,其實我們可以消息消費方,也通過一個“消費狀態(tài)表”來記錄消費狀態(tài)畔派。在執(zhí)行“加款”操作之前铅碍,檢測下該消息(提供標識)是否已經(jīng)消費過,消費完成后线椰,通過本地事務控制來更新這個“消費狀態(tài)表”该酗。這樣子就避免重復消費的問題。
總結(jié):上訴的方式是一種非常經(jīng)典的實現(xiàn),基本避免了分布式事務呜魄,實現(xiàn)了“最終一致性”悔叽。但是,關(guān)系型數(shù)據(jù)庫的吞吐量和性能方面存在瓶頸爵嗅,頻繁的讀寫消息會給數(shù)據(jù)庫造成壓力娇澎。所以,在真正的高并發(fā)場景下睹晒,該方案也會有瓶頸和限制的趟庄。
MQ(非事務消息)
通常情況下,在使用非事務消息支持的MQ產(chǎn)品時伪很,我們很難將業(yè)務操作與對MQ的操作放在一個本地事務域中管理戚啥。通俗點描述,還是以上述提到的“跨行轉(zhuǎn)賬”為例锉试,我們很難保證在扣款完成之后對MQ投遞消息的操作就一定能成功猫十。這樣一致性似乎很難保證。
先從消息生產(chǎn)者這端來分析呆盖,請看偽代碼:
根據(jù)上述代碼及注釋拖云,我們來分析下可能的情況:
操作數(shù)據(jù)庫成功,向MQ中投遞消息也成功应又,皆大歡喜
操作數(shù)據(jù)庫失敗宙项,不會向MQ中投遞消息了
操作數(shù)據(jù)庫成功,但是向MQ中投遞消息時失敗株扛,向外拋出了異常尤筐,剛剛執(zhí)行的更新數(shù)據(jù)庫的操作將被回滾
從上面分析的幾種情況來看,貌似問題都不大的洞就。那么我們來分析下消費者端面臨的問題:
消息出列后盆繁,消費者對應的業(yè)務操作要執(zhí)行成功。如果業(yè)務執(zhí)行失敗奖磁,消息不能失效或者丟失改基。需要保證消息與業(yè)務操作一致
盡量避免消息重復消費。如果重復消費咖为,也不能因此影響業(yè)務結(jié)果
如何保證消息與業(yè)務操作一致秕狰,不丟失?
主流的MQ產(chǎn)品都具有持久化消息的功能躁染。如果消費者宕機或者消費失敗鸣哀,都可以執(zhí)行重試機制的(有些MQ可以自定義重試次數(shù))。
如何避免消息被重復消費造成的問題吞彤?
保證消費者調(diào)用業(yè)務的服務接口的冪等性
通過消費日志或者類似狀態(tài)表來記錄消費狀態(tài)我衬,便于判斷(建議在業(yè)務上自行實現(xiàn)叹放,而不依賴MQ產(chǎn)品提供該特性)
總結(jié):這種方式比較常見,性能和吞吐量是優(yōu)于使用關(guān)系型數(shù)據(jù)庫消息表的方案挠羔。如果MQ****自身和業(yè)務都具有高可用性井仰,理論上是可以滿足大部分的業(yè)務場景的。不過在沒有充分測試的情況下破加,不建議在交易業(yè)務中直接使用俱恶。
MQ(事務消息)
舉個例子,Bob向Smith轉(zhuǎn)賬范舀,那我們到底是先發(fā)送消息合是,還是先執(zhí)行扣款操作?
好像都可能會出問題锭环。如果先發(fā)消息聪全,扣款操作失敗,那么Smith的賬戶里面會多出一筆錢辅辩。反過來难礼,如果先執(zhí)行扣款操作,后發(fā)送消息汽久,那有可能扣款成功了但是消息沒發(fā)出去鹤竭,Smith收不到錢踊餐。除了上面介紹的通過異常捕獲和回滾的方式外景醇,還有沒有其他的思路呢?
下面以阿里巴巴的RocketMQ中間件為例吝岭,分析下其設計和實現(xiàn)思路三痰。
RocketMQ第一階段發(fā)送Prepared消息時,會拿到消息的地址窜管,第二階段執(zhí)行本地事物散劫,第三階段通過第一階段拿到的地址去訪問消息,并修改狀態(tài)幕帆。細心的讀者可能又發(fā)現(xiàn)問題了获搏,如果確認消息發(fā)送失敗了怎么辦?RocketMQ會定期掃描消息集群中的事物消息失乾,這時候發(fā)現(xiàn)了Prepared消息常熙,它會向消息發(fā)送者確認,Bob的錢到底是減了還是沒減呢碱茁?如果減了是回滾還是繼續(xù)發(fā)送確認消息呢裸卫?RocketMQ會根據(jù)發(fā)送端設置的策略來決定是回滾還是繼續(xù)發(fā)送確認消息。這樣就保證了消息發(fā)送與本地事務同時成功或同時失敗纽竣。如下圖:
總結(jié):據(jù)筆者的了解墓贿,各大知名的電商平臺和互聯(lián)網(wǎng)公司茧泪,幾乎都是采用類似的設計思路來實現(xiàn)“最終一致性”的。這種方式適合的業(yè)務場景廣泛聋袋,而且比較可靠队伟。不過這種方式技術(shù)實現(xiàn)的難度比較大。目前主流的開源MQ(ActiveMQ幽勒、RabbitMQ缰泡、Kafka)均未實現(xiàn)對事務消息的支持,所以需二次開發(fā)或者新造輪子代嗤。比較遺憾的是棘钞,RocketMQ事務消息部分的代碼也并未開源,需要自己去實現(xiàn)干毅。
其他補償方式
做過支付寶交易接口的同學都知道宜猜,我們一般會在支付寶的回調(diào)頁面和接口里,解密參數(shù)硝逢,然后調(diào)用系統(tǒng)中更新交易狀態(tài)相關(guān)的服務姨拥,將訂單更新為付款成功。同時渠鸽,只有當我們回調(diào)頁面中輸出了success字樣或者標識業(yè)務處理成功相應狀態(tài)碼時叫乌,支付寶才會停止回調(diào)請求。否則徽缚,支付寶會每間隔一段時間后憨奸,再向客戶方發(fā)起回調(diào)請求,直到輸出成功標識為止凿试。
其實這就是一個很典型的補償例子排宰,跟一些MQ重試補償機制很類似。
一般成熟的系統(tǒng)中那婉,對于級別較高的服務和接口板甘,整體的可用性通常都會很高。如果有些業(yè)務由于瞬時的網(wǎng)絡故障或調(diào)用超時等問題详炬,那么這種重試機制其實是非常有效的盐类。
當然,考慮個比較極端的場景呛谜,假如系統(tǒng)自身有bug或者程序邏輯有問題在跳,那么重試1W次那也是無濟于事的。那豈不是就發(fā)生了“明明已經(jīng)付款呻率,卻顯示未付款不發(fā)貨”類似的悲動脖稀?
其實為了交易系統(tǒng)更可靠礼仗,我們一般會在類似交易這種高級別的服務代碼中吐咳,加入詳細日志記錄的逻悠,一旦系統(tǒng)內(nèi)部引發(fā)類似致命異常,會有郵件通知韭脊。同時童谒,后臺會有定時任務掃描和分析此類日志,檢查出這種特殊的情況沪羔,會嘗試通過程序來補償并郵件通知相關(guān)人員饥伊。
在某些特殊的情況下,還會有“人工補償”的蔫饰,這也是最后一道屏障琅豆。
小結(jié)
上訴的幾種方案中,筆者也大致總結(jié)了其設計思路篓吁,優(yōu)勢茫因,劣勢等,相信讀者已經(jīng)有了一定的理解杖剪。其實分布式系統(tǒng)的事務一致性本身是一個技術(shù)難題冻押,目前沒有一種很簡單很完美的方案能夠應對所有場景。具體還是要使用者根據(jù)不同的業(yè)務場景去抉擇盛嘿。