分佈式系統事務一致性解決方案

轉自:http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency/

開篇

在OLTP系統領域,咱們在不少業務場景下都會面臨事務一致性方面的需求,例如最經典的Bob給Smith轉帳的案例。傳統的企業開發,系統每每是以單體應用形式存在的,也沒有橫跨多個數據庫。咱們一般只需藉助開發平臺中特有數據訪問技術和框架(例如Spring、JDBC、ADO.NET),結合關係型數據庫自帶的事務管理機制來實現事務性的需求。關係型數據庫一般具備ACID特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。mysql

而大型互聯網平臺每每是由一系列分佈式系統構成的,開發語言平臺和技術棧也相對比較雜,尤爲是在SOA和微服務架構盛行的今天,一個看起來簡單的功能,內部可能須要調用多個「服務」並操做多個數據庫或分片來實現,狀況每每會複雜不少。單一的技術手段和解決方案,已經沒法應對和知足這些複雜的場景了。spring

分佈式系統的特性

對分佈式系統有過研究的讀者,可能據說過「CAP定律」、「Base理論」等,很是巧的是,化學理論中ACID是酸、Base剛好是鹼。這裏筆者不對這些概念作過多的解釋,有興趣的讀者能夠查看相關參考資料。CAP定律以下圖:sql

在分佈式系統中,同時知足「CAP定律」中的「一致性」、「可用性」和「分區容錯性」三者是不可能的,這比現實中找對象需同時知足「高、富、帥」或「白、富、美」更加困難。在互聯網領域的絕大多數的場景,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。數據庫

分佈式事務

提到分佈式系統,必然要提到分佈式事務。要想理解分佈式事務,不得不先介紹一下兩階段提交協議。先舉個簡單但不精準的例子來講明:編程

第一階段,張老師做爲「協調者」,給小強和小明(參與者、節點)發微信,組織他們倆明天8點在學校門口集合,一塊兒去登山,而後開始等待小強和小明答覆。後端

第二階段,若是小強和小明都回答沒問題,那麼你們如約而至。若是小強或者小明其中一人回答說「明天沒空,不行」,那麼張老師會當即通知小強和小明「登山活動取消」。服務器

細心的讀者會發現,這個過程當中可能有不少問題的。若是小強沒看手機,那麼張老師會一直等着答覆,小明可能在家裏把登山裝備都準備好了卻一直等着張老師確認信息。更嚴重的是,若是到明天8點小強尚未答覆,那麼就算「超時」了,那小明到底去仍是不去集合登山呢?微信

這就是兩階段提交協議的弊病,因此後來業界又引入了三階段提交協議來解決該類問題。網絡

兩階段提交協議在主流開發語言平臺,數據庫產品中都有普遍應用和實現的,下面來介紹一下XOpen組織提供的DTP模型圖:架構

 

 

XA協議指的是TM(事務管理器)和RM(資源管理器)之間的接口。目前主流的關係型數據庫產品都是實現了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的,事務管理器和資源管理器之間也使用了XA協議。 本質上也是藉助兩階段提交協議來實現分佈式事務的,下面分別來看看XA事務成功和失敗的模型圖:

在JavaEE平臺下,WebLogic、Webshare等主流商用的應用服務器提供了JTA的實現和支持。而在Tomcat下是沒有實現的(其實筆者並不認爲Tomcat能算是JavaEE應用服務器),這就須要藉助第三方的框架Jotm、Automikos等來實現,二者均支持spring事務整合。

而在Windows .NET平臺中,則能夠藉助ado.net中的TransactionScop API來編程實現,還必須配置和藉助Windows操做系統中的MSDTC服務。若是你的數據庫使用的mysql,而且mysql是部署在Linux平臺上的,那麼是沒法支持分佈式事務的。 因爲篇幅關係,這裏不展開,感興趣的讀者能夠自行查閱相關資料並實踐。

總結:這種方式實現難度不算過高,比較適合傳統的單體應用,在同一個方法中存在跨庫操做的狀況。但分佈式事務對性能的影響會比較大,不適合高併發和高性能要求的場景。

提供回滾接口

在服務化架構中,功能X,須要去協調後端的A、B甚至更多的原子服務。那麼問題來了,假如A和B其中一個調用失敗了,那可怎麼辦呢?

