歡迎光臨
每天分享高質量文章

分佈式事務 Seata TCC 樣式深度解析 | SOFAChannel#4 直播整理

,有趣實用的分佈式架構頻道。

本文根據 SOFAChannel#4 直播分享整理,本期主題:分佈式事務 Seata TCC 樣式深度解析。

Seata:https://github.com/seata/seata

回顧視頻以及 PPT 查看地址見文末。

歡迎加入直播互動釘釘群:23127468,不錯過每場直播。

2019 年 3 月,螞蟻金服加入分佈式事務 Seata 的社區共建中,並貢獻其 TCC 樣式。本期是 SOFAChannel 第四期,主題:分佈式事務 Seata TCC 樣式深度解析,本文根據覺生的直播整理。

大家晚上好,我是 Seata Committer 覺生,來自螞蟻金服資料中間件團隊。今天的內容主要分為以下四個部分:

  • Seata TCC 樣式的原理解析;

  • 從 TCC 的業務模型與併發控制分享如何設計一個 TCC 接口,並且適配 TCC 模型;

  • 如何控制異常;

  • 性能優化,使得 TCC 樣式能夠滿足更高的業務需求。

1

 
Seata 的 TCC 樣式

1.1 服務化拆分

下麵我們就進入第一個主題,Seata 的 TCC 樣式。螞蟻金服早期是單系統架構,所有業務服務幾乎都在少數幾個系統中。隨著業務的發展,業務越來越複雜,服務之間的耦合度也越來越高,故我們對系統進行了重構,服務按照功能進行解耦和垂直拆分。拆分之後所帶來的問題就是一個業務活動原來只需要呼叫一個服務就能完成,現在需要呼叫多個服務才能完成,而網絡、機器等不可靠,資料一致性的問題很容易出現,與可擴展性、高可用容災等要求並肩成為金融 IT 架構支撐業務轉型升級的最大挑戰之一。

從圖中可以看到,從單系統到微服務轉變,其實是一個資源橫向擴展的過程,資源的橫向擴展是指當單台機器達到資源性能瓶頸,無法滿足業務增長需求時,就需要橫向擴展資源,形成集群。通過橫向擴展資源,提升非熱點資料的併發性能,這對於大體量的互聯網產品來說,是至關重要的。服務的拆分,也可以認為是資源的橫向擴展,只不過方向不同而已。

資源橫向擴展可能沿著兩個方向發展,包括業務拆分和資料分片:

  • 業務拆分。根據功能對資料進行分組,並將不同的微服務分佈在多個不同的資料庫上,這實際上就是 SOA 架構下的服務化。業務拆分就是把業務邏輯從一個單系統拆分到多個微服務中。

  • 資料分片。在微服務內部將資料拆分到多個資料庫上,為橫向擴展增加一個新的維度。資料分片就是把一個微服務下的單個 DB 拆分成多個 DB,具備一個 Sharding 的功能。通過這樣的拆解,相當於一種資源的橫向擴展,從而使得整個架構可以承載更高的吞吐。

橫向擴展的兩種方法可以同時進行運用:交易、支付與賬務三個不同微服務可以儲存在不同的資料庫中。另外,每個微服務內根據其業務量可以再拆分到多個資料庫中,各微服務可以相互獨立地進行擴展。

Seata 關註的就是微服務架構下的資料一致性問題,是一整套的分佈式事務解決方案。Seata 框架包含兩種樣式,一種是 AT 樣式。AT 樣式主要從資料分片的角度,關註多 DB 訪問的資料一致性,當然也包括多服務下的多 DB 資料訪問一致性問題。

另外一個就是 TCC 樣式,TCC 樣式主要關註業務拆分,在按照業務橫向擴展資源時,解決微服務間呼叫的一致性問題,保證讀資源訪問的事務屬性。

今天我們主要講的就是TCC樣式。在講 TCC 之前,我們先回顧一下 AT 樣式,這樣有助於我們理解後面的 TCC 樣式。

1.2 AT 樣式

