代碼改變世界

阿里是如何處理分布式事務的

2019-05-20 11:36 春哥大魔王 閱讀(...) 評論(...) 編輯 收藏

分布式事務中的TCC模式,貌似是阿里提出來的,所以阿里自研的分布式事務框架總是少不了TCC的影子。

服務拆分

很多系統早期都是單系統服務架構,所有業務聚合在少數幾個系統中對外提供服務。隨著業務發展,服務之間耦合比較嚴重,一般會對服務進行重構,重構的主要思想也就是圍繞“拆分”展開。

比如按照功能進行解耦的垂直拆分,拆分之后原有系統中的業務調用,就變成了分布式的調用了,但是由于網絡的不可靠性,數據一致性問題,可擴展性問題,高可用容災問題成為分布式事務的主要挑戰。而對于在服務之間數據交付的時候容易造成的數據不一致問題,一般需要引入分布式事務對數據一致性做控制。

單系統到微服務拆分的過程,是一個資源橫向擴展的過程,當單臺機器資源無法承擔更大的請求時,可以多臺機器形成集群。

資源拆分主要有兩個執行方向:

  • 按業務拆分,也就是將數據按照業務分組,將不同服務的數據放到不同的存儲上,類似于soa架構下的服務化,已業務單元為核心。
  • 按數據拆分,也就是常說的數據分片,按照橫向擴展緯度,將單個DB拆分成多個DB,數據存儲具備統一的Sharding功能,達到資源橫向擴展,承擔更高的吞吐。

Seata模式

Seata關注的是微服務架構下的數據一致性問題,是整套的分布式事務解決方案。Seata框架包含兩種模式:

  • AT模式,關注的是數據分片角度,關注DB訪問的數據一致性,多服務下多DB數據訪問的一致性
  • TCC模式,TCC模式主要是圍繞業務拆分展開,當業務在橫向擴展資源時,解決了服務之間調用的一致性,保證資源訪問的事務性

AT模式

AT模式下會把每個DB當作一個Resource,數據庫就是 DataSource Resource。業務通過標準的JDBC接口訪問數據庫資源,Seata框架會對所有請求進行攔截,做事務操作。

在每個事務提交時,Seata Resource Manager(RM 資源管理器)都會向Transaction Coorrdinator(TC 事務協調器)注冊一個分支事務。

當請求鏈路調用完成后,發起方通知TC事務提交或者進行事務回滾,進入兩階段提交調用流程。

二階段操作時,TC根據之前注冊的分支事務回調對應參與者去執行對應資源的第二階段。

每個資源都有全局唯一的資源ID,在初始化時用這個ID向TC注冊,之后的事務協調過程中,TC就可以根據事務ID找到事務和資源的對應關系。事務協調過程中,每個事務的注冊都會攜帶這個資源ID,這樣TC可以通過資源ID在第二階段調用時找到正確的資源了。

簡單來說AT模式,就是把數據庫當作一個Resource,本地事務提交時會去注冊一個分支事務。

TCC模式

在Seata框架中,每組TCC接口當作一個Resource,稱為TCC Resource。當然一組TCC接口可以是RPC,也可以是服務內JVM調用。

業務啟動時,Seata框架自動掃描識別到對應的TCC接口及其調用方和發布方。

如果是事務的發布方,會在業務啟動時向TC注冊TC Resource,類似于DataSource Resource,每個資源有唯一的全局資源ID。

如果是事務的調用方,Seata框架給調用方加上切面,類似于AT模式,運行時攔截所有TCC接口調用。
每調用一次Try接口,切面會先向TC注冊一個分支事務,然后才會執行原有的RPC調用。
當請求鏈路調用完成后,TC通過分支事務的資源ID回調正確的參與者去執行對應的TCC資源的Confirm或Cancel方法。

了解了框架模型后,可以知道框架本身會掃描TCC接口,注冊資源,攔截接口調用,注冊分支事務,之后回調第二階段接口。

核心是TCC接口的實現邏輯。

TCC接口實現

在業務接入事務框架的TCC模式之后,大部分工作都是在考慮如何實現TCC服務上。

設計TCC接口需要注意業務邏輯的拆解和資源調用的隔離。

業務邏輯分解

需要將操作分成兩階段完成的方式,TCC=Try-Confirm-Cancel 相對于XA等傳統模式,特征在于不依賴RM對分布式事務的支持,而是通過業務邏輯分解來實現分布式事務。

TCC模式對于業務系統存在假設,其對外提供的服務需要接受一些不確定性,外部對于業務邏輯的調用首先是個臨時操作,外部調用對于后續的業務處理保留取消權。如果業務調用認為全局事務應該回滾,就需要取消之前的臨時操作。如果業務調用認為全局事務可以提交,就會放棄之前臨時操作的取消權。初步的臨時操作最后都會被確認或取消。