在筆者的工做中常常遇到這類問題,每每提供了一個BFF層來協調調用A、B服務。若是有些是須要同步返回結果的,我會盡可能按照「串行」的方式去調用。若是調用A失敗,則不會盲目去調用B。若是調用A成功,而調用B失敗,會嘗試去回滾剛剛對A的調用操做。

固然,有些時候咱們沒必要嚴格提供單獨對應的回滾接口,能夠經過傳遞參數巧妙的實現。

這樣的狀況,咱們會盡可能把可提供回滾接口的服務放在前面。舉個例子說明:

咱們的某個論壇網站,天天登陸成功後會獎勵用戶5個積分,可是積分和用戶又是兩套獨立的子系統服務,對應不一樣的DB,這控制起來就比較麻煩了。解決思路:

  1. 把登陸和加積分的服務調用放在BFF層一個本地方法中。
  2. 當用戶請求登陸接口時,先執行加積分操做,加分紅功後再執行登陸操做
  3. 若是登陸成功,那固然最好了,積分也加成功了。若是登陸失敗,則調用加積分對應的回滾接口(執行減積分的操做)。

總結:這種方式缺點比較多,一般在複雜場景下是不推薦使用的,除非是很是簡單的場景,很是容易提供回滾,並且依賴的服務也很是少的狀況。

 

這種實現方式會形成代碼量龐大,耦合性高。並且很是有侷限性,由於有不少的業務是沒法很簡單的實現回滾的,若是串行的服務不少,回滾的成本實在過高。

本地消息表

這種實現方式的思路,實際上是源於ebay,後來經過支付寶等公司的佈道,在業內普遍使用。其基本的設計思想是將遠程分佈式事務拆分紅一系列的本地事務。若是不考慮性能及設計優雅,藉助關係型數據庫中的表便可實現。

舉個經典的跨行轉帳的例子來描述。

第一步僞代碼以下,扣款1W,經過本地事務保證了憑證消息插入到消息表中。

第二步,通知對方銀行帳戶上加1W了。那問題來了,如何通知到對方呢?

一般採用兩種方式:

  1. 採用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件
  2. 採用定時輪詢掃描的方式,去檢查消息表的數據。

兩種方式其實各有利弊,僅僅依靠MQ,可能會出現通知失敗的問題。而過於頻繁的定時輪詢,效率也不是最佳的(90%是無用功)。因此,咱們通常會把兩種方式結合起來使用。

解決了通知的問題,又有新的問題了。萬一這消息有重複被消費,往用戶賬號上多加了錢,那豈不是後果很嚴重?

仔細思考,其實咱們能夠消息消費方,也經過一個「消費狀態表」來記錄消費狀態。在執行「加款」操做以前,檢測下該消息(提供標識)是否已經消費過,消費完成後,經過本地事務控制來更新這個「消費狀態表」。這樣子就避免重複消費的問題。

總結:上訴的方式是一種很是經典的實現,基本避免了分佈式事務,實現了「最終一致性」。可是,關係型數據庫的吞吐量和性能方面存在瓶頸,頻繁的讀寫消息會給數據庫形成壓力。因此,在真正的高併發場景下,該方案也會有瓶頸和限制的。

MQ(非事務消息)

一般狀況下,在使用非事務消息支持的MQ產品時,咱們很難將業務操做與對MQ的操做放在一個本地事務域中管理。通俗點描述,仍是以上述提到的「跨行轉帳」爲例,咱們很難保證在扣款完成以後對MQ投遞消息的操做就必定能成功。這樣一致性彷佛很難保證。

先從消息生產者這端來分析,請看僞代碼:

根據上述代碼及註釋,咱們來分析下可能的狀況:

  1. 操做數據庫成功,向MQ中投遞消息也成功,皆大歡喜
  2. 操做數據庫失敗,不會向MQ中投遞消息了
  3. 操做數據庫成功,可是向MQ中投遞消息時失敗,向外拋出了異常,剛剛執行的更新數據庫的操做將被回滾

從上面分析的幾種狀況來看,貌似問題都不大的。那麼咱們來分析下消費者端面臨的問題:

  1. 消息出列後,消費者對應的業務操做要執行成功。若是業務執行失敗,消息不能失效或者丟失。須要保證消息與業務操做一致
  2. 儘可能避免消息重複消費。若是重複消費,也不能所以影響業務結果