對於 AT 樣式,之前其他同學已經分享過很多次,大家也應該比較熟悉了。AT 樣式下,把每個資料庫被當做是一個 Resource,Seata 里稱為 DataSource Resource。業務通過 JDBC 標準接口訪問資料庫資源時,Seata 框架會對所有請求進行攔截,做一些操作。每個本地事務提交時,Seata RM(Resource Manager,資源管理器) 都會向 TC(Transaction Coordinator,事務協調器) 註冊一個分支事務。當請求鏈路呼叫完成後,發起方通知 TC 提交或回滾分佈式事務,進入二階段呼叫流程。此時,TC 會根據之前註冊的分支事務回呼到對應參與者去執行對應資源的第二階段。TC 是怎麼找到分支事務與資源的對應關係呢?每個資源都有一個全域性唯一的資源 ID,並且在初始化時用該 ID 向 TC 註冊資源。在運行時,每個分支事務的註冊都會帶上其資源 ID。這樣 TC 就能在二階段呼叫時正確找到對應的資源。

這就是我們的 AT 樣式。簡單總結一下,就是把每個資料庫當做一個 Resource,在本地事務提交時會去註冊一個分支事務。

1.3 TCC 樣式

那麼對應到 TCC 樣式里,也是一樣的,Seata 框架把每組 TCC 接口當做一個 Resource,稱為 TCC Resource。這套 TCC 接口可以是 RPC,也以是服務內 JVM 呼叫。在業務啟動時,Seata 框架會自動掃描識別到 TCC 接口的呼叫方和發佈方。如果是 RPC 的話,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。

掃描到 TCC 接口的呼叫方和發佈方之後。如果是發佈方,會在業務啟動時向 TC 註冊 TCC Resource,與 DataSource Resource 一樣,每個資源也會帶有一個資源 ID。

如果是呼叫方,Seata 框架會給呼叫方加上切麵,與 AT 樣式一樣,在運行時,該切麵會攔截所有對 TCC 接口的呼叫。每呼叫一次 Try 接口,切麵會先向 TC 註冊一個分支事務,然後才去執行原來的 RPC 呼叫。當請求鏈路呼叫完成後,TC 通過分支事務的資源 ID 回呼到正確的參與者去執行對應 TCC 資源的 Confirm 或 Cancel 方法。

在講完了整個框架模型以後,大家可能會問 TCC 三個接口怎麼實現。因為框架本身很簡單,主要是掃描 TCC 接口,註冊資源,攔截接口呼叫,註冊分支事務,最後回呼二階段接口。最核心的實際上是 TCC 接口的實現邏輯。下麵我將根據螞蟻金服內部多年的實踐來為大家分析怎麼實現一個完備的 TCC 接口。

 

2

TCC 業務樣式與併發控制

2.1 TCC 設計原則

從 TCC 模型的框架可以發現,TCC 模型的核心在於 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 中的隔離性。後面會詳細講到。

2.2 賬務系統模型設計

下麵我們以金融核心鏈路里的賬務服務來分析一下。首先一個最簡化的賬務模型就是圖中所列,每個用戶或商戶有一個賬戶及其可用餘額。然後,分析下賬務服務的所有業務邏輯操作,無論是交易、充值、轉賬、退款等,都可以認為是對賬戶的加錢與扣錢。

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

那這兩套接口分別需要做什麼事情呢?如何將其分成兩個階段完成?下麵將會舉例說明 TCC 業務樣式的設計過程,並逐漸優化。

我們先來看扣錢的 TCC 資源怎麼實現。場景為 A 轉賬 30 元給 B。賬戶 A 的餘額中有 100 元,需要扣除其中 30 元。這裡的餘額就是所謂的業務資源,按照前面提到的原則,在第一階段需要檢查並預留業務資源,因此,我們在扣錢 TCC 資源的 Try 接口裡先檢查 A 賬戶餘額是否足夠,然後預留餘額里的業務資源,即扣除 30 元。

在 Confirm 接口,由於業務資源已經在 Try 接口裡扣除掉了,那麼在第二階段的 Confirm 接口裡,可以什麼都不用做。而在 Cancel 接口裡,則需要把 Try 接口裡扣除掉的 30 元還給賬戶。這是一個比較簡單的扣錢 TCC 資源的實現,後面會繼續優化它。

