這才是分散式事務的正確開啟方式!

Java全能架構師發表於2020-09-28

背景

隨著微服務的普及,分散式事務成為了系統設計中不得不面對的一個問題,而分散式事務的實現則十分複雜。閱讀本文之前,需要你對資料庫事務的ACID、CAP理論、Base理論以及兩階段提交有一定的認知,不熟悉者請自行百度或者閱讀參考部落格1、2、3和4。除此之外,在閱讀本文過程中,如果對某種方案不理解,強烈建議先閱讀對應方案中的參考部落格後再閱讀本文中對應的介紹。

為了便於後文敘述,這裡對ACID中的C(一致性)做一個強調:嚴格的事務一致性是使資料庫從一個一致性狀態變到另一個一致性狀態,且事務中間狀態不能被觀察到。

分散式事務的七種實現方案:

1、基於可靠訊息服務(基於可靠訊息中介軟體);

2、最大努力嘗試(基於訊息中介軟體);

3、TX-LCN(對LCN的實現);

4、X/Open DTP模型(XA規範,基於兩階段提交);

5、阿里DTS(基於TCC);

6、華為ServiceComb(對SAGA模式的實現);

7、阿里GTS(開源產品為Fescar,對XA協議改進後的實現)。

1、基於可靠訊息服務

我們常見的訊息中介軟體比如LinkedLin公司的Kafka、Rabbit科技有限公司的RabbitMQ以及Apache提供的ActiveMQ等等,都不支援事務。而阿里提供的RocketMQ則在解決了訊息的順序性和重複訊息冪等性的基礎上,實現了對事務的支援(詳見參考部落格5)。因而基於RocketMQ,就可以實現分散式事務(詳見參考部落格6)。實際上,參考部落格6中給出了兩套基於訊息服務的實現方案:第一,基於本地訊息服務的方案,針對的是訊息中介軟體本身不支援事務的場景,需要從應用設計的角度實現訊息資料的可靠性;第二,基於獨立訊息服務的方案,針對的是訊息中介軟體本身支援事務的場景。為了便於後面對比分析,這裡貼出了基於獨立訊息服務的設計方案:

基於可靠訊息服務的分散式事務方案的核心原理是對傳送到訊息中介軟體的訊息進行“兩階段提交”,即先提交,執行完本地事務後再確認訊息。通過這樣的機制,給事務的回滾提供了可能。

如下圖所示。如果用ACID來衡量該方案,基於可靠訊息服務的分散式事務方案能保證事務的最終原子性和永續性,但無法保證一致性和隔離性。這裡在原子性前面加“最終”二字,是因為基於訊息的操作本質上屬於非同步操作,顯然是非實時的。永續性自然不必多說。那麼為什麼說該方案無法保證一致性和隔離性呢?由下圖可知,本地事務執行提交成功之後,才會對訊息進行確認,而這個時候,遠端事務還未提交,一致性顯然無法滿足。

我們知道,資料庫的隔離性是通過鎖機制來保證的,因而基於可靠訊息服務的分散式事務方案要想滿足隔離性,往往還需要在事務發起方採用分散式鎖機制。因而總的來說,基於可靠訊息服務的分散式方案適用於對業務的實時一致性以及事務的隔離性要求都不高的內部系統。

2、最大努力嘗試

最大努力嘗試方案和基於可靠訊息服務一樣,都依賴於訊息中介軟體(參考部落格7)。不同的是,訊息中介軟體不需要保證可靠性,分散式事務的實現是依靠額外的校對系統或者報警系統(報警後人工處理)來保障的。因而和基於可靠訊息服務一樣,最大努力嘗試的分散式方案只能保證事務的最終原子性和永續性,無法保證一致性和隔離性。在應用場景方面,正如參考部落格7中所說,最大努力嘗試方案常常用於對業務的實時一致性以及事務的隔離性要求都不高的內部系統或者跨企業的業務活動中。下圖為最大努力嘗試的方案:

同基於可靠訊息服務的方案一樣,最大努力嘗試方案能保證事務的最終原子性和永續性,但無法保證一致性和隔離性。儘管最大努力嘗試方案只能保證最終原子性和永續性,但因其實現十分簡單,常常成為企業很多非核心業務的首選方案。

3、TX-LCN