TCC對假設抽象成以下邏輯:

  1. 初步操作Try:完成所有業務檢查,預留必要的業務資源。
  2. 確認操作Confirm:真正執行業務邏輯,不做任何檢查,只使用Try階段預留的業務資源。所以只要try成功,confirm必須成功。同時confirm需滿足冪等性,因為框架面對不確定性普遍會進行重試,以保證事務提交并只成功一次。
  3. 取消操作Cancel:釋放Try階段預留的資源,同樣,cancel操作需要滿足冪等性。

資源調用隔離

業務系統需要根據自身業務特點和業務模型控制并發,類似于ACID的隔離性。

以金融核心鏈路的簡化模型為例:

每個賬戶或商戶有一個賬號及其可用余額。交易邏輯涉及到交易,充值,轉賬,退款等這些都是對賬戶進行加錢和扣錢。

于是可以把賬務系統拆分成兩套TCC接口,兩個TCC Resource,一個加錢TCC接口,一個扣錢TCC接口。

扣錢TCC

A轉賬30元給B,A的余額需要從100元減去30元,余額就是所謂的業務資源。

按照TCC原則,第一階段需要檢查并預留業務資源:

  • 檢查:在TCC資源的Try接口中檢查A是否有足夠的余額
  • 預留:然后預留余額紫玉啊,并扣除30元

由于業務資源已經在第一階段的try接口里面扣除了,第二階段的confirm接口可以什么都不做,是個空實現。
cancel接口需要把try接口里面扣除的30元還給賬戶,進行資源釋放。

加錢TCC

第一階段的try接口不能直接給賬戶加錢,因為如果加錢之后,賬戶的余額就會被使用了。因此真正的加錢操作需要放到confirm接口中。
第一階段的try接口不需要預留任何資源,可以設計為空實現。
Cancel接口沒有資源需要釋放,所以也可以是個空實現。
真正提交時,執行confirm接口增加可用余額。

事務并發控制

Seata框架本身提供兩階段原子提交,保證分布式事務原子性。事務的隔離則是交給了業務邏輯來實現。隔離的本質就是控制并發,防止并發事務操作相同資源引起結果錯亂。

以經典的轉賬為例,當用戶發起交易時,首先檢查用戶資金,資金充足,扣除交易金額,增加賣家資金,完成交易。
如果沒有事務隔離,用戶發起兩筆交易,兩筆交易都認為資金充足,實際上只夠一筆交易,結果兩筆交易都支付成功,導致資損。

所以并發控制是業務邏輯正確執行的保證,如果采用基于數據庫的兩階段鎖控制并發訪問,需要在事務中一直持有數據庫資源鎖到整個事務執行結束,如果在分布式架構下,鎖需要持有到事務第二階段結束,由于鎖的持有時間過長,會導致并發能力的下降。

因此TCC模式的隔離思想體現在通過業務改造實現。

第一階段結束之后,從底層數據庫資源層面加鎖過度到上層業務層面的加鎖,從而降低底層數據庫鎖資源,放寬分布式事務鎖協議,將鎖粒度降到最低,更大限度提高并發性能。

如果A賬戶有100元,事務T1需要扣除30元,事務T2需要扣除20元,出現了并發。
TCC對于這種操作,在第一階段Try操作中,需要利用數據庫資源層面加鎖,檢查賬戶可用余額,如果余額充足,則預留業務資源,扣除本次交易金額,一階段結束后,雖然數據庫層面資源鎖釋放了,但是這筆資金被業務隔離,不允許本次事務之外的其他并發事務動用。

事務T1結束之后釋放數據庫層面資源鎖,事務T2可以發起自己的第一階段操作,進行加鎖,檢查余額,扣除金額等操作。

事務T1和事務T2分別扣除自己資金,相互直接不受干擾,這樣在第二階段時,無論T1是提交還是回滾都不會對T2產生影響,這樣T1和T2就可以在同一個賬戶上并發執行了。

所以第一階段結束后,實際上采用業務加鎖方式,隔離賬戶資金。第一階段結束后,釋放底層資源鎖,用戶和賣家的其他交易都可以立刻并發執行,而不用等到整個分布式事務結束。

轉賬模型優化

在系統了解了TCC模型的思想后,可以對我們之前的轉賬模型進行優化了。

真實項目中,為了更好的用戶體驗,第一階段一般不會直接把賬戶的余額自動扣除,而是凍結,這樣給用戶展示的時候,可以清晰的知道,可用余額有哪些,凍結中金額有哪些。

業務模型變成如下:

需要在模型中增加凍結金額字段,用來表示賬戶中多少金額處于凍結狀態。

