1. 程式人生 > >訊息中介軟體(一)分散式系統事務一致性解決方案大對比,誰最好使?(轉)

訊息中介軟體(一)分散式系統事務一致性解決方案大對比,誰最好使?(轉)

原文轉載至:https://blog.csdn.net/lovesomnus/article/details/51785108

 

在分散式系統中,同時滿足“一致性”、“可用性”和“分割槽容錯性”三者是不可能的。分散式系統的事務一致性是一個技術難題,各種解決方案孰優孰劣?

在OLTP系統領域,我們在很多業務場景下都會面臨事務一致性方面的需求,例如最經典的Bob給Smith轉賬的案例。傳統的企業開發,系統往往是以單體應用形式存在的,也沒有橫跨多個數據庫。

我們通常只需藉助開發平臺中特有資料訪問技術和框架(例如Spring、JDBC、ADO.NET),結合關係型資料庫自帶的事務管理機制來實現事務性的需求。關係型資料庫通常具有ACID特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability)。

而大型網際網路平臺往往是由一系列分散式系統構成的,開發語言平臺和技術棧也相對比較雜,尤其是在SOA和微服務架構盛行的今天,一個看起來簡單的功能,內部可能需要呼叫多個“服務”並操作多個數據庫或分片來實現,情況往往會複雜很多。單一的技術手段和解決方案,已經無法應對和滿足這些複雜的場景了。

分散式系統的特性
對分散式系統有過研究的讀者,可能聽說過“CAP定律”、“Base理論”等,非常巧的是,化學理論中ACID是酸、Base恰好是鹼。這裡筆者不對這些概念做過多的解釋,有興趣的讀者可以檢視相關參考資料。CAP定律如下圖:


在分散式系統中,同時滿足“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次那也是無濟於事的。那豈不是就發生了“明明已經付款,卻顯示未付款不發貨”類似的悲劇?

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

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

小結
上訴的幾種方案中,筆者也大致總結了其設計思路,優勢,劣勢等,相信讀者已經有了一定的理解。其實分散式系統的事務一致性本身是一個技術難題,目前沒有一種很簡單很完美的方案能夠應對所有場景。具體還是要使用者根據不同的業務場景去抉擇。
---------------------
作者:若水三千-LOVE
來源:CSDN
原文:https://blog.csdn.net/lovesomnus/article/details/51785108
版權宣告:本文為博主原創文章,轉載請附上博文連結!