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

關於如何實現一個TCC分佈式事務框架的一點思考

點擊上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

來源:百特開源

一個TCC事務框架需要解決的當然是分佈式事務的管理。關於TCC事務機制的介紹,可以參考TCC事務機制簡介

TCC事務模型雖然說起來簡單,然而要基於TCC實現一個通用的分佈式事務框架,卻比它看上去要複雜的多,不只是簡單的呼叫一下Confirm/Cancel業務就可以了的。

本文將以Spring容器為例,試圖分析一下,實現一個通用的TCC分佈式事務框架需要註意的一些問題。

一、TCC全域性事務必須基於RM本地事務來實現全域性事務


TCC服務是由Try/Confirm/Cancel業務構成的,其Try/Confirm/Cancel業務在執行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取資料。這些存取操作,必須要參與RM本地事務,以使其更改的資料要麼全部commit,要麼全部rollback。

這一點不難理解,考慮一下如下場景:

假設圖中的服務B沒有基於RM本地事務(以RDBS為例,可通過設置auto-commit為true來模擬),那麼一旦[B:Try]操作中途執行出錯,TCC事務框架後續決定回滾全域性事務時,該[B:Cancel]則需要判斷[B:Try]中哪些操作已經寫到DB、哪些操作還沒有寫到DB:如果[B:Try]業務有5個寫庫操作,[B:Cancel]業務則需要逐個判斷這5個操作是否生效,並將生效的操作執行反向操作。

不幸的是,由於[B:Cancel]業務也有n(0<=n<=5)個反向的寫庫操作,此時一旦[B:Cancel]也中途出錯,則後續的[B:Cancel]執行任務更加繁重。因為,相比第一次[B:Cancel]操作,後續的[B:Cancel]操作還需要判斷先前的[B:Cancel]操作的n(0<=n<=5)個寫庫中哪幾個已經執行、哪幾個還沒有執行,這就涉及到了冪等性問題。

然而,對冪等性的保障,很可能也需要涉及額外的寫庫操作,該寫庫操作又會因為沒有RM本地事務的支持而存在類似問題。。。

可想而知,如果不基於RM本地事務,TCC事務框架是無法有效的管理TCC全域性事務的。

反之,基於RM本地事務的TCC事務,這種情況則會很容易處理:[B:Try]操作中途執行失敗,TCC事務框架將其參與RM本地事務直接rollback即可。後續TCC事務框架決定回滾全域性事務時,在知道“[B:Try]操作涉及的RM本地事務已經rollback”的情況下,根本無需執行[B:Cancel]操作。

換句話說,基於RM本地事務實現TCC事務框架時,一個TCC型服務的Cancel業務要麼執行,要麼不執行,不需要考慮部分執行的情況。

二、TCC事務框架應該接管Spring容器的TransactionManager


基於RM本地事務的TCC事務框架,可以將各Try/Confirm/Cancel業務看著一個原子服務:一個RM本地事務提交,參與該RM本地事務的所有Try/Confirm/Cancel業務操作都生效;反之,則都不生效。

掌握每個RM本地事務的狀態以及它們與Try/Confirm/Cancel業務方法之間的對應關係,以此為基礎,TCC事務框架才能有效的構建TCC全域性事務。

TCC服務的Try/Confirm/Cancel業務方法在RM上的資料存取操作,其RM本地事務是由Spring容器的PlatformTransactionManager來commit/rollback的,TCC事務框架想要瞭解RM本地事務的狀態,只能通過接管Spring的事務管理器功能。

2.1. 為什麼TCC事務框架需要掌握RM本地事務的狀態?


首先,根據TCC機制的定義,TCC事務是通過執行Cancel業務來達到回滾效果的。仔細分析一下,這裡暗含一個事實:只有生效的Try業務操作才需要執行對應的Cancel業務操作。換句話說,只有Try業務操作所參與的RM本地事務被commit了,後續TCC全域性事務回滾時才需要執行其對應的Cancel業務操作;否則,如果Try業務操作所參與的RM本地事務被rollback了,後續TCC全域性事務回滾時就不能執行其Cancel業務,此時若盲目執行Cancel業務反而會導致資料不一致。

其次,Confirm/Cancel業務操作必須保證生效。Confirm/Cancel業務操作也會涉及RM資料存取操作,其參與的RM本地事務也必須被commit。TCC事務框架需要在確切的知道所有Confirm/Cancel業務操作參與的RM本地事務都被成功commit後,才能將標記該TCC全域性事務為完成。如果TCC事務框架誤判了Confirm/Cancel業務參與RM本地事務的狀態,就會造成全域性事務不一致。

