TCC方案是多是目前最火的一種柔性事務方案了。關於TCC(Try-Confirm-Cancel)的概念,最先是由Pat Helland於2007年發表的一篇名爲《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。在該論文中,TCC仍是以Tentative-Confirmation-Cancellation命名。正式以Try-Confirm-Cancel做爲名稱的是Atomikos公司,其註冊了TCC商標。spring
國內最先關於TCC的報道,應該是InfoQ上對阿里程立博士的一篇採訪。通過程博士的這一次傳道以後,TCC在國內逐漸被你們廣爲了解並接受。sql
Atomikos公司在商業版本事務管理器ExtremeTransactions中提供了TCC方案的實現,可是因爲其是收費的,所以相應的不少的開源實現方案也就涌現出來,如:tcc-transaction、ByteTCC、spring-cloud-rest-tcc。數據庫
TCC的做用主要是解決跨服務調用場景下的分佈式事務問題,在本文中,筆者將先介紹一個跨服務的場景案例,並分析其中存在的分佈式事務問題;而後介紹TCC的基本概念以及其是如何解決這個問題的。json
Atomikos官網上<<Composite Transactions for SOA>>一文中,以航班預約的案例,來介紹TCC要解決的事務場景。在這裏筆者虛構一個徹底相同的場景,把本身當作航班預約的主人公,來介紹這個案例。事實上,你能夠把本案例當作官方文檔案例的一個翻譯,只不過把地點從Brussels-->Toronto-->Washington,改爲從合肥-->昆明-->大理。app
有一次,筆者買彩票中獎了(純屬虛構),準備從合肥出發,到雲南大理去遊玩,而後使用美團App(機票代理商)來訂機票。發現沒有從合肥直達大理的航班,須要到昆明進行中轉。以下圖: 框架
從圖中咱們能夠看出來,從合肥到昆明乘坐的是四川航空,從昆明到大理乘坐的是東方航空。異步
因爲使用的是美團App預約,當我選擇了這種航班預約方案後,美團App要去四川航空和東方航空各幫我購買一張票。以下圖: 分佈式
考慮最簡單的狀況:美團先去川航幫我買票,若是買不到,那麼東航也不必買了。若是川航購買成功,再去東航購買另外一張票。post
如今問題來了:假設美團先從川航成功買到了票,而後去東航買票的時候,由於天氣問題,東航航班被取消了。那麼此時,美團必須取消川航的票,由於只有一張票是沒用的,不取消就是浪費個人錢。那麼若是取消會怎樣呢?若是讀者有取消機票經歷的話,非正常退票,確定要扣手續費的。在這裏,川航原本已經購買成功,如今由於東航的緣由要退川航的票,川航應該是要扣代理商的錢的。性能
那麼美團就要保證,若是任一航班購買失敗,都不能扣錢,怎麼作呢?
兩個航空公司都爲美團提供如下3個接口:機票預留接口、確認接口、取消接口。美團App分2個階段進行調用,以下所示:
在第1階段:
美團分別請求兩個航空公司預留機票,兩個航空公司分別告訴美圖預留成功仍是失敗。航空公司須要保證,機票預留成功的話,以後必定能購買到。
在第2階段:
若是兩個航空公司都預留成功,則分別向兩個公司發送確認購買請求。
若是兩個航空公司任意一個預留失敗,則對於預留成功的航空公司也要取消預留。這種狀況下,對於以前預留成功機票的航班取消,也不會扣用戶的錢,由於購買並沒實際發生,以前只是請求預留機票而已。
經過這種方案,能夠保證兩個航空公司購買機票的一致性,要不都成功,要不都失敗,即便失敗也不會扣用戶的錢。若是在兩個航班都已經已經確認購買後,再退票,那確定仍是要扣錢的。
固然,實際狀況確定這裏提到的確定要複雜,一般航空公司在第一階段,對於預留的機票,會要求在指定的時間必須確認購買(支付成功),若是沒有及時確認購買,會自動取消。假設川航要求10分鐘內支付成功,東航要求30分鐘內支付成功。以較短的時間算,若是用戶在10分鐘內支付成功的話,那麼美團會向兩個航空公司都發送確認購買的請求,若是超過10分鐘(以較短的時間爲準),那麼就不能進行支付。
再次強調,這個案例,能夠算是<<Composite Transactions for SOA>>中航班預約案例的漢化版。而實際美團App是如何實現這種須要中轉的航班預約需求,筆者並不知情。
另外,注意這只是一個案例場景,實際狀況中,你是很難去驅動航空公司進行接口改造的。
Whatever,這個方案提供給咱們一種跨服務條用保證事務一致性的一種解決思路,能夠把這種方案當作TCC的雛形。
TCC是Try-Confirm-Cancel的簡稱:
Try階段:
完成全部業務檢查(一致性),預留業務資源(準隔離性)
回顧上面航班預約案例的階段1,機票就是業務資源,全部的資源提供者(航空公司)預留都成功,try階段纔算陳宮
Confirm階段:
確認執行業務操做,不作任何業務檢查, 只使用Try階段預留的業務資源。回顧上面航班預約案例的階段2,美團APP確認兩個航空公司機票都預留成功,所以向兩個航空公司分別發送確認購買的請求。
Cancel階段:
取消Try階段預留的業務資源。回顧上面航班預約案例的階段2,若是某個業務方的業務資源沒有預留成功,則取消全部業務資源預留請求。
敏銳的讀者立馬會想到,TCC與XA兩階段提交有着殊途同歸之妙,下圖列出了兩者之間的對比:
1) 在階段1:
在XA中,各個RM準備提交各自的事務分支,事實上就是準備提交資源的更新操做(insert、delete、update等);而在TCC中,是主業務活動請求(try)各個從業務服務預留資源。
2) 在階段2:
XA根據第一階段每一個RM是否都prepare成功,判斷是要提交仍是回滾。若是都prepare成功,那麼就commit每一個事務分支,反之則rollback每一個事務分支。
TCC中,若是在第一階段全部業務資源都預留成功,那麼confirm各個從業務服務,不然取消(cancel)全部從業務服務的資源預留請求。
TCC兩階段提交與XA兩階段提交的區別是:
XA是資源層面的分佈式事務,強一致性,在兩階段提交的整個過程當中,一直會持有資源的鎖。
XA事務中的兩階段提交內部過程是對開發者屏蔽的,回顧咱們以前講解JTA規範時,經過UserTransaction的commit方法來提交全局事務,這只是一次方法調用,其內部會委派給TransactionManager進行真正的兩階段提交,所以開發者從代碼層面是感知不到這個過程的。而事務管理器在兩階段提交過程當中,從prepare到commit/rollback過程當中,資源實際上一直都是被加鎖的。若是有其餘人須要更新這兩條記錄,那麼就必須等待鎖釋放。
TCC是業務層面的分佈式事務,最終一致性,不會一直持有資源的鎖。
TCC中的兩階段提交併無對開發者徹底屏蔽,也就是說從代碼層面,開發者是能夠感覺到兩階段提交的存在。如上述航班預約案例:在第一階段,航空公司須要提供try接口(機票資源預留)。在第二階段,航空公司提須要提供confirm/cancel接口(確認購買機票/取消預留)。開發者明顯的感知到了兩階段提交過程的存在。try、confirm/cancel在執行過程當中,通常都會開啓各自的本地事務,來保證方法內部業務邏輯的ACID特性。其中:
一、try過程的本地事務,是保證資源預留的業務邏輯的正確性。
二、confirm/cancel執行的本地事務邏輯確認/取消預留資源,以保證最終一致性,也就是所謂的補償型事務
(Compensation-Based Transactions)。
因爲是多個獨立的本地事務,所以不會對資源一直加鎖。
另外,這裏提到confirm/cancel執行的本地事務是補償性事務,關於什麼事補償性事務,atomikos 官網上有如下描述:
紅色框中的內容,是對補償性事務的解釋。大體含義是,"補償是一個獨立的支持ACID特性的本地事務,用於在邏輯上取消服務提供者上一個ACID事務形成的影響,對於一個長事務(long-running transaction),與其實現一個巨大的分佈式ACID事務,不如使用基於補償性的方案,把每一次服務調用當作一個較短的本地ACID事務來處理,執行完就當即提交」。
在這裏,筆者理解爲confirm和cancel就是補償事務,用於取消try階段本地事務形成的影響。由於第一階段try只是預留資源,以後必需要明確的告訴服務提供者,這個資源你到底要不要,對應第二階段的confirm/cancel。
提示:讀者如今應該明白爲何把TCC叫作兩階段補償性事務了,提交過程分爲2個階段,第二階段的confirm/cancel執行的事務屬於補償事務。
在介紹完TCC的基本概念以後,咱們再來比較一下TCC事務模型和DTP事務模型,以下所示:
這兩張圖看起來差異較大,實際上不少地方是相似的!
一、TCC模型中的主業務服務 至關於 DTP模型中的AP,TCC模型中的從業務服務 至關於 DTP模型中的RM
在DTP模型中,應用AP操做多個資源管理器RM上的資源;而在TCC模型中,是主業務服務操做多個從業務服務上的資源。例如航班預約案例中,美團App就是主業務服務,而川航和東航就是從業務服務,主業務服務須要使用從業務服務上的機票資源。不一樣的是DTP模型中的資源提供者是相似於Mysql這種關係型數據庫,而TCC模型中資源的提供者是其餘業務服務。
二、TCC模型中,從業務服務提供的try、confirm、cancel接口 至關於 DTP模型中RM提供的prepare、commit、rollback接口
XA協議中規定了DTP模型中定RM須要提供prepare、commit、rollback接口給TM調用,以實現兩階段提交。
而在TCC模型中,從業務服務至關於RM,提供了相似的try、confirm、cancel接口。
三、事務管理器
DTP模型和TCC模型中都有一個事務管理器。不一樣的是:
在DTP模型中,階段1的(prepare)和階段2的(commit、rollback),都是由TM進行調用的。
在TCC模型中,階段1的try接口是主業務服務調用(綠色箭頭),階段2的(confirm、cancel接口)是事務管理器TM調用(紅色箭頭)。這就是 TCC 分佈式事務模型的二階段異步化功能,從業務服務的第一階段執行成功,主業務服務就能夠提交完成,而後再由事務管理器框架異步的執行各從業務服務的第二階段。這裏犧牲了必定的隔離性和一致性的,可是提升了長事務的可用性。問題來了,既然第二階段是異步執行的,主業務服務怎麼知道異步執行的結果呢?發消息異步通知?返回一個id,後面讓業務去查?
TCC事務的優缺點:
優勢:XA兩階段提交資源層面的,而TCC實際上把資源層面二階段提交上提到了業務層面來實現。有效了的避免了XA兩階段提交佔用資源鎖時間過長致使的性能地下問題。
缺點:主業務服務和從業務服務都須要進行改造,從業務方改形成本更高。仍是航班預約案例,原來只須要提供一個購買接口,如今須要改形成try、confirm、canel3個接口,開發成本高。
提示:國內有一些關於TCC方案介紹的文章中,把TCC分紅三種類型:
通用型TCC,若是咱們上面介紹的TCC模型實例,從業務服務須要提供try、confirm、cancel
補償性TCC,從業務服務只須要提供 Do 和 Compensate 兩個接口
異步確保型 TCC,主業務服務的直接從業務服務是可靠消息服務,而真正的從業務服務則經過消息服務解耦,做爲消息服務的消費端,異步地執行。
關於這種劃分,筆者並不贊同,基於兩點:
一、筆者在Atomikos在官網上參考了多份資料,並無看到這種劃分,猜想應該是這些公司在內部實踐中,自行提出的概念。
二、對於上面所謂的"補償性TCC」、」異步確保型 TCC」,從業務服務不須要提供try、confirm、cancel三個接口,在這種狀況下,好像稱之爲TCC也不太合適。
經過前面的介紹,咱們基本已經掌握了TCC的工做原理。在本節中,筆者借用Atomikos官網提供的<<Tcc For Rest>>進行TCC案例講解。
須要注意的是,這個案例的主要目的是說明,在使用基於HTTP的REST服務中,TCC模型中各個參與方的API接口應該如何設計。一般一個TCC方案是不會依賴於底層通訊框架的,例如咱們也可使用業界比較火的spring cloud、dubbo等。這個時候,提供實現相似接口的功能就能夠了。
首先咱們總結一下TCC模型各個參與方須要提供的API:
Participant API:從業務服務須要提供的API,其須要提供try接口供主業務服務調用,須要提供confirm、cancel接口供事務管理器調用。這裏將從業務服務稱之爲Participant。
Transaction Coordinator API:事務管理/協調器須要提供的API,其須要提供事務日誌上報接口,讓主業務活動上報try階段各個從業務活動資源是否預留成功的信息
Application:主業務服務,其不須要提供任何接口,只須要操做上述 Participant、 Transaction Coordinator提供的接口便可。
熟悉的配方,熟悉的味道,<<Tcc For Rest>>採用的依然航班預約案例,以下圖所示:
其中:
Booking Proccess是主業務活動,處理機票預約業務
Swiss和easyjet是從業務服務,能夠理解爲兩個不一樣航空公司的機票預約系統
Transaction Coordinator是事務協調器,或者稱之爲事務管理器。
上圖中描述的總體流程以下所示:
1 Booking Proccess接受到一個須要中轉的航班預約請求(bookTrip)
-1.1:Booking Proccess向swiss發起機票預約請求 R1,其中/booking/A表示swis提供的預留機票資源的try接口
-1.2:Booking Proccess向easyjet發起機票預約請求 R2,其中/booking/B表示easyjet提供的預留機票資源的try接口
-1.3:Booking Proccess將請求1.一、1.2步驟中try的結果合併上報給Transaction Coordinator。
-1.3.1 Transaction Coordinator 向swiss發送確認執行業務操做的請求
-1.3.2 Transaction Coordinator 向easyjet發送確認執行業務操做的請求
從業務服務須要提供try、confirm和cancel三個接口,其中try接口是給主業務服務調用的,confirm和cancel是給事務協調器調用的。
try接口
在1.1和1.2步驟中,Booking Proccess向swiss和easyjet提供的try接口分別發起機票預留請求,swiss和easyjet做爲參與者,返回的響應格式以下所示
這裏返回的是一個JSON格式,事實上返回的格式什麼是無所謂的,不過Atomikos官方建議使用JSON格式。其中:
uri :表示的是稍後在Confirm確認執行業務操做時,須要調用的url
expires:表示截止時間,也就是說,若是超過這個時間依然沒有確認購買,那麼swiss和easyjet將會自動取消這個機票的預留
confirm接口
Transaction Coordinator判斷資源都預留成功,解析出json格式中的uri部分,向swiss和easyjet發送確認執行請求(Confirm),請求格式以下:
注意請求頭中Accept接受的MIME類型, 暗示了客戶端的語義指望。 這個並非強制的。
若是swiss和easyjet都確認執行成功,應該返回204,表示執行成功
須要注意的是,若是在截止時間(expires)後發送確認執行的請求,swiss和easyjet應該返回404
而Transaction Coordinator自身也應該有這種超時判斷,覺得較小的expires爲準,當超過這個時間時,就不該該發送confirm確認執行的請求。
而在expires以前若是確認執行失敗,Transaction Coordinator應該進行重試。
cancel接口(可選實現)
參與者能夠選擇是否顯式的提供cancel接口,若是提供了。Transaction Coordinator應該發送DELETE請求,告訴參與者取消資源預留,格式以下
若是取消成功,則返回
若是取消失敗,也不會影響結果。前面提到過,資源預留都有一個expires截止時間,超過這個截止時間,參與者就能夠主動取消這個預留的資源。
若是是由於超時,參與者自行取消資源預留的狀況下,應該返回
另外,因爲參與具有超時自動取消預留的功能,所以DELETE接口是可選的。若是參與者不提供DELETE接口來支持顯式cancel,能夠返回
不過筆者仍是建議顯式的提供cancel接口,例如,若是swiss預留成功,easyjet預留失敗。對於預留失敗的狀況,其實咱們已經沒有必要進行cancel了。可是swiss預留成功了,若是等待超時自動取消,可能會比較耗時,經過顯式提供cancel接口,來更快的取消預留的資源,將機票賣給其餘客戶。
TCC模型中,主業務服務須要將事務日誌上報給事務管理器/協調器,而後由協調器來調用從業務服務的confirm或者cancel接口。所以事務管理器/協調器必須提供一個事務日誌上報的接口。而本節就是介紹這個接口接受的參數類型和響應類型。
主業務服務在第一階段,調用各個從業務服務的try接口,而且將響應合併起來上傳報給Transaction Coordinator。考慮一下,這裏應該分爲2種狀況:
一、全部的try接口都調用成功了,所以主業務服務但願Transaction Coordinator向各個從業務服務進行confirm
二、try接口部分紅功,部分失敗。所以主業務服務但願Transaction Coordinator對已經try成功的從業務服務都進行cancel
所以對Transaction Coordinator來講,須要提供2個事務日誌上報接口:confirm接口、cancel接口.
confirm接口
請求格式以下:
而後協調器會對參與者逐個發起Confirm請求, 若是一切順利那麼將會返回以下結果
若是發起Confirm請求的時間太晚, 那麼意味着全部被動方都已經進行了超時補償
最糟糕的狀況就是有些參與者確認了, 可是有些就沒有. 這種狀況就應該要返回409, 這種狀況在Atomikos中定義爲啓發式異常
固然, 這種狀況應該儘可能地避免發生, 要求Confirm與Cancel實現冪等性, 出現差錯時協調器可屢次對參與者重試以儘可能下降啓發性異常發生的概率. 萬一409真的發生了, 則應該由請求方主動進行檢查或者由協調器返回給請求方詳細的執行信息, 例如對每一個參與者發起故障診斷的GET請求, 記錄故障信息並進行人工干預.
cancel接口
一個cancel請求跟confirm請求相似, 都是使用PUT請求, 惟一的區別是URI的不一樣
惟一可預見的響應就是
由於當預留資源沒有被確認時最後都會被釋放, 因此參與者返回其餘錯誤也不會影響最終一致性。