而在加錢的 TCC 資源里。在第一階段 Try 接口裡不能直接給賬戶加錢,如果這個時候給賬戶增加了可用餘額,那麼在一階段執行完後,賬戶里的錢就可以被使用了。但是一階段執行完以後,有可能是要回滾的。因此,真正加錢的動作需要放在 Confirm  接口裡。對於加錢這個動作,第一階段 Try 接口裡不需要預留任何資源,可以設計為空操作。那相應的,Cancel 接口沒有資源需要釋放,也是一個空操作。只有真正需要提交時,再在 Confirm 接口裡給賬戶增加可用餘額。

這就是一個最簡單的扣錢和加錢的 TCC 資源的設計。在扣錢 TCC 資源里,Try 接口預留資源扣除餘額,Confirm 接口空操作,Cancel 接口釋放資源,增加餘額。在加錢 TCC 資源里,Try 接口無需預留資源,空操作;Confirm 接口直接增加餘額;Cancel 接口無需釋放資源,空操作。

2.3 賬務系統模型併發控制

之前提到,設計一套 TCC 接口需要有兩點,一點是需要拆分業務邏輯成兩階段完成。這個我們已經介紹了。另外一點是要根據自身的業務模型控制併發。

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

舉個例子,比如金融行業里管理用戶資金,當用戶發起交易時,一般會先檢查用戶資金,如果資金充足,則扣除相應交易金額,增加賣家資金,完成交易。如果沒有事務隔離,用戶同時發起兩筆交易,兩筆交易的檢查都認為資金充足,實際上卻只夠支付一筆交易,結果兩筆交易都支付成功,導致資損。

可以發現,併發控制是業務邏輯執行正確的保證,但是像兩階段鎖這樣的併發訪問控制技術要求一直持有資料庫資源鎖直到整個事務執行結束,特別是在分佈式事務架構下,要求持有鎖到分佈式事務第二階段執行結束,也就是說,分佈式事務會加長資源鎖的持有時間,導致併發性能進一步下降。

因此,TCC 模型的隔離性思想就是通過業務的改造,在第一階段結束之後,從底層資料庫資源層面的加鎖過渡為上層業務層面的加鎖,從而釋放底層資料庫鎖資源,放寬分佈式事務鎖協議,將鎖的粒度降到最低,以最大限度提高業務併發性能。

還是以上面的例子舉例,“賬戶 A 上有 100 元,事務 T1 要扣除其中的 30 元,事務 T2 也要扣除 30 元,出現併發”。在第一階段 Try 操作中,需要先利用資料庫資源層面的加鎖,檢查賬戶可用餘額,如果餘額充足,則預留業務資源,扣除本次交易金額,一階段結束後,雖然資料庫層面資源鎖被釋放了,但這筆資金被業務隔離,不允許除本事務之外的其它併發事務動用。

併發的事務 T2 在事務 T1 一階段接口結束釋放了資料庫層面的資源鎖以後,就可以繼續操作,跟事務 T1 一樣,加鎖,檢查餘額,扣除交易金額。

事務 T1 和 T2 分別扣除的那一部分資金,相互之間無干擾。這樣在分佈式事務的二階段,無論 T1 是提交還是回滾,都不會對 T2 產生影響,這樣 T1 和 T2 可以在同一個賬戶上併發執行。

大家可以感受下,一階段結束以後,實際上採用業務加鎖的方式,隔離賬戶資金,在第一階段結束後直接釋放底層資源鎖,該用戶和賣家的其他交易都可以立刻併發執行,而不用等到整個分佈式事務結束,可以獲得更高的併發交易能力。

這裡稍微有點抽象,下麵我們將會針對業務模型進行優化,大家可以更直觀的感受業務加鎖的思想。

2.4 賬務系統模型優化

前面的模型大家肯定會想,為啥一階段就把錢扣除了?是的。之前只是為了簡單說明 TCC 模型的設計思想。在實際中,為了更好的用戶體驗,在第一階段,一般不會直接把賬戶的餘額扣除,而是凍結,這樣給用戶展示的時候,就可以很清晰的知道,哪些是可用餘額,哪些是凍結金額。

那業務模型變成什麼樣了呢?如圖所示,需要在業務模型中增加凍結金額欄位,用來表示賬戶有多少金額處以凍結狀態。