優化之后的TCC模型里面的扣錢TCC邏輯如下:

  • try接口不再直接扣除賬戶可用余額,而是真正預留資源,凍結部分空用余額,也就相應減少了可用金額。
  • confirm接口不再是空操作,而是使用try接口預留的業務資源,將凍結金額扣除。
  • cancel接口中,釋放預留資源,把try里面凍結的金額扣除,增加可用金額。

加錢TCC邏輯不涉及凍結金額的使用,無需修改。

優化后的模型可以規整的看到預留資源,使用資源,釋放資源的過程。

并發控制邏輯如下:

  • 事務T1在第一階段try操作中,先鎖定賬戶,檢查賬戶可用余額,如果余額充足,預留業務資源,減少可用金額,增加凍結金額。
  • 并發的事務T2,類似的需要加鎖,檢查余額,減少可用余額,增加凍結余額。

在第二階段各自事務使用第一階段try鎖定的凍結金額資源即可。
所以第一層面的是通過數據庫層面的鎖,預留業務資源,凍結金額。通過業務隔離方式將這部分資源加鎖,不允許本地事務之外的其他并發事務調用,保證事務在第二階段正確順利執行。

所以整個TCC模式核心是進行業務邏輯拆分,拆成兩個階段,try,confirm,cancel。try進行資源檢查,資源預留,confirm使用資源,cancel接口釋放預留資源。
并發控制采用數據庫鎖和業務加鎖組合方式實現,由于業務加鎖特性不影響性能,可以降低數據庫鎖粒度,提高并發能力。

TCC異常處理

在面對分布式系統需要面對的網絡超時,重發,宕機等不可用問題時,事務框架往往有不同的問題,最常見的有:空回滾,冪等,懸掛。

因此在TCC接口里面需要處理這三類異常。

空回滾

就是對于一個分布式事務,在沒有調用TCC資源try方法的情況下,調用了第二階段的cancel方法,cancel方法需要識別出這是一個空回滾,然后返回成功。

什么情況會返回空回滾呢?

在進行RPC調用時,Seata框架會進行切面攔截請求,進行分支事務注冊,先向TC注冊分布式事務,然后執行RPC調用邏輯。
如果RPC調用邏輯有問題,比如調用方機器宕機,網絡異常,會造成RPC調用失敗,也就是未能成功執行Try方法。但事務已經開啟,需要推進到終態,因此TC會回調第二階段cancel接口,從而形成空回滾。

解決空回滾需要額外的一個事務控制表,其中有分布式事務id和分支事務id,第一階段try方法里面插入一條記錄,表示一階段執行了。cancel接口讀取該記錄,如果記錄存在,正常回滾。如果記錄不存在,執行空回滾。

冪等

事務框架里面冪等的目的是為了解決,同一個分布式事務里面同一個分支事務,調用該分支事務的第二階段接口,因此TCC里面的二階段提交的confirm和cancel接口需要保證冪等,不會重發使用或者釋放資源。冪等控制沒有做好的話,很有可能導致資損等問題。

什么樣情況會造成重復提交呢?

提交或回滾是一次TC到參與者網絡的調用。因此,網絡故障,參與者宕機等都有可能造成參與者TCC資源實際執行第二階段方法,但是TC沒有收到返回結果的情況,這是TC會重復調用,直到調用成功,整個分布式事務結束。

解決重復執行冪等問題的思路是,可以記錄每個分支事務的執行狀態,在執行前狀態,如果執行已執行,就不再執行。否則,正常執行。

參照事務控制表,事務控制表的每條記錄關聯一個分支事務,可以在這張事務控制表增加一個狀態字段,用來記錄每個分支事務的執行狀態。

該狀態字段有三個值,分別是初始化,已提交,已回滾。
try方法插入時,是初始化狀態。
第二階段confirm和cancel方法執行后修改為已提交或回滾狀態。
當重復調用二階段接口時,先獲取該事務控制表對應記錄,檢查狀態,如果已執行,則返回成功,否則正常執行。

懸掛

懸掛就是對于一個分布式事務,第二階段cancel接口比try接口先執行,因為允許空回滾,cancel接口認為try接口沒有執行,空回滾執行返回成功,seata框架認為,分布式事務第二階段接口已經執行成功,整個分布式事務就結束了。

但是此時有可能真正的try方法才真正執行,預留業務資源,由于try過程中會加鎖預留資源,并且只有當前事務可以使用,但seata框架認為分布式事務已經結束,就會出現第一階段預留的業務資源沒人能夠處理,這種情況屬于懸掛。

在RPC調用時,先注冊分支事務,在執行RPC調用,如果此時RPC調用網絡阻塞,通常RPC調用是有超時時間的,RPC超時以后,發起方通知TC回滾該事務,可能回滾完成后,RPC請求才到達參與者,真正執行,從而造成懸掛。

