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

一個TCC事務框架須要解決的固然是分佈式事務的管理。關於TCC事務機制的介紹,能夠參考TCC事務機制簡介。 TCC事務模型雖說起來簡單,然而要基於TCC實現一個通用的分佈式事務框架,卻比它看上去要複雜的多,不僅是簡單的調用一下Confirm/Cancel業務就能夠了的。git

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

1、TCC全局事務必須基於RM本地事務來實現全局事務

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

這一點不難理解,考慮一下以下場景:數據庫

image

假設圖中的服務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業務要麼執行,要麼不執行,不須要考慮部分執行的狀況。網絡

2、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,就沒法瞭解事務於什麼時候被建立,也沒法瞭解它於什麼時候被提交/回滾。 第2、一個業務方法可能會包含多個RM本地事務的狀況。好比: A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),這種狀況下,A服務所參與的RM本地事務被提交時,B服務和C服務參與的RM本地事務則可能會被回滾。 第3、並非拋出了異常的業務方法,其參與的事務就回滾了。Spring容器的聲明式事務定義了兩類異常,其事務完成方向都不同:系統異常(通常爲Unchecked異常,默認事務完成方向是rollback)、應用異常(通常爲Checked異常,默認事務完成方向是commit)。兩者的事務完成方向又能夠經過@Transactional配置顯式的指定,如rollbackFor/noRollbackFor等。 第4、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全局事務,基本上能夠說是緣木求魚,本地事務尚且沒法管理,更何談管理分佈式事務?

3、TCC事務框架應該具有故障恢復機制

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

分佈式事務管理框架的職責,不是作出全局事務提交/回滾的指令,而是管理全局事務提交/回滾的過程。它須要可以協調多個RM資源、多個節點的分支事務,保證它們按全局事務的完成方向各自完成本身的分支事務。這一點,是不容易作到的。由於,實際應用中,會有各類故障出現,不少都會形成事務的中斷,從而使得統一提交/回滾全局事務的目標不能達到,甚至出現」一部分分支事務已經提交,而另外一部分分支事務則已回滾」的狀況。比較常見的故障,好比:業務系統服務器宕機、重啓;數據庫服務器宕機、重啓;網絡故障;斷電等。這些故障可能單獨發生,也可能會同時發生。做爲分佈式事務框架,應該具有相應的故障恢復機制,無視這些故障的影響是不負責任的作法。

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

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

4、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事務框架來解決;並且,考慮一下由業務系統來負責冪等性須要考慮的問題,就會發現,這無疑增大了業務系統的複雜度。

5、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)次所產生的影響相同。可是,未被執行和被執行過,兩者效果確定是不同的,這不屬於冪等性的範疇。

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

這應該算TCC事務機制特有的一個難以想象的陷阱。通常來講,一個特定的TCC服務,其Try操做的執行,是應該在其Confirm/Cancel操做以前的。Try操做執行完畢以後,Spring容器再根據Try操做的執行狀況,指示TCC事務框架提交/回滾全局事務。而後,TCC事務框架再去逐個調用各TCC服務的Confirm/Cancel操做。

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

image

上圖中,假設[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本地事務。

7、TCC服務複用性是否是相對較差?

TCC事務機制的定義,決定了一個服務須要提供三個業務實現:Try業務、Confirm業務、Cancel業務。可能會有人所以認爲TCC服務的複用性較差。怎麼說呢,要是將 Try/Confirm/Cancel業務邏輯單獨拿出來複用,其複用性固然是很差的,Try/Confirm/Cancel 邏輯做爲TCC型服務中的一部分,是不能單獨做爲一個組件來複用的。Try、Confirm、Cancel業務共同才構成一個組件,若是要複用,應該是複用整個TCC服務組件,而不是單獨的Try/Confirm/Cancel業務。

8、TCC服務是否須要對外暴露三個服務接口?")**8、TCC服務是否須要對外暴露三個服務接口?

不須要。TCC服務與普通的服務同樣,只須要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務邏輯,只是由於全局事務提交/回滾的須要才提供的,所以Confirm/Cancel業務只須要被TCC事務框架發現便可,不須要被調用它的其餘業務服務所感知。

換句話說,業務系統的其餘服務在須要調用TCC服務時,根本不須要知道它是否爲TCC型服務。由於,TCC服務能被其餘業務服務調用的也僅僅是其Try業務,Confirm/Cancel業務是不能被其餘業務服務直接調用的。

9、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的開箱即用的支持,可知足多數據源、跨應用、跨服務器等各類分佈式事務場景的需求。

往期精彩文章

架構:經過案例讀懂 RESTful 架構風格
架構:一文讀懂Apache Flink架構及特性分析
架構:大數據推薦系統實時架構和離線架構
微服務:架構下靜態數據通用緩存機制
微服務:小型系統如何「微服務」開發
微服務:深刻理解爲何要設計冪等性的服務
微服務:有贊服務化架構演進
中間件:應用消息中間件設計能夠解決哪些實際問題
分佈式:理解大型分佈式架構的演進歷史、技術原理、最佳實踐
面試必備:HashMap源碼解析(JDK8)
緩存穿透:緩存雪崩解決方案分析

做者:百特開源
出處:www.bytesoft.org

相關文章
相關標籤/搜索