既然業務模型發生了變化,那扣錢和加錢的 TCC 接口也應該相應的調整。還是以前面的例子來說明。

在扣錢的 TCC 資源里。Try 接口不再是直接扣除賬戶的可用餘額,而是真正的預留資源,凍結部分可用餘額,即減少可用餘額,增加凍結金額。Confirm 接口也不再是空操作,而是使用 Try 接口預留的業務資源,即將該部分凍結金額扣除;最後在 Cancel 接口裡,就是釋放預留資源,把 Try 接口的凍結金額扣除,增加賬戶可用餘額。加錢的 TCC 資源由於不涉及凍結金額的使用,所以無需更改。

通過這樣的優化,可以更直觀的感受到 TCC 接口的預留資源、使用資源、釋放資源的過程。

那併發控制又變成什麼樣了呢?跟前面大部分類似,在事務 T1 的第一階段 Try 操作中,先鎖定賬戶,檢查賬戶可用餘額,如果餘額充足,則預留業務資源,減少可用餘額,增加凍結金額。併發的事務 T2 類似,加鎖,檢查餘額,減少可用餘額金額,增加凍結金額。

這裡可以發現,事務 T1 和 T2 在一階段執行完成後,都釋放了資料庫層面的資源鎖,但是在各自二階段的時候,相互之間並無干擾,各自使用本事務內第一階段Try接口內凍結金額即可。這裡大家就可以直觀感受到,在每個事務的第一階段,先通過資料庫層面的資源鎖,預留業務資源,即凍結金額。雖然在一階段結束以後,資料庫層面的資源鎖被釋放了,但是第二階段的執行並不會被干擾,這是因為資料庫層面資源鎖釋放以後通過業務隔離的方式為這部分資源加鎖,不允許除本事務之外的其它併發事務動用,從而保證該事務的第二階段能夠正確順利的執行。

通過這兩個例子,為大家講解了怎麼去設計一套完備的 TCC 接口。最主要的有兩點,一點是將業務邏輯拆分成兩個階段完成,即 Try、Confirm、Cancel 接口。其中  Try 接口檢查資源、預留資源、Confirm 使用資源、Cancel 接口釋放預留資源。另外一點就是併發控制,採用資料庫鎖與業務加鎖的方式結合。由於業務加鎖的特性不影響性能,因此,盡可能降低資料庫鎖粒度,過渡為業務加鎖,從而提高業務併發能力。

3

TCC 異常控制

在有了一套完備的 TCC 接口之後,是不是就真的高枕無憂了呢?答案是否定的。在微服務架構下,很有可能出現網絡超時、重發,機器宕機等一系列的異常 Case。一旦遇到這些 Case,就會導致我們的分佈式事務執行過程出現異常。根據螞蟻金服內部多年的使用來看,最常見的主要是這三種異常,分別是空回滾、冪等、懸掛。
 
因此,TCC 接口裡還需要解決這三類異常。
實際上,這三類問題可以在 Seata 框架里完成,只不過我們現在的 Seata 框架還不具備,之後我們會把這些異常 Case 的處理移植到 Seata 框架里,業務就無需關註這些異常情況,專註於業務邏輯即可。
 
雖然業務之後無需關心,但是瞭解一下其內部實現機制,也能更好的排查問題。
下麵我將為大家一一講解這三類異常出現的原因以及對應的解決方案。

3.1 空回滾

首先是空回滾。什麼是空回滾?空回滾就是對於一個分佈式事務,在沒有呼叫 TCC 資源 Try 方法的情況下,呼叫了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然後直接傳回成功。
 
什麼樣的情形會造成空回滾呢?
可以看圖中的第 2 步,前面講過,註冊分支事務是在呼叫 RPC 時,Seata 框架的切麵會攔截到該次呼叫請求,先向 TC 註冊一個分支事務,然後才去執行 RPC 呼叫邏輯。如果 RPC 呼叫邏輯有問題,比如呼叫方機器宕機、網絡異常,都會造成 RPC 呼叫失敗,即未執行 Try 方法。但是分佈式事務已經開啟了,需要推進到終態,因此,TC 會回呼參與者二階段 Cancel 接口,從而形成空回滾。