為了防止懸掛,如果第二階段完成,一階段就不能在繼續了,因此一階段執行時,需要先檢查二階段釋放已經執行完成,如果執行完成,則一階段不再執行。否則可以正常執行。

同樣依賴于事務控制表,在二階段執行時插入一條事務控制記錄,狀態為回滾,這樣當一階段執行時,先讀取該記錄,如果存在,就認為二階段已執行。否則認為二階段沒有執行。

異常控制

分析完回滾,冪等,懸掛之后,考慮如何通過TCC解決問題。

try方法需要考慮兩個問題,try方法能夠告訴二階段接口已經預留資源成功。還需要檢查二階段是否執行完成,如果完成不再執行。

先插入事務控制表,如果插入成功,說明二階段還沒有執行,可以繼續執行第一階段,如果插入失敗,說二階段已經執行或正在執行,拋出異常,終止。

confirm方法不允許空回滾,所以confirm方法一定要在try方法之后執行,所以confirm方法只需要關注重復提交的問題,可以先鎖事務記錄,如果事務記錄為空,則說明是一個空提交,不允許,終止執行。
如果事務記錄不為空,則繼續檢查狀態是否為初始化,如果是,說明一階段正確執行,二階段正常執行即可。如果狀態為已提交,則認為重復提交,直接返回成功即可。如果狀態是已回滾,就是一個異常事務,一個已經回滾的事務不能重新提交,需要攔截到這種情況,并報警。

cancel方法不允許空回滾,在先執行時,需要讓try感知到,所以需要鎖定事務記錄,如果事務記錄為空,則認為try方法還沒有執行,為空回滾。空回滾情況下先插入一條事務記錄,確保后續try方法不會再執行。
如果插入成功,說明try還沒有執行,空回滾繼續執行。如果插入失敗,認為try方法正在執行,等待tc重試即可。

如果一開始讀取事務記錄不為空,說明try方法已經執行完畢,在檢查狀態是否為初始化,如果是,則還沒有執行二階段方法,正常執行cancel邏輯。
如果狀態為已回滾,說明是重復調用,允許冪等,直接返回成功即可。如果狀態為已提交,則同樣是個異常,一個已提交的事務,不能再次回滾。

性能優化

隨著業務中對于Seata框架的使用越來越多,TCC的性能問題越來越明顯。

同數據庫

分支事務記錄和業務數據在相同的數據庫中,在切面調用時不再向TC注冊,而是直接向業務數據庫里面插入一條記錄。

一個分布式事務的提交和回滾還是由發起方通知TC,但是由于分支事務記錄保存在業務數據庫,不是TC端,所以TC不知道哪些分支事務記錄,在收到提交或回滾通知后,僅僅記錄下該分布式事務的狀態。
為了執行二階段操作,各個參與者內部啟動一個異步任務,定時撈取業務數據庫中未結束的分支事務記錄,然后向TC檢查整個分布式事務的狀態,就是statecheckrequest請求。TC在收到這個請求后,根據之前保存的分布式事務狀態,告訴參與者是提交還是回滾,從而完成分支記錄。

左邊是同步模式前調用圖,每次調用一個參與者的時候,都是向TC注冊一個分布式事務記錄,TC持久化存儲在自己的數據庫中,就是說一個分支事務注冊包含了一次RPC和一次持久化存儲。

右邊是優化后的調用圖,每次調用一個參與者的時候,都是直接保存在業務數據庫中,減少了和TC之間的RPC調用,優化后,有多少個參與者,就節約了多少RPC調用。

一個數據庫方案,把分支記錄保存在業務數據庫中,減少了和TC的RPC調用。

異步化

TCC模型把兩階段拆分成了兩個獨立的階段,通過資源業務鎖定方式進行關聯。資源鎖定好處是,不會阻塞其他事務第一階段對于相同資源的繼續使用,也不會影響第二階段的正確執行,理論上說,只要業務允許,事務的二階段什么時候執行都可以,反正資源已經鎖定了,不會被其他事務鎖定該資源。

對于一些資源鎖定,但是資源執行間隔比較久的業務場景來說,可以在第一階段后,認為本次交易環節完成,并向用戶和商戶返回支付成功結果,并不需要馬上執行二階段的confirm操作,可以降低熱點數據性能問題,在業務低峰期慢慢消化,異步的執行。

總結

整體上了解了一個分布式事務框架的原理和實現,并解決常見的異常問題和性能問題,可以幫助我們自研一套框架解決業務分布式事務需求。
當然不同業務要求不同,一個好的分布式事務需要適配自身業務特點,找到更合適的結合點。

更多內容:

四川金7乐历史开奖号码查询