由LCN的官網(參考部落格8)可知,LCN是lock、confirm和notify三個單詞的縮寫。個人人為,LCN方案是本文中七個方案中除了剛剛已經介紹的兩個基於訊息中介軟體的方案之外,最容易理解的方案,因而將其排在第三位來介紹。正如其官網文件(詳見參考部落格8)所說,LCN並不產生事務,LCN只是本地事務的協調工。這就意味著,使用LCN的系統完全依賴於本地事務。遺憾的是,LCN的官網對其核心原理介紹得比較簡略,看完不得其要領,反而是在另外的部落格(參考部落格9和10)中相對詳細地介紹了其實現原理。

LCN中包含一個TxManager和一個TxClient。其中TxManager負責維護全域性的事務資訊,而TxClient位於業務模組和本地事務層之間,其作用是代理本地事務層,通過代理連線詞實現了javax.sql.DataSource介面,並重寫了close方法,事務模組在提交關閉以後TxClient連線池將執行"假關閉"操作,等待TxManager協調完成事務以後在關閉連線,如下圖所示。

LCN的核心步驟

1. 建立事務組

是指在事務發起方開始執行業務程式碼之前先呼叫TxManager建立事務組物件,然後拿到事務標示GroupId的過程。

2. 新增事務組

新增事務組是指參與方在執行完業務方法以後,將該模組的事務資訊新增通知給TxManager的操作。

3. 關閉事務組

是指在發起方執行完業務程式碼以後,將發起方執行結果狀態通知給TxManager的動作。當執行完關閉事務組的方法以後,TxManager將根據事務組資訊來通知相應的參與模組提交或回滾事務。

以資料庫為例,前面提到的“假關閉”就是指步驟2中執行完業務方法後呼叫的close()方法是覆寫後的方法,該方法並不會真正提交事務,而是一直持有資料庫連線,並持有事務所需的相關資源鎖,直到第3步,由TxManager非同步通知事務提交或回滾才釋放鎖。

以參考部落格10中的時序圖為例(見下圖),參與方A和參與方B在執行完業務操作後,事務實際上並未提交,而是將提交前本地事務的操作結果返回給事務發起方,由事務發起方通知TxManager,並由後者根據通知的結果來非同步通知各參與方最終提交或回滾事務。當然TxManager對參與方的通知可能會失敗,因而需要補償機制。

由此可知,LCN中的三個單詞對應了LCN分散式事務操作中的三個關鍵步驟:1、分散式事務操作前先鎖定(lock)所有資源直到非同步通知(notify)釋放資源;2、執行業務操作,根據操作結果確認(confirm)事務應該提交還是回滾;3、根據第2步中的操作結果非同步通知(notify)事務的提交或回滾並最終釋放資源。

至此,我們瞭解到,LCN的核心原理是通過協調本地事務來實現分散式事務,分散式事務的實現依賴於本地事務。因而基於LCN的分散式事務的ACID特性取決於本地事務的ACID特性。一般來說,如果本地事務都能保證ACID,那麼基於LCN的分散式事務也能滿足AID。而對於一致性(Consistency),這是分散式事務的一個通病。

由Base理論可知,對於分散式系統,我們更關注的是最終一致性和最終一致性的實時性。相對於前面介紹的基於訊息中介軟體的兩種方案來說,基於LCN的分散式事務方案最終一致性的實時性遠高於前兩者。當然,其代價是併發效能的極大降低。實際上,對於分散式系統而言,能做到準實時的最終一致性就已經能滿足絕大多數應用場景。對於像銀行業務等對一致性有極致要求的極端場景,還可以通過在業務系統中使用分散式鎖或者分散式佇列來保證。

小結:LCN方案相對於後面將要介紹的其他方案來說,其優點是實現相對簡單,但其缺點也是顯然的:第一,依賴本地事務,如果要操作的資源不支援本地事務,則LCN模式無法直接使用。當然,對於這個限制,新版的TX-LCN通過支援後面將會介紹的TCC模式和TXC模式來解決。第二,LCN事務提交的整個過程都需要鎖住資源,因而效能低於TCC和TXC(TCC和TXC就是為了縮短分散式事務操作過程中資源的鎖定時間)。

4、基於XA規範的X/Open DTP模型