那會不會有空提交呢?理論上來說不會的,如果呼叫方宕機,那分佈式事務預設是回滾的。如果是網絡異常,那 RPC 呼叫失敗,發起方應該通知 TC 回滾分佈式事務,這裡可以看出為什麼是理論上的,就是說發起方可以在 RPC 呼叫失敗的情況下依然通知 TC 提交,這時就會發生空提交,這種情況要麼是編碼問題,要麼開發同學明確知道需要這樣做。
 
那怎麼解決空回滾呢?
前面提到,Cancel 要識別出空回滾,直接傳回成功。那關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。因此,需要一張額外的事務控製表,其中有分佈式事務 ID 和分支事務 ID,第一階段 Try 方法里會插入一條記錄,表示一階段執行了。Cancel 接口裡讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存在,則是空回滾。

3.2 冪等

接下來是冪等。冪等就是對於同一個分佈式事務的同一個分支事務,重覆去呼叫該分支事務的第二階段接口,因此,要求 TCC 的二階段 Confirm 和 Cancel 接口保證冪等,不會重覆使用或者釋放資源。如果冪等控制沒有做好,很有可能導致資損等嚴重問題。

什麼樣的情形會造成重覆提交或回滾?從圖中可以看到,提交或回滾是一次 TC 到參與者的網絡呼叫。因此,網絡故障、參與者宕機等都有可能造成參與者 TCC 資源實際執行了二階段防範,但是 TC 沒有收到傳回結果的情況,這時,TC 就會重覆呼叫,直至呼叫成功,整個分佈式事務結束。
 

怎麼解決重覆執行的冪等問題呢?一個簡單的思路就是記錄每個分支事務的執行狀態。在執行前狀態,如果已執行,那就不再執行;否則,正常執行。前面在講空回滾的時候,已經有一張事務控製表了,事務控製表的每條記錄關聯一個分支事務,那我們完全可以在這張事務控製表上加一個狀態欄位,用來記錄每個分支事務的執行狀態。

如圖所示,該狀態欄位有三個值,分別是初始化、已提交、已回滾。Try 方法插入時,是初始化狀態。二階段 Confirm 和 Cancel 方法執行後修改為已提交或已回滾狀態。當重覆呼叫二階段接口時,先獲取該事務控製表對應記錄,檢查狀態,如果已執行,則直接傳回成功;否則正常執行。

3.3 懸掛

最後是防懸掛。按照慣例,咱們來先講講什麼是懸掛。懸掛就是對於一個分佈式事務,其二階段 Cancel 接口比 Try 接口先執行。因為允許空回滾的原因,Cancel 接口認為 Try 接口沒執行,空回滾直接傳回成功,對於 Seata 框架來說,認為分佈式事務的二階段接口已經執行成功,整個分佈式事務就結束了。但是這之後 Try 方法才真正開始執行,預留業務資源,前面提到事務併發控制的業務加鎖,對於一個 Try 方法預留的業務資源,只有該分佈式事務才能使用,然而 Seata 框架認為該分佈式事務已經結束,也就是說,當出現這種情況時,該分佈式事務第一階段預留的業務資源就再也沒有人能夠處理了,對於這種情況,我們就稱為懸掛,即業務資源預留後沒法繼續處理。
 
什麼樣的情況會造成懸掛呢?
按照前面所講,在 RPC 呼叫時,先註冊分支事務,再執行 RPC 呼叫,如果此時 RPC 呼叫的網絡發生擁堵,通常 RPC 呼叫是有超時時間的,RPC 超時以後,發起方就會通知 TC 回滾該分佈式事務,可能回滾完成後,RPC 請求才到達參與者,真正執行,從而造成懸掛。

怎麼實現才能做到防懸掛呢?根據懸掛出現的條件先來分析下,懸掛是指二階段 Cancel 執行完後,一階段才執行。也就是說,為了避免懸掛,如果二階段執行完成,那一階段就不能再繼續執行。因此,當一階段執行時,需要先檢查二階段是否已經執行完成,如果已經執行,則一階段不再執行;否則可以正常執行。那怎麼檢查二階段是否已經執行呢?大家是否想到了剛纔解決空回滾和冪等時用到的事務控製表,可以在二階段執行時插入一條事務控制記錄,狀態為已回滾,這樣當一階段執行時,先讀取該記錄,如果記錄存在,就認為二階段已經執行;否則二階段沒執行。

