分散式事務中的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對假設抽象成以下邏輯:
- 初步操作Try:完成所有業務檢查,預留必要的業務資源。
- 確認操作Confirm:真正執行業務邏輯,不做任何檢查,只使用Try階段預留的業務資源。所以只要try成功,confirm必須成功。同時confirm需滿足冪等性,因為框架面對不確定性普遍會進行重試,以保證事務提交併只成功一次。
- 取消操作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操作,可以降低熱點資料效能問題,在業務低峰期慢慢消化,非同步的執行。
總結
整體上了解了一個分散式事務框架的原理和實現,並解決常見的異常問題和效能問題,可以幫助我們自研一套框架解決業務分散式事務需求。
當然不同業務要求不同,一個好的分散式事務需要適配自身業務特點,找到更合適的結合點。
更多內容: