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

分佈式事務中的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操作,可以降低熱點數據性能問題,在業務低峯期慢慢消化,異步的執行。

總結

整體上了解了一個分佈式事務框架的原理和實現,並解決常見的異常問題和性能問題,可以幫助我們自研一套框架解決業務分佈式事務需求。

當然不同業務要求不同,一個好的分佈式事務需要適配自身業務特點,找到更合適的結合點。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章