X/Open,即現在的open group,是一個獨立的組織,主要負責制定各種行業技術標準。X/OpenDTP即為該組織制定的一套分散式事務的方案。關於該模型詳細的介紹請閱讀參考部落格11,這裡只介紹其最核心的原理。

DTP模型元素

應用程式(Application Program ,簡稱AP):用於定義事務邊界(即定義事務的開始和結束),並且在事務邊界內對資源進行操作。

資源管理器(Resource Manager,簡稱RM):如資料庫、檔案系統等,並提供訪問資源的方式。

事務管理器(Transaction Manager ,簡稱TM):負責分配事務唯一標識,監控事務的執行進度,並負責事務的提交、回滾等。

通訊資源管理器(Communication Resource Manager,簡稱CRM):控制一個TM域(TMdomain)內或者跨TM域的分散式應用之間的通訊。

通訊協議(Communication Protocol,簡稱CP):提供CRM提供的分散式應用節點之間的底層通訊服務。

這裡我們重點看看AP、TM和RM之間的關係:

由上圖可知,XA規範的最主要作用是,定義了RM-TM的互動介面。實際上,XA規範除了定義的RM-TM互動的介面之外,還對兩階段提交協議進行了優化。具體的優化包括只讀斷言和一階段提交。具體請閱讀參考部落格11。

接下來我們來看XA規範的具體工作原理。參考部落格14中給出了XA規範提交流程示意圖:

提交步驟為:

在開始一個全域性事務之前,涉及的RM必須通過ax_regr(),向TM註冊以加入叢集;對應的,在沒有事務需要處理的時候,RM可以通過ax_unreg()向TM要求登出,離開叢集。

TM在對一個RM執行xa_開頭的具體操作前,必須先通過xa_open()開啟這個RM(本質是建立對話)——這其實也是分配XID的一個行為;與之相應的,TM執行xa_close()來關閉RM。

TM對RM呼叫的xa_start()和xa_stop()這對組合,一般用於標記區域性事務的開頭和結尾。這裡需要注意的有三點:

對於同一個RM,根據全域性事務的要求,可以前後執行多對組合——俾如說,先標記一個流水賬INSERT的區域性事務操作,然後再標記賬戶UPDATE的區域性事務操作。

TM執行該組合只是起到標記事務的作用,具體的業務命令是由AP交給RM的。

該組合除了執行這些標記工作外,其實還能在RM中實現多執行緒的join/suspend/resume管理。

TM呼叫RM的xa_prepare()來進行第一階段,呼叫xa_commit()或xa_rollback()執行第二階段。

這裡需要強調的是,XA規範中,整個兩階段提交過程資源都是處於鎖定狀態(見下圖)。在下圖中,無論Phase2的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。由此可知,基於XA規範的X/OpenDTP和LCN都存在資源長期鎖定的問題。

 

小結:X/Open DTP的核心原理是基於兩階段提交(XA規範),通過在整個提交過程中對資源進行鎖定來實現分散式事務,能很好地滿足AID特性,以及準實時的最終一致性。此外,該方案同LCN類似,效能低於TCC和TXC。因而在實際應用中,較少有系統會選擇該方案。

5、基於TCC的支付寶DTS

關於TCC原理,強烈建議先閱讀參考部落格12和13。這裡給出參考部落格13中的示意圖:

TCC 分散式事務模型包括三部分:

主業務服務:主業務服務為整個業務活動的發起方,服務的編排者,負責發起並完成整個業務活動。

從業務服務:從業務服務是整個業務活動的參與方,負責提供 TCC 業務操作,實現初步操作(Try)、確認操作(Confirm)、取消操作(Cancel)三個介面,供主業務服務呼叫。

業務活動管理器:業務活動管理器管理控制整個業務活動,包括記錄維護 TCC 全域性事務的事務狀態和每個從業務服務的子事務狀態,並在業務活動提交時呼叫所有從業務服務的Confirm 操作,在業務活動取消時呼叫所有從業務服務的 Cancel 操作。

一個完整的 TCC 分散式事務流程如下:

1. 主業務服務首先開啟本地事務;

2. 主業務服務向業務活動管理器申請啟動分散式事務主業務活動;