最後,未完成的TCC全域性,TCC事務框架必須重新嘗試提交/回滾操作。重試時會再次呼叫各TCC服務的Confirm/Cancel業務方法。如果某個服務的Confirm/Cancel業務之前已經生效(其參與的RM本地事務已經提交),重試時就不應該再次被呼叫。否則,其Confirm/Cancel業務方法被多次呼叫,就會有“服務冪等性”的問題。

2.2. 攔截TCC服務的Try/Confirm/Cancel業務方法的執行,根據其異常信息可否知道其RM本地事務是否commit/rollback了呢?


基本上很難做到。為什麼這麼說呢?

第一,事務是可以在多個(本地/遠程)服務之間互相傳播其事務背景關係的,一個業務方法(Try/Confirm/Cancel)執行完畢並不一定會觸發當前事務的commit/rollback操作。比如,被傳播事務背景關係的業務方法,在它開始執行時,容器並不會為其創建新的事務,而是它的呼叫方參與的事務,使得二者操作在同一個事務中;同樣,在它執行完畢時,容器也不會提交/回滾它參與的事務的。因此,這類業務方法上的異常情況並不能反映他們是否生效。不接管Spring的TransactionManager,就無法瞭解事務於何時被創建,也無法瞭解它於何時被提交/回滾。

第二、一個業務方法可能會包含多個RM本地事務。比如:A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),這種情況下,A服務所參與的RM本地事務被提交時,B服務和C服務參與的RM本地事務則可能會被回滾。

第三、並不是丟擲了異常的業務方法,其參與的事務就回滾了。Spring容器的宣告式事務定義了兩類異常,其事務完成方向都不一樣:系統異常(一般為Unchecked異常,預設事務完成方向是rollback)、應用異常(一般為Checked異常,預設事務完成方向是commit)。二者的事務完成方向又可以通過@Transactional配置顯式的指定,如rollbackFor/noRollbackFor等。

第四、Spring容器還支持使用setRollbackOnly的方式顯式的控制事務完成方向;

最後、自行攔截業務方法的攔截器和Spring的事務處理的攔截器還會存在執行先後、攔截範圍不同等問題。例如,如果自行攔截器執行在前,就會出現業務方法雖然已經執行完畢但此時其參與的RM本地事務還沒有commit/rollback。

TCC事務框架的定位應該是一個TransactionManager,其職責是負責commit/rollback事務。而一個事務應該被commit還是被rollback,則應該是由Spring容器來決定的:Spring決定提交事務時,會呼叫TransactionManager來完成commit操作;Spring決定回滾事務時,會呼叫TransactionManager來完成rollback操作。

接管Spring容器的TransactionManager,TCC事務框架可以明確的得到Spring的事務性指令,並管理Spring容器中各服務的RM本地事務。否則,如果通過自行攔截的機制,則使得業務系統存在TCC事務處理、RM本地事務處理兩套事務處理邏輯,二者互不通信,各行其是。這種情況下要協調TCC全域性事務,基本上可以說是緣木求魚,本地事務尚且無法管理,更何談管理分佈式事務?

三、TCC事務框架應該具備故障恢復機制


一個TCC事務框架,若是沒有故障恢復的保障,是不成其為分佈式事務框架的。

分佈式事務管理框架的職責,不是做出全域性事務提交/回滾的指令,而是管理全域性事務提交/回滾的過程。它需要能夠協調多個RM資源、多個節點的分支事務,保證它們按全域性事務的完成方向各自完成自己的分支事務。

這一點,是不容易做到的。因為,實際應用中,會有各種故障出現,很多都會造成事務的中斷,從而使得統一提交/回滾全域性事務的標的不能達到,甚至出現”一部分分支事務已經提交,而另一部分分支事務則已回滾”的情況。

比較常見的故障,比如:業務系統服務器宕機、重啟;資料庫服務器宕機、重啟;網絡故障;斷電等。這些故障可能單獨發生,也可能會同時發生。作為分佈式事務框架,應該具備相應的故障恢復機制,無視這些故障的影響是不負責任的做法。

一個完整的分佈式事務框架,應該保障即使在最嚴苛的條件下也能保證全域性事務的一致性,而不是只能在最理想的環境下才能提供這種保障。退一步說,如果能有所謂“理想的環境”,那也無需使用分佈式事務了。