如何保證消息與業務操做一致,不丟失?

主流的MQ產品都具備持久化消息的功能。若是消費者宕機或者消費失敗,均可以執行重試機制的(有些MQ能夠自定義重試次數)。

如何避免消息被重複消費形成的問題?

  1. 保證消費者調用業務的服務接口的冪等性
  2. 經過消費日誌或者相似狀態表來記錄消費狀態,便於判斷(建議在業務上自行實現,而不依賴MQ產品提供該特性)

 

總結:這種方式比較常見,性能和吞吐量是優於使用關係型數據庫消息表的方案。若是MQ自身和業務都具備高可用性,理論上是能夠知足大部分的業務場景的。不過在沒有充分測試的狀況下,不建議在交易業務中直接使用。

MQ(事務消息)

舉個例子,Bob向Smith轉帳,那咱們究竟是先發送消息,仍是先執行扣款操做?

好像均可能會出問題。若是先發消息,扣款操做失敗,那麼Smith的帳戶裏面會多出一筆錢。反過來,若是先執行扣款操做,後發送消息,那有可能扣款成功了可是消息沒發出去,Smith收不到錢。除了上面介紹的經過異常捕獲和回滾的方式外,還有沒有其餘的思路呢?

下面以阿里巴巴的RocketMQ中間件爲例,分析下其設計和實現思路。

RocketMQ第一階段發送Prepared消息時,會拿到消息的地址,第二階段執行本地事物,第三階段經過第一階段拿到的地址去訪問消息,並修改狀態。細心的讀者可能又發現問題了,若是確認消息發送失敗了怎麼辦?RocketMQ會按期掃描消息集羣中的事物消息,這時候發現了Prepared消息,它會向消息發送者確認,Bob的錢究竟是減了仍是沒減呢?若是減了是回滾仍是繼續發送確認消息呢?RocketMQ會根據發送端設置的策略來決定是回滾仍是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。以下圖:

總結:據筆者的瞭解,各大知名的電商平臺和互聯網公司,幾乎都是採用相似的設計思路來實現「最終一致性」的。這種方式適合的業務場景普遍,並且比較可靠。不過這種方式技術實現的難度比較大。目前主流的開源MQ(ActiveMQ、RabbitMQ、Kafka)均未實現對事務消息的支持,因此需二次開發或者新造輪子。比較遺憾的是,RocketMQ事務消息部分的代碼也並未開源,須要本身去實現。

其餘補償方式

作過支付寶交易接口的同窗都知道,咱們通常會在支付寶的回調頁面和接口裏,解密參數,而後調用系統中更新交易狀態相關的服務,將訂單更新爲付款成功。同時,只有當咱們回調頁面中輸出了success字樣或者標識業務處理成功相應狀態碼時,支付寶纔會中止回調請求。不然,支付寶會每間隔一段時間後,再向客戶方發起回調請求,直到輸出成功標識爲止。

其實這就是一個很典型的補償例子,跟一些MQ重試補償機制很相似。

通常成熟的系統中,對於級別較高的服務和接口,總體的可用性一般都會很高。若是有些業務因爲瞬時的網絡故障或調用超時等問題,那麼這種重試機制實際上是很是有效的。

固然,考慮個比較極端的場景,假如系統自身有bug或者程序邏輯有問題,那麼重試1W次那也是無濟於事的。那豈不是就發生了「明明已經付款,卻顯示未付款不發貨」相似的悲劇?

其實爲了交易系統更可靠,咱們通常會在相似交易這種高級別的服務代碼中,加入詳細日誌記錄的,一旦系統內部引起相似致命異常,會有郵件通知。同時,後臺會有定時任務掃描和分析此類日誌,檢查出這種特殊的狀況,會嘗試經過程序來補償並郵件通知相關人員。

在某些特殊的狀況下,還會有「人工補償」的,這也是最後一道屏障。

關於做者

丁浪,現就任於某垂直電商平臺,擔任技術架構師。關注高併發、高可用的架構設計,對系統服務化、分庫分表、性能調優等方面有深刻研究和豐富實踐經驗。熱衷於技術研究和分享

相關文章
相關標籤/搜索