3. 然後針對要呼叫的從業務服務,主業務活動先向業務活動管理器註冊從業務活動,然後呼叫從業務服務的 Try 介面;

4. 當所有從業務服務的 Try 介面呼叫成功,主業務服務提交本地事務;若呼叫失敗,主業務服務回滾本地事務;

5. 若主業務服務提交本地事務,則 TCC 模型分別呼叫所有從業務服務的 Confirm 介面;若主業務服務回滾本地事務,則分別呼叫Cancel 介面;

6. 所有從業務服務的 Confirm 或 Cancel 操作完成後,全域性事務結束。

小結:以ACID來衡量TCC可知,TCC能滿足AID特性,以及準實時的最終一致性。TCC的核心原理是,在分散式事務操作中,先將所需資源預佔,然後將鎖釋放,最後再根據資源預佔的情況來決定使用資源還是退回資源。相比於XA規範,TCC方案採用預留資源的方式,將兩階段提交過程中的全域性鎖分成了兩段本地事務鎖,縮短了分散式資源鎖定的時間,從而提高了事務的併發度。相對於後面即將介紹的SAGA而言,因為TCC採用預佔資源的方式,其補償動作實現比較簡單。當然,TCC的缺點是業務入侵性大,尤其是已有業務如果想使用TCC方案,就需要修改原來的業務邏輯(後面介紹SAGA時還會再強調這一點)。

6、基於SAGA的華為ServiceComb

saga是1987年的一篇資料庫論文裡提到的一個概念(參考部落格16~19,重點是16),而ServiceComb是華為一個專案組對saga模式的實現方案。論文中指出,一個Saga事務就是一個Long Live Transaction(LLT),而一個LLT可以分解為多個本地事務所組成,即LLT= T1+ T2 + ... + Tn。每個本地事務執行完提交之後就會立即生效,比如執行完T1和T2之後,在執行T3時,T1和T2的事務就已經提交了。

假如執行T3失敗,由於T1和T2已經提交,無法回滾了。為了解決這個問題,saga要求業務服務方對每個本地事務Tx都提供對應的反向補償操作Cx(反向補償是指Tx的逆向操作),反向補償操作也是執行完就立即生效。在執行一個LLT的過程中,如果任意一個Tx出錯了,則通過呼叫所有已執行的Tx對應的Cx來進行反向補償。比如執行完T1和T2之後,執行T3時出錯,則需要執行C3、C2和C1來進行補償。

論文中對被saga呼叫的服務提出了兩點要求:其一是被呼叫的服務要支援冪等。由於分散式服務一定存在網路超時,所以這一點對於分散式服務來說,一般都能滿足。其二是服務要滿足可交換補償。如圖所示:

 

這裡有必要對可交換補償做一下說明。其實服務支援冪等和服務滿足可交換補償這兩點要求是為了處理執行補償操作時必然會遇到的兩個細分場景。我們首先來說說執行補償操作時會遇到的哪兩個場景:第一,事務操作Tx根本未被執行(比如因網路丟包導致事務操作Tx在圖中被丟棄,未到達服務端,或者在服務端執行失敗)。

對於這種場景,Cx無須也不能執行,否則反而會出錯,也就是說,saga執行Cx操作的前提是服務方確實執行了Tx操作(成功或失敗均可);第二,如上圖中的右圖所示,事務操作T發出後事務操作在網路中出現了較大的延時,觸發了saga的超時機制,因而執行了正向重試,發現重試失敗,接著執行反向補償操作C,當C已經執行完之後,最開始的事務操作T才到達服務方。

這個時候,saga要求最開始的事務操作T不能生效,因為C已經生效了。也就是說,saga要求服務一旦接受並執行了反向補償操作C,則不會再處理與之對應的正向操作。這麼做的目的是為了防止先到達的反向補償操作被後到達的正向事務操作給覆蓋掉。

其實場景二的實現依賴場景一中提供的保障,因為示例中正向的重試操作也有可能無法到達服務端,這就變成了場景一了。其實這個問題在後面介紹的TCC方案中也會遇到,並需要被解決(有興趣可提前看參考部落格20)。