TCC事務框架要支持故障恢復,就必須記錄相應的事務日誌。事務日誌是故障恢復的基礎和前提,它記錄了事務的各項資料。TCC事務框架做故障恢復時,可以根據事務日誌的資料將中斷的事務恢復至正確的狀態,併在此基礎上繼續執行先前未完成的提交/回滾操作。

四、TCC事務框架應該提供Confirm/Cancel服務的冪等性保障


一般認為,服務的冪等性,是指標對同一個服務的多次(n>1)請求和對它的單次(n=1)請求,二者具有相同的副作用。

在TCC事務模型中,Confirm/Cancel業務可能會被重覆呼叫,其原因很多。比如,全域性事務在提交/回滾時會呼叫各TCC服務的Confirm/Cancel業務邏輯。執行這些Confirm/Cancel業務時,可能會出現如網絡中斷的故障而使得全域性事務不能完成。因此,故障恢復機制後續仍然會重新提交/回滾這些未完成的全域性事務,這樣就會再次呼叫參與該全域性事務的各TCC服務的Confirm/Cancel業務邏輯。

既然Confirm/Cancel業務可能會被多次呼叫,就需要保障其冪等性。

那麼,應該由TCC事務框架來提供冪等性保障?還是應該由業務系統自行來保障冪等性呢?

個人認為,應該是由TCC事務框架來提供冪等性保障。如果僅僅只是極個別服務存在這個問題的話,那麼由業務系統來負責也是可以的;然而,這是一類公共問題,毫無疑問,所有TCC服務的Confirm/Cancel業務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決。

而且,考慮一下由業務系統來負責冪等性需要考慮的問題,就會發現,這無疑增大了業務系統的複雜度。

五、TCC事務框架不能盲目的依賴Cancel業務來回滾事務


前文以及提到過,TCC事務通過Cancel業務來對Try業務進行回撤的機制暗含了一個事實:Try操作已經生效。

也就是說,只有Try操作所參與的RM本地事務已經提交的情況下,才需要執行其Cancel操作進行回撤。沒有執行、或者執行了但是其RM本地事務被rollback的Try業務,是一定不能執行其Cancel業務進行回撤的。

因此,TCC事務框架在全域性事務回滾時,應該根據TCC服務的Try業務的執行情況選擇合適的處理機制。而不能盲目的執行Cancel業務,否則就會導致資料不一致。

一個TCC服務的Try操作是否生效,這是TCC事務框架應該知道的,因為其Try業務所參與的RM事務也是由TCC事務框架所commit/rollbac的(前提是TCC事務框架接管了Spring的事務管理器)。所以,TCC事務回滾時,TCC事務框架可考慮如下處理策略:

1)如果TCC事務框架發現某個服務的Try操作的本地事務尚未提交,應該直接將其回滾,而後就不必再執行該服務的cancel業務;

2)如果TCC事務框架發現某個服務的Try操作的本地事務已經回滾,則不必再執行該服務的cancel業務;

3)如果TCC事務框架發現某個服務的Try操作尚未被執行過,那麼,也不必再執行該服務的cancel業務。

總之,TCC事務框架應該保障:

1)已生效的Try操作應該被其Cancel操作所回撤;

2)尚未生效的Try操作,則不應該執行其Cancel操作。這一點,不是冪等性所能解決的問題。如上文所述,冪等性是指服務被執行一次和被執行n(n>0)次所產生的影響相同。但是,未被執行和被執行過,二者效果肯定是不一樣的,這不屬於冪等性的範疇。

六、Cancel業務與Try業務並行,甚至先於Try操作完成


這應該算TCC事務機制特有的一個不可思議的陷阱。一般來說,一個特定的TCC服務,其Try操作的執行,是應該在其Confirm/Cancel操作之前的。Try操作執行完畢之後,Spring容器再根據Try操作的執行情況,指示TCC事務框架提交/回滾全域性事務。然後,TCC事務框架再去逐個呼叫各TCC服務的Confirm/Cancel操作。

然而,超時、網絡故障、服務器的重啟等故障的存在,使得這個順序會被打亂。比如:

上圖中,假設[B:Try]操作執行過程中,網絡閃斷,[A:Try]會收到一個RPC遠程呼叫異常。A不處理該異常,導致全域性事務決定回滾,TCC事務框架就會去呼叫[B:Cancel],而此刻A、B之間網絡剛好已經恢復。如果[B:Try]操作耗時較長(網絡阻塞/資料庫操作阻塞),就會出現[B:Try]和[B:Cancel]二者並行處理的現象,甚至[B:Cancel]先完成的現象。