3.3 異常控制實現

在分析完空回滾、冪等、懸掛等異常 Case 的成因以及解決方案以後,下麵我們就綜合起來考慮,一個 TCC 接口如何完整的解決這三個問題。
 
首先是 Try 方法。結合前面講到空回滾和懸掛異常,Try 方法主要需要考慮兩個問題,一個是 Try 方法需要能夠告訴二階段接口,已經預留業務資源成功。
第二個是需要檢查第二階段是否已經執行完成,如果已完成,則不再執行。因此,Try 方法的邏輯可以如圖所示:

先插入事務控製表記錄,如果插入成功,說明第二階段還沒有執行,可以繼續執行第一階段。如果插入失敗,則說明第二階段已經執行或正在執行,則丟擲異常,終止即可。
 
接下來是 Confirm 方法。因為 Confirm 方法不允許空回滾,也就是說,Confirm 方法一定要在 Try 方法之後執行。
因此,Confirm 方法只需要關註重覆提交的問題。可以先鎖定事務記錄,如果事務記錄為空,則說明是一個空提交,不允許,終止執行。如果事務記錄不為空,則繼續檢查狀態是否為初始化,如果是,則說明一階段正確執行,那二階段正常執行即可。如果狀態是已提交,則認為是重覆提交,直接傳回成功即可;如果狀態是已回滾,也是一個異常,一個已回滾的事務,不能重新提交,需要能夠攔截到這種異常情況,並報警。
 
最後是 Cancel 方法。因為 Cancel 方法允許空回滾,並且要在先執行的情況下,讓 Try 方法感知到 Cancel 已經執行,所以和 Confirm 方法略有不同。
首先依然是鎖定事務記錄。如果事務記錄為空,則認為 Try 方法還沒執行,即是空回滾。空回滾的情況下,應該先插入一條事務記錄,確保後續的 Try 方法不會再執行。如果插入成功,則說明 Try 方法還沒有執行,空回滾繼續執行。如果插入失敗,則認為Try 方法正再執行,等待 TC 的重試即可。如果一開始讀取事務記錄不為空,則說明 Try 方法已經執行完畢,再檢查狀態是否為初始化,如果是,則還沒有執行過其他二階段方法,正常執行 Cancel 邏輯。如果狀態為已回滾,則說明這是重覆呼叫,允許冪等,直接傳回成功即可。如果狀態為已提交,則同樣是一個異常,一個已提交的事務,不能再次回滾。
 
通過這一部分的講解,大家應該對 TCC 模型下最常見的三類異常 Case,空回滾、冪等、懸掛的成因有所瞭解,也從實際例子中知道了怎麼解決這三類異常,在解決了這三類異常的情況下,我們的 TCC 接口設計就是比較完備的了。
後續我們將會把這些解決方案移植到 Seata 框架中,由 Seata 框架來完成異常的處理,開發 TCC 接口的同學就不再需要關心了。

4

TCC 性能優化

雖然 TCC 模型已經完備,但是隨著業務的增長,對於 TCC 模型的挑戰也越來越大,可能還需要一些特殊的優化,才能滿足業務需求。下麵我們將會給大家講講,螞蟻金服內部在 TCC 模型上都做了哪些優化。

4.1 同庫樣式

第一個優化方案是改為同庫樣式。同庫樣式簡單來說,就是分支事務記錄與業務資料在相同的庫中。什麼意思呢?之前提到,在註冊分支事務記錄的時候,框架的呼叫方切麵會先向 TC 註冊一個分支事務記錄,註冊成功後,才會繼續往下執行 RPC 呼叫。TC 在收到分支事務記錄註冊請求後,會往自己的資料庫里插入一條分支事務記錄,從而保證事務資料的持久化儲存。那同庫樣式就是呼叫方切麵不再向 TC 註冊了,而是直接往業務的資料庫里插入一條事務記錄。
 