除此之外,參考部落格16中還說明了saga模式本身只支援ACID中的ACD,而無法支援隔離性(而如果用本文的評判標準來看,saga也無法只能滿足準實時的一致性,而無法滿足強一致性)。為了支援隔離性,需要考慮在業務層加入鎖機制或者類似TCC的方式,採用在業務層預先凍結資源的方式對資源進行隔離。

關於saga模式的隔離性我還想補充說明的是,因為saga模式的本地事務是執行完就立即提交,而不能回滾,那麼在沒有隔離性保障的情況下,反向補償操作很可能無法執行成功。比如說有A和B和C三個賬戶,賬戶餘額都為100元,執行如下兩個併發事務:

小結:saga模式的核心原理是,將一個全域性事務分成若干個能獨立提交的本地事務,每個本地事務都對應一個反向補償操作,當本地事務提交失敗後,通過反向補償操作來取消本地事務的影響。

相比於TCC,Saga缺少預留動作,導致某些業務的補償動作的實現比較麻煩,比如業務是傳送郵件,在TCC模式下,先儲存草稿(Try)再傳送(Confirm),撤銷的話直接刪除草稿(Cancel)就行了。而Saga則就直接傳送郵件了(Ti),如果要撤銷則得再傳送一份郵件說明撤銷(Ci)。當然,對於另外一些簡單業務來說,Saga沒有預留動作也可以認為是優點(詳見參考部落格18):

有些業務很簡單,套用TCC需要修改原來的業務邏輯,而Saga只需要新增一個補償動作就行了。

TCC最少通訊次數為2n,而Saga為n(n=sub-transaction的數量)。

有些第三方服務沒有Try介面,TCC模式實現起來就比較tricky了,而Saga則很簡單。

沒有預留動作就意味著不必擔心資源釋放的問題,異常處理起來也更簡單(請對比Saga的恢復策略和TCC的異常處理)。

7、基於改進XA協議的阿里GTS

GTS,最初名為TXC,是Taobao Transaction Constructor的縮寫,於2014年4月立項,2014年10月釋出了TXC1.0版本,2015年12月釋出TXC 2.0版本,2017年2月阿里雲公測,外部更名為GTS(GlobalTransaction Service)。2019年1月,阿里分散式事務框架GTS開源了一個免費社群版Fescar。

前面在介紹XA規範的時候提到,兩階段提交過程資源都是處於鎖定狀態,為了便於對比,我再次貼出XA規範的提交示意圖(參考部落格21):

由圖可知,無論Phase2的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。設想一個正常執行的業務,大概率是90%以上的事務最終應該是成功提交的,我們是否可以在Phase1 就將本地事務提交呢?這樣 90% 以上的情況下,可以省去 Phase2 持鎖的時間,整體提高效率。

 

分支事務中資料的 本地鎖 由本地事務管理,在分支事務 Phase1 結束時釋放。

同時,隨著本地事務結束,連線 也得以釋放。

分支事務中資料的 全域性鎖(詳見參考部落格25) 在事務協調器側管理,在決議 Phase2 全域性提交時,全域性鎖馬上可以釋放。只有在決議全域性回滾的情況下,全域性鎖 才被持有至分支的 Phase2 結束。

這個設計,極大地減少了分支事務對資源(資料和連線)的鎖定時間,給整體併發和吞吐的提升提供了基礎。

當然,你肯定會問:Phase1 即提交的情況下,Phase2 如何回滾呢?首先,應用需要使用 Fescar的 JDBC 資料來源代理,也就是 Fescar 的 RM。

Phase1:

Fescar 的 JDBC 資料來源代理通過對業務 SQL 的解析,把業務資料在更新前後的資料映象組織成回滾日誌,利用本地事務的ACID 特性,將業務資料的更新和回滾日誌的寫入在同一個本地事務中提交。

這樣,可以保證:任何提交的業務資料的更新一定有相應的回滾日誌存在。

基於這樣的機制,分支的本地事務便可以在全域性事務的 Phase1 提交,馬上釋放本地事務鎖定的資源。

Phase2:

如果決議是全域性提交,此時分支事務此時已經完成提交,不需要同步協調處理(只需要非同步清理回滾日誌),Phase2 可以非常快速地完成。

如果決議是全域性回滾,RM 收到協調器發來的回滾請求,通過 XID 和 Branch ID 找到相應的回滾日誌記錄,通過回滾記錄生成反向的更新SQL 並執行,以完成分支的回滾。

 