這種情況下,由於[B:Cancel]執行時,[B:Try]尚未生效(其RM本地事務尚未提交),因此,[B:Cancel]是不能執行的,至少是不能生效(執行了其RM本地事務也要rollback)的。

然而,當[B:Cancel]處理完畢(跳過執行、或者執行後rollback其RM本地事務)後,[B:Try]操作完成又生效了(其RM本地事務成功提交),這就使得[B:Cancel]雖然提供了,但卻沒有起到回撤[B:Try]的作用,導致資料的不一致。

所以,TCC框架在這種情況下,需要:

1)將[B:Try]的本地事務標註為rollbackOnly,阻止其後續生效;

2)禁止其再次將事務背景關係傳遞給其他遠程分支,否則該問題將在其他分支上出現;

3)相應地,[B:Cancel]也不必執行,至少不能生效。

當然,TCC事務框架也可以簡單的選擇阻塞[B:Cancel],待[B:Try]執行完畢後,再根據它的執行情況判斷是否需要執行[B:Cancel]。不過,這種處理方式因為需要等待,所以,處理效率上會有所不及。

同樣的情況也會出現在confirm業務上,只不過,發生在Confirm業務上的處理邏輯與發生在Cancel業務上的處理邏輯會不一樣,TCC框架必須保證:

1)Confirm業務在Try業務之後執行,若發現並行,則只能阻塞相應的Confirm業務操作;

2)在進入Confirm執行階段之後,也不可以再提交同一全域性事務內的新的Try操作的RM本地事務。

七、TCC服務是否需要對外暴露三個服務接口?


不需要。

TCC服務與普通的服務一樣,只需要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務邏輯,只是因為全域性事務提交/回滾的需要才提供的,因此Confirm/Cancel業務只需要被TCC事務框架發現即可,不需要被呼叫它的其他業務服務所感知。

換句話說,業務系統的其他服務在需要呼叫TCC服務時,根本不需要知道它是否為TCC型服務。因為,TCC服務能被其他業務服務呼叫的也僅僅是其Try業務,Confirm/Cancel業務是不能被其他業務服務直接呼叫的。

八、TCC服務A的Confirm/Cancel業務方法中能否呼叫它依賴的TCC服務B的Confirm/Cancel業務方法?


最好是不要這樣做。

首先,沒有必要。TCC服務A依賴TCC服務B,那麼[A:Try]已經將事務背景關係傳播給[B:Try]了,後續由TCC事務框架來呼叫各自的Confirm/Cancel業務即可;

其次,Confirm/Cancel業務如果被允許呼叫其他服務,那麼它就有可能再次發起新的TCC全域性事務。如此遞迴下去,將會導致全域性事務關係混亂且不可控。

TCC全域性事務,應該儘量在Try操作階段傳播事務背景關係。Confirm/Cancel操作階段僅需要完成各自Try業務操作的確認操作/補償操作即可,不適合再做遠程呼叫,更不能再對外傳播事務背景關係。

綜上所述,本文傾向於認為,實現一個通用的TCC分佈式事務管理框架,還是相對比較複雜的。一般業務系統如果需要使用TCC事務機制,並不推薦自行設計實現。

這裡,給大家推薦一款開源的TCC分佈式事務管理器ByteTCC。ByteTCC基於Try/Confirm/Cancel機制實現,可與Spring容器無縫集成,兼容Spring的宣告式事務管理。提供對dubbo框架、Spring Cloud的開箱即用的支持,可滿足多資料源、跨應用、跨服務器等各種分佈式事務場景的需求。



如果你對 Dubbo / Netty 等等原始碼與原理感興趣,歡迎加入我的知識星球一起交流。長按下方二維碼噢

目前在知識星球更新了《Dubbo 原始碼解析》目錄如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 服務器

16. P2P 服務器

17. HTTP 服務器

18. 序列化 Serialization

19. 集群容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing

… 一共 69+ 篇

目前在知識星球更新了《Netty 原始碼解析》目錄如下:

01. 除錯環境搭建
02. NIO 基礎
03. Netty 簡介
04. 啟動 Bootstrap

05. 事件輪詢 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 位元組緩衝區 ByteBuf

09. 通道處理器 ChannelHandler

10. 編解碼 Codec

11. 工具類 Util

… 一共 61+ 篇

目前在知識星球更新了《資料庫物體設計》目錄如下:


01. 商品模塊
02. 交易模塊
03. 營銷模塊
04. 公用模塊

… 一共 17+ 篇

原始碼不易↓↓↓

點贊支持老艿艿↓↓

赞(0)

分享創造快樂