6.0 柔性事務 :TCC兩階段補償型
2018-02-05 03:12:04 22,771 12
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商標。
國內最早關於TCC的報道,應該是InfoQ上對阿里程立博士的一篇採訪。經過程博士的這一次傳道之後,TCC在國內逐漸被大家廣爲了解並接受。
Atomikos公司在商業版本事務管理器ExtremeTransactions中提供了TCC方案的實現,但是由於其是收費的,因此相應的很多的開源實現方案也就涌現出來,如:tcc-transaction、ByteTCC、spring-cloud-rest-tcc。
TCC的作用主要是解決跨服務調用場景下的分佈式事務問題,在本文中,筆者將先介紹一個跨服務的場景案例,並分析其中存在的分佈式事務問題;然後介紹TCC的基本概念以及其是如何解決這個問題的。
場景案例
Atomikos官網上<<Composite Transactions for SOA>>一文中,以航班預定的案例,來介紹TCC要解決的事務場景。在這裏筆者虛構一個完全相同的場景,把自己當做航班預定的主人公,來介紹這個案例。事實上,你可以把本案例當做官方文檔案例的一個翻譯,只不過把地點從Brussels-->Toronto-->Washington,改成從合肥-->昆明-->大理。
有一次,筆者買彩票中獎了(純屬虛構),準備從合肥出發,到雲南大理去遊玩,然後使用美團App(機票代理商)來訂機票。發現沒有從合肥直達大理的航班,需要到昆明進行中轉。如下圖:
從圖中我們可以看出來,從合肥到昆明乘坐的是四川航空,從昆明到大理乘坐的是東方航空。
由於使用的是美團App預定,當我選擇了這種航班預定方案後,美團App要去四川航空和東方航空各幫我購買一張票。如下圖:
考慮最簡單的情況:美團先去川航幫我買票,如果買不到,那麼東航也沒必要買了。如果川航購買成功,再去東航購買另一張票。
現在問題來了:假設美團先從川航成功買到了票,然後去東航買票的時候,因爲天氣問題,東航航班被取消了。那麼此時,美團必須取消川航的票,因爲只有一張票是沒用的,不取消就是浪費我的錢。那麼如果取消會怎樣呢?如果讀者有取消機票經歷的話,非正常退票,肯定要扣手續費的。在這裏,川航本來已經購買成功,現在因爲東航的原因要退川航的票,川航應該是要扣代理商的錢的。
那麼美團就要保證,如果任一航班購買失敗,都不能扣錢,怎麼做呢?
兩個航空公司都爲美團提供以下3個接口:機票預留接口、確認接口、取消接口。美團App分2個階段進行調用,如下所示:
在第1階段:
美團分別請求兩個航空公司預留機票,兩個航空公司分別告訴美圖預留成功還是失敗。航空公司需要保證,機票預留成功的話,之後一定能購買到。
在第2階段:
如果兩個航空公司都預留成功,則分別向兩個公司發送確認購買請求。
如果兩個航空公司任意一個預留失敗,則對於預留成功的航空公司也要取消預留。這種情況下,對於之前預留成功機票的航班取消,也不會扣用戶的錢,因爲購買並沒實際發生,之前只是請求預留機票而已。
通過這種方案,可以保證兩個航空公司購買機票的一致性,要不都成功,要不都失敗,即使失敗也不會扣用戶的錢。如果在兩個航班都已經已經確認購買後,再退票,那肯定還是要扣錢的。
當然,實際情況肯定這裏提到的肯定要複雜,通常航空公司在第一階段,對於預留的機票,會要求在指定的時間必須確認購買(支付成功),如果沒有及時確認購買,會自動取消。假設川航要求10分鐘內支付成功,東航要求30分鐘內支付成功。以較短的時間算,如果用戶在10分鐘內支付成功的話,那麼美團會向兩個航空公司都發送確認購買的請求,如果超過10分鐘(以較短的時間爲準),那麼就不能進行支付。
再次強調,這個案例,可以算是<<Composite Transactions for SOA>>中航班預定案例的漢化版。而實際美團App是如何實現這種需要中轉的航班預定需求,筆者並不知情。
另外,注意這只是一個案例場景,實際情況中,你是很難去驅動航空公司進行接口改造的。
Whatever,這個方案提供給我們一種跨服務條用保證事務一致性的一種解決思路,可以把這種方案當做TCC的雛形。
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特性。其中:
1、try過程的本地事務,是保證資源預留的業務邏輯的正確性。
2、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事務模型 VS DTP事務模型
在介紹完TCC的基本概念之後,我們再來比較一下TCC事務模型和DTP事務模型,如下所示:
這兩張圖看起來差別較大,實際上很多地方是類似的!
1、TCC模型中的主業務服務 相當於 DTP模型中的AP,TCC模型中的從業務服務 相當於 DTP模型中的RM
在DTP模型中,應用AP操作多個資源管理器RM上的資源;而在TCC模型中,是主業務服務操作多個從業務服務上的資源。例如航班預定案例中,美團App就是主業務服務,而川航和東航就是從業務服務,主業務服務需要使用從業務服務上的機票資源。不同的是DTP模型中的資源提供者是類似於Mysql這種關係型數據庫,而TCC模型中資源的提供者是其他業務服務。
2、TCC模型中,從業務服務提供的try、confirm、cancel接口 相當於 DTP模型中RM提供的prepare、commit、rollback接口
XA協議中規定了DTP模型中定RM需要提供prepare、commit、rollback接口給TM調用,以實現兩階段提交。
而在TCC模型中,從業務服務相當於RM,提供了類似的try、confirm、cancel接口。
3、事務管理器
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,主業務服務的直接從業務服務是可靠消息服務,而真正的從業務服務則通過消息服務解耦,作爲消息服務的消費端,異步地執行。
關於這種劃分,筆者並不贊同,基於兩點:
1、筆者在Atomikos在官網上參考了多份資料,並沒有看到這種劃分,猜測應該是這些公司在內部實踐中,自行提出的概念。
2、對於上面所謂的"補償性TCC”、”異步確保型 TCC”,從業務服務不需要提供try、confirm、cancel三個接口,在這種情況下,好像稱之爲TCC也不太合適。
TCC For REST案例
通過前面的介紹,我們基本已經掌握了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、1.2步驟中try的結果合併上報給Transaction Coordinator。
-1.3.1 Transaction Coordinator 向swiss發送確認執行業務操作的請求
-1.3.2 Transaction Coordinator 向easyjet發送確認執行業務操作的請求
Participant API
從業務服務需要提供try、confirm和cancel三個接口,其中try接口是給主業務服務調用的,confirm和cancel是給事務協調器調用的。
try接口
在1.1和1.2步驟中,Booking Proccess向swiss和easyjet提供的try接口分別發起機票預留請求,swiss和easyjet作爲參與者,返回的響應格式如下所示
- {"participantLink":
- {"uri":"http://www.example.com/part/123",
- "expires":"2014-01-11T10:15:54Z"
- }
- }
這裏返回的是一個JSON格式,事實上返回的格式什麼是無所謂的,不過Atomikos官方建議使用JSON格式。其中:
uri :表示的是稍後在Confirm確認執行業務操作時,需要調用的url
expires:表示截止時間,也就是說,如果超過這個時間依然沒有確認購買,那麼swiss和easyjet將會自動取消這個機票的預留
confirm接口
Transaction Coordinator判斷資源都預留成功,解析出json格式中的uri部分,向swiss和easyjet發送確認執行請求(Confirm),請求格式如下:
- PUT /part/123 HTTP/1.1
- Host: www.example.com
- Accept: application/tcc
注意請求頭中Accept接受的MIME類型, 暗示了客戶端的語義期望。 這個並不是強制的。
如果swiss和easyjet都確認執行成功,應該返回204,表示執行成功
- HTTP/1.1 204 No Content
需要注意的是,如果在截止時間(expires)後發送確認執行的請求,swiss和easyjet應該返回404
- HTTP/1.1 404 Not Found
而Transaction Coordinator自身也應該有這種超時判斷,以爲較小的expires爲準,當超過這個時間時,就不應該發送confirm確認執行的請求。
而在expires之前如果確認執行失敗,Transaction Coordinator應該進行重試。
cancel接口(可選實現)
參與者可以選擇是否顯式的提供cancel接口,如果提供了。Transaction Coordinator應該發送DELETE請求,告訴參與者取消資源預留,格式如下
- DELETE /part/123 HTTP/1.1
- Host: www.example.com
- Accept: application/tcc
如果取消成功,則返回
- HTTP/1.1 204 No Content
如果取消失敗,也不會影響結果。前面提到過,資源預留都有一個expires截止時間,超過這個截止時間,參與者就可以主動取消這個預留的資源。
如果是因爲超時,參與者自行取消資源預留的情況下,應該返回
- HTTP/1.1 404 Not Found
另外,由於參與具備超時自動取消預留的功能,因此DELETE接口是可選的。如果參與者不提供DELETE接口來支持顯式cancel,可以返回
- HTTP/1.1 405 Method Not Allowed
不過筆者還是建議顯式的提供cancel接口,例如,如果swiss預留成功,easyjet預留失敗。對於預留失敗的情況,其實我們已經沒有必要進行cancel了。但是swiss預留成功了,如果等待超時自動取消,可能會比較耗時,通過顯式提供cancel接口,來更快的取消預留的資源,將機票賣給其他客戶。
Transaction Coordinator API
TCC模型中,主業務服務需要將事務日誌上報給事務管理器/協調器,然後由協調器來調用從業務服務的confirm或者cancel接口。因此事務管理器/協調器必須提供一個事務日誌上報的接口。而本節就是介紹這個接口接受的參數類型和響應類型。
主業務服務在第一階段,調用各個從業務服務的try接口,並且將響應合併起來上傳報給Transaction Coordinator。考慮一下,這裏應該分爲2種情況:
1、所有的try接口都調用成功了,因此主業務服務希望Transaction Coordinator向各個從業務服務進行confirm
2、try接口部分成功,部分失敗。因此主業務服務希望Transaction Coordinator對已經try成功的從業務服務都進行cancel
因此對Transaction Coordinator來說,需要提供2個事務日誌上報接口:confirm接口、cancel接口.
confirm接口
請求格式如下:
- PUT /coordinator/confirm HTTP/1.1
- Host: www.taas.com
- Content-Type: application/tcc+json
- {
- "participantLinks": [
- {
- "uri": "http://www.example.com/part1",
- "expires": "2014-01-11T10:15:54Z"
- },
- {
- "uri": "http://www.example.com/part2",
- "expires": "2014-01-11T10:15:54+01:00"
- }
- ]
- }
然後協調器會對參與者逐個發起Confirm請求, 如果一切順利那麼將會返回如下結果
- HTTP/1.1 204 No Content
如果發起Confirm請求的時間太晚, 那麼意味着所有被動方都已經進行了超時補償
- HTTP/1.1 404 Not Found
最糟糕的情況就是有些參與者確認了, 但是有些就沒有. 這種情況就應該要返回409, 這種情況在Atomikos中定義爲啓發式異常
- HTTP/1.1 409 Conflict
當然, 這種情況應該儘量地避免發生, 要求Confirm與Cancel實現冪等性, 出現差錯時協調器可多次對參與者重試以儘量降低啓發性異常發生的機率. 萬一409真的發生了, 則應該由請求方主動進行檢查或者由協調器返回給請求方詳細的執行信息, 例如對每個參與者發起故障診斷的GET請求, 記錄故障信息並進行人工干預.
cancel接口
一個cancel請求跟confirm請求類似, 都是使用PUT請求, 唯一的區別是URI的不同
唯一可預見的響應就是
- HTTP/1.1 204 No Content
因爲當預留資源沒有被確認時最後都會被釋放, 所以參與者返回其他錯誤也不會影響最終一致性。
推薦閱讀資料:
開源TCC實現方案
Atomikos的官方文檔
<<Composite Transactions for SOA>>
<< Transaction management API for REST: TCC>>
<<Atomikos ExtremeTransactions Guide>>