由此可知,Fescar的核心原理是,對業務SQL進行解析,把業務資料在更新前後的資料映象組織成回滾日誌,利用本地事務的ACID特性,將業務資料的更新和回滾日誌的寫入在同一個本地事務中提交。除此之外,為了保證分散式事務的隔離性,在事務協調器側還增加了一把全域性鎖,以保證回滾日誌得以順利執行(可以回過頭再看看Saga方案中列舉的回滾失敗的示例)。

小結:以ACID來衡量Fescar可知,該方案能保證AID特性,以及準實時的最終一致性。實際上,對於分散式系統,如果分散式方案能同時保證AID特性以和準實時的最終一致性,就等價於能保證增刪改操作的ACID特性,而至於查詢操作,可能會讀取到事務中間狀態的資料,這在絕大多數業務場景中都是能接受的。而且前面也講了,對於像銀行業務等對一致性有極致要求的極端場景,也可以通過在業務系統中使用分散式鎖或者分散式佇列來保證。

此外,對比TCC和Fescar可知,TCC無論事務最終是提交還是回滾,本質上都需要對同一個資源執行兩次操作,一次是try,另一次是confirm或者cancel;而對於Fescar來說,大多數情況下,分散式事務都不需要回滾,而對於不需要回滾的分散式事務,每個資源只需要執行一次操作。從這個角度來說,Fescar的平均效能將比TCC更高。

總結

基於可靠訊息中介軟體的分散式方案的核心原理是對傳送到訊息中介軟體的訊息進行“兩階段提交”,即先提交,執行完本地事務後再確認訊息。通過這樣的機制,給事務的回滾提供了可能。該方案能保證事務的最終原子性和永續性,但無法保證一致性和隔離性。基於可靠訊息服務的分散式方案適用於對業務的實時一致性以及事務的隔離性要求都不高的系統。

最大努力嘗試方案的核心原理是依靠額外的校對系統或者報警系統來保證分散式事務。該方案能保證事務的最終原子性和永續性,但無法保證一致性和隔離性。基於可靠訊息服務的分散式方案適用於對業務的實時一致性以及事務的隔離性要求都不高的系統。

TX-LCN方案的核心原理是通過協調本地事務來實現分散式事務,分散式事務的實現依賴於本地事務。一般來說,如果本地事務都能保證ACID,那麼基於LCN的分散式事務也能滿足AID,而不能滿足一致性。TX-LCN實現相對簡單,但事務對資源的鎖定時間長,因而適用於對併發效能要求不高的場景。

X/Open DTP的核心原理是基於兩階段提交(XA規範),通過在整個提交過程中對資源進行鎖定來實現分散式事務,能很好地滿足AID特性,以及準實時的最終一致性。由於該方案對資源的鎖定時間長,因而適用於對併發效能要求不高的場景。

TCC的核心原理是,在分散式事務操作中,先將所需資源預佔,然後將鎖釋放,最後再根據資源預佔的情況來決定使用資源還是退回資源。相比於XA規範,TCC方案採用預留資源的方式,將兩階段提交過程中的全域性鎖分成了兩段本地事務鎖,縮短了分散式資源鎖定的時間,從而提高了事務的併發度。

saga模式的核心原理是,將一個全域性事務分成若干個能獨立提交的本地事務,每個本地事務都對應一個反向補償操作,當本地事務提交失敗後,通過反向補償操作來取消本地事務的影響。相比於TCC,Saga缺少預留動作,導致某些業務的補償動作的實現比較麻煩,比如業務是傳送郵件,但對於另外一些簡單業務來說,Saga沒有預留動作的特性降低了老系統接入Saga方案的成本。

Fescar的核心原理是,對業務SQL進行解析,把業務資料在更新前後的資料映象組織成回滾日誌,利用本地事務的ACID特性,將業務資料的更新和回滾日誌的寫入在同一個本地事務中提交。此外,為了保證分散式事務的隔離性,在事務協調器側還增加了一把全域性鎖,以保證回滾日誌得以順利執行。

如果覺得本文對你有幫助,可以點贊關注支援一下,也可以關注我公眾號,上面有更多技術乾貨文章以及相關資料共享,大家一起學習進步!

相關文章