在講解同庫樣式的性能優化點之前,先給大家簡單講講同庫樣式的恢復邏輯。一個分佈式事務的提交或回滾還是由發起方通知 TC,但是由於分支事務記錄儲存在業務資料庫,而不是 TC 端。因此,TC 不知道有哪些分支事務記錄,在收到提交或回滾的通知後,僅僅是記錄一下該分佈式事務的狀態。那分支事務記錄怎麼真正執行第二階段呢?需要在各個參與者內部啟動一個異步任務,定期撈取業務資料庫中未結束的分支事務記錄,然後向 TC 檢查整個分佈式事務的狀態,即圖中的 StateCheckRequest 請求。TC 在收到這個請求後,會根據之前儲存的分佈式事務的狀態,告訴參與者是提交還是回滾,從而完成分支事務記錄。
 

那這樣做有什麼好處呢?左邊是採用同庫樣式前的呼叫關係圖,在每次呼叫一個參與者的時候,都是先向 TC 註冊一個分佈式事務記錄,TC 再持久化儲存在自己的資料庫中,也就是說,一個分支事務記錄的註冊,包含一次 RPC 和一次持久化儲存。
 
右邊是優化後的呼叫關係圖。
從圖中可以看出,每次呼叫一個參與者的時候,都是直接儲存在業務的資料庫中,從而減少與 TC 之間的 RPC 呼叫。優化後,有多少個參與者,就節約多少次 RPC 呼叫。
 
這就是同庫樣式的性能方案。
把分支事務記錄儲存在業務資料庫中,從而減少與 TC  的 RPC 呼叫。

4.2 異步化

另外一個性能優化方式就是異步化,什麼是異步化。TCC 模型的一個作用就是把兩階段拆分成了兩個獨立的階段,通過資源業務鎖定的方式進行關聯。資源業務鎖定方式的好處在於,既不會阻塞其他事務在第一階段對於相同資源的繼續使用,也不會影響本事務第二階段的正確執行。從理論上來說,只要業務允許,事務的第二階段什麼時候執行都可以,反正資源已經業務鎖定,不會有其他事務動用該事務鎖定的資源。

假設只有一個中間賬戶的情況下,每次呼叫支付服務的 Commit 接口,都會鎖定中間賬戶,中間賬戶存在熱點性能問題。

但是,在擔保交易場景中,七天以後才需要將資金從中間賬戶劃撥給商戶,中間賬戶並不需要對外展示。因此,在執行完支付服務的第一階段後,就可以認為本次交易的支付環節已經完成,並向用戶和商戶傳回支付成功的結果,並不需要馬上執行支付服務二階段的 Commit 接口,等到低鋒期時,再慢慢消化,異步地執行。

5

總結

今天進行了 Seata TCC 樣式的深度解析。主要介紹 Seata TCC 樣式的原理,從 TCC 業務模型與併發控制的角度告訴大家怎麼設計一個 TCC 的接口以及怎麼處理空回滾、冪等、懸掛等異常,最後對螞蟻金服內部對 TCC 的性能優化點簡單介紹,使得 TCC 樣式能夠滿足更高的業務需求。

業務各有不同,有些業務能容忍短期不一致,有些業務的操作可以冪等,無論什麼樣的分佈式事務解決方案都有其優缺點,沒有一個銀彈能夠適配所有。因此,業務需要什麼樣的解決方案,還需要結合自身的業務需求、業務特點、技術架構以及各解決方案的特性,綜合分析,才能找到最適合的方案。

如果大家對 Seata 的性能和需求有自己的想法,歡迎大家在釘釘群(搜索群號即可加入:23127468)或者 Github 上與我們討論交流。

Seata:https://github.com/seata/seata

今天的直播分享到這裡結束了,謝謝大家!

本期視頻回顧以及 PPT 查看地址

https://tech.antfin.com/activities/462

往期直播精彩回顧

  • SOFAChannel#3 SOFARPC 性能優化實踐(下):

    https://tech.antfin.com/activities/245

  • SOFAChannel#2 SOFARPC 性能優化實踐(上):

    https://tech.antfin.com/activities/244

  • SOFAChannel#1 從螞蟻金服微服務實踐談起:

    https://tech.antfin.com/activities/148

    已同步到看一看
    赞(0)

    分享創造快樂