目錄
一、寫在前面
二、業務場景介紹
三、進一步思考
四、落地實現TCC分布式事務
(1)TCC實現階段一:Try
(2)TCC實現階段二:Confirm
(3)TCC實現階段三:Cancel
五、總結與思考
一、寫在前面
之前網上看到很多寫分布式事務的文章,不過大多都是將分布式事務各種技術方案簡單介紹一下。很多朋友看了不少文章,還是不知道分布式事務到底怎么回事,在項目里到底如何使用。
所以咱們這篇文章,就用大白話+手工繪圖,並結合一個電商系統的案例實踐,來給大家講清楚到底什么是TCC分布式事務。
首先說一下,這里可能會牽扯到一些Spring Cloud的原理,如果有不太清楚的同學,可以參考之前的文章:《拜托,面試請不要再問我Spring Cloud底層原理!》。
二、業務場景介紹
咱們先來看看業務場景,假設你現在有一個電商系統,里面有一個支付訂單的場景。
那對一個訂單支付之后,我們需要做下面的步驟:
-
更改訂單的狀態為“已支付”
-
扣減商品庫存
-
給會員增加積分
-
創建銷售出庫單通知倉庫發貨
這是一系列比較真實的步驟,無論大家有沒有做過電商系統,應該都能理解。
三、進一步思考
好,業務場景有了,現在我們要更進一步,實現一個TCC分布式事務的效果。
什么意思呢?也就是說,訂單服務-修改訂單狀態,庫存服務-扣減庫存,積分服務-增加積分,倉儲服務-創建銷售出庫單。
上述這幾個步驟,要么一起成功,要么一起失敗,必須是一個整體性的事務。
舉個例子,現在訂單的狀態都修改為“已支付”了,結果庫存服務扣減庫存失敗。那個商品的庫存原來是100件,現在賣掉了2件,本來應該是98件了。
結果呢?由於庫存服務操作數據庫異常,導致庫存數量還是100。這不是在坑人么,當然不能允許這種情況發生了!
但是如果你不用TCC分布式事務方案的話,就用個Spring Cloud開發這么一個微服務系統,很有可能會干出這種事兒來。
我們來看看下面的這個圖,直觀的表達了上述的過程。
所以說,我們有必要使用TCC分布式事務機制來保證各個服務形成一個整體性的事務。
上面那幾個步驟,要么全部成功,如果任何一個服務的操作失敗了,就全部一起回滾,撤銷已經完成的操作。
比如說庫存服務要是扣減庫存失敗了,那么訂單服務就得撤銷那個修改訂單狀態的操作,然后得停止執行增加積分和通知出庫兩個操作。
說了那么多,老規矩,給大家上一張圖,大伙兒順着圖來直觀的感受一下。
四、落地實現TCC分布式事務
那么現在到底要如何來實現一個TCC分布式事務,使得各個服務,要么一起成功?要么一起失敗呢?
大家稍安勿躁,我們這就來一步一步的分析一下。咱們就以一個Spring Cloud開發系統作為背景來解釋。
1、TCC實現階段一:Try
首先,訂單服務那兒,他的代碼大致來說應該是這樣子的:
如果你之前看過Spring Cloud架構原理那篇文章,同時對Spring Cloud有一定的了解的話,應該是可以理解上面那段代碼的。
其實就是訂單服務完成本地數據庫操作之后,通過Spring Cloud的Feign來調用其他的各個服務罷了。
但是光是憑借這段代碼,是不足以實現TCC分布式事務的啊?!兄弟們,別着急,我們對這個訂單服務修改點兒代碼好不好。
首先,上面那個訂單服務先把自己的狀態修改為:OrderStatus.UPDATING。
這是啥意思呢?也就是說,在pay()那個方法里,你別直接把訂單狀態修改為已支付啊!你先把訂單狀態修改為UPDATING,也就是修改中的意思。
這個狀態是個沒有任何含義的這么一個狀態,代表有人正在修改這個狀態罷了。
然后呢,庫存服務直接提供的那個reduceStock()接口里,也別直接扣減庫存啊,你可以是凍結掉庫存。
舉個例子,本來你的庫存數量是100,你別直接100 - 2 = 98,扣減這個庫存!
你可以把可銷售的庫存:100 - 2 = 98,設置為98沒問題,然后在一個單獨的凍結庫存的字段里,設置一個2。也就是說,有2個庫存是給凍結了。
積分服務的addCredit()接口也是同理,別直接給用戶增加會員積分。你可以先在積分表里的一個預增加積分字段加入積分。
比如:用戶積分原本是1190,現在要增加10個積分,別直接1190 + 10 = 1200個積分啊!
你可以保持積分為1190不變,在一個預增加字段里,比如說prepare_add_credit字段,設置一個10,表示有10個積分准備增加。
倉儲服務的saleDelivery()接口也是同理啊,你可以先創建一個銷售出庫單,但是這個銷售出庫單的狀態是“UNKNOWN”。
也就是說,剛剛創建這個銷售出庫單,此時還不確定他的狀態是什么呢!
上面這套改造接口的過程,其實就是所謂的TCC分布式事務中的第一個T字母代表的階段,也就是Try階段。
總結上述過程,如果你要實現一個TCC分布式事務,首先你的業務的主流程以及各個接口提供的業務含義,不是說直接完成那個業務操作,而是完成一個Try的操作。
這個操作,一般都是鎖定某個資源,設置一個預備類的狀態,凍結部分數據,等等,大概都是這類操作。
咱們來一起看看下面這張圖,結合上面的文字,再來捋一捋這整個過程。
2、TCC實現階段二:Confirm
然后就分成兩種情況了,第一種情況是比較理想的,那就是各個服務執行自己的那個Try操作,都執行成功了,bingo!
這個時候,就需要依靠TCC分布式事務框架來推動后續的執行了。
這里簡單提一句,如果你要玩兒TCC分布式事務,必須引入一款TCC分布式事務框架,比如國內開源的ByteTCC、himly、tcc-transaction。
否則的話,感知各個階段的執行情況以及推進執行下一個階段的這些事情,不太可能自己手寫實現,太復雜了。
如果你在各個服務里引入了一個TCC分布式事務的框架,訂單服務里內嵌的那個TCC分布式事務框架可以感知到,各個服務的Try操作都成功了。
此時,TCC分布式事務框架會控制進入TCC下一個階段,第一個C階段,也就是Confirm階段。
為了實現這個階段,你需要在各個服務里再加入一些代碼。
比如說,訂單服務里,你可以加入一個Confirm的邏輯,就是正式把訂單的狀態設置為“已支付”了,大概是類似下面這樣子:
庫存服務也是類似的,你可以有一個InventoryServiceConfirm類,里面提供一個reduceStock()接口的Confirm邏輯,這里就是將之前凍結庫存字段的2個庫存扣掉變為0。
這樣的話,可銷售庫存之前就已經變為98了,現在凍結的2個庫存也沒了,那就正式完成了庫存的扣減。
積分服務也是類似的,可以在積分服務里提供一個CreditServiceConfirm類,里面有一個addCredit()接口的Confirm邏輯,就是將預增加字段的10個積分扣掉,然后加入實際的會員積分字段中,從1190變為1120。
倉儲服務也是類似,可以在倉儲服務中提供一個WmsServiceConfirm類,提供一個saleDelivery()接口的Confirm邏輯,將銷售出庫單的狀態正式修改為“已創建”,可以供倉儲管理人員查看和使用,而不是停留在之前的中間狀態“UNKNOWN”了。
好了,上面各種服務的Confirm的邏輯都實現好了,一旦訂單服務里面的TCC分布式事務框架感知到各個服務的Try階段都成功了以后,就會執行各個服務的Confirm邏輯。
訂單服務內的TCC事務框架會負責跟其他各個服務內的TCC事務框架進行通信,依次調用各個服務的Confirm邏輯。然后,正式完成各個服務的所有業務邏輯的執行。
同樣,給大家來一張圖,順着圖一起來看看整個過程。
3、TCC實現階段三:Cancel
好,這是比較正常的一種情況,那如果是異常的一種情況呢?
舉個例子:在Try階段,比如積分服務吧,他執行出錯了,此時會怎么樣?
那訂單服務內的TCC事務框架是可以感知到的,然后他會決定對整個TCC分布式事務進行回滾。
也就是說,會執行各個服務的第二個C階段,Cancel階段。
同樣,為了實現這個Cancel階段,各個服務還得加一些代碼。
首先訂單服務,他得提供一個OrderServiceCancel的類,在里面有一個pay()接口的Cancel邏輯,就是可以將訂單的狀態設置為“CANCELED”,也就是這個訂單的狀態是已取消。
庫存服務也是同理,可以提供reduceStock()的Cancel邏輯,就是將凍結庫存扣減掉2,加回到可銷售庫存里去,98 + 2 = 100。
積分服務也需要提供addCredit()接口的Cancel邏輯,將預增加積分字段的10個積分扣減掉。
倉儲服務也需要提供一個saleDelivery()接口的Cancel邏輯,將銷售出庫單的狀態修改為“CANCELED”設置為已取消。
然后這個時候,訂單服務的TCC分布式事務框架只要感知到了任何一個服務的Try邏輯失敗了,就會跟各個服務內的TCC分布式事務框架進行通信,然后調用各個服務的Cancel邏輯。
大家看看下面的圖,直觀的感受一下。
五、總結與思考
好了,兄弟們,聊到這兒,基本上大家應該都知道TCC分布式事務具體是怎么回事了!
總結一下,你要玩兒TCC分布式事務的話:
首先需要選擇某種TCC分布式事務框架,各個服務里就會有這個TCC分布式事務框架在運行。
然后你原本的一個接口,要改造為3個邏輯,Try-Confirm-Cancel。
-
先是服務調用鏈路依次執行Try邏輯
-
如果都正常的話,TCC分布式事務框架推進執行Confirm邏輯,完成整個事務
-
如果某個服務的Try邏輯有問題,TCC分布式事務框架感知到之后就會推進執行各個服務的Cancel邏輯,撤銷之前執行的各種操作
這就是所謂的TCC分布式事務。
TCC分布式事務的核心思想,說白了,就是當遇到下面這些情況時,
-
某個服務的數據庫宕機了
-
某個服務自己掛了
-
那個服務的redis、elasticsearch、MQ等基礎設施故障了
-
某些資源不足了,比如說庫存不夠這些
先來Try一下,不要把業務邏輯完成,先試試看,看各個服務能不能基本正常運轉,能不能先凍結我需要的資源。
如果Try都ok,也就是說,底層的數據庫、redis、elasticsearch、MQ都是可以寫入數據的,並且你保留好了需要使用的一些資源(比如凍結了一部分庫存)。
接着,再執行各個服務的Confirm邏輯,基本上Confirm就可以很大概率保證一個分布式事務的完成了。
那如果Try階段某個服務就失敗了,比如說底層的數據庫掛了,或者redis掛了,等等。
此時就自動執行各個服務的Cancel邏輯,把之前的Try邏輯都回滾,所有服務都不要執行任何設計的業務邏輯。保證大家要么一起成功,要么一起失敗。
寫到這里,本文差不多該結束了。等一等,你有沒有想到一個問題?
如果有一些意外的情況發生了,比如說訂單服務突然掛了,然后再次重啟,TCC分布式事務框架是如何保證之前沒執行完的分布式事務繼續執行的呢?
所以,TCC事務框架都是要記錄一些分布式事務的活動日志的,可以在磁盤上的日志文件里記錄,也可以在數據庫里記錄。保存下來分布式事務運行的各個階段和狀態。
問題還沒完,萬一某個服務的Cancel或者Confirm邏輯執行一直失敗怎么辦呢?
那也很簡單,TCC事務框架會通過活動日志記錄各個服務的狀態。
舉個例子,比如發現某個服務的Cancel或者Confirm一直沒成功,會不停的重試調用他的Cancel或者Confirm邏輯,務必要他成功!
當然了,如果你的代碼沒有寫什么bug,有充足的測試,而且Try階段都基本嘗試了一下,那么其實一般Confirm、Cancel都是可以成功的!
最后,再給大家來一張圖,來看看給我們的業務,加上分布式事務之后的整個執行流程:
不少大公司里,其實都是自己研發TCC分布式事務框架的,專門在公司內部使用,比如我們就是這樣。
不過如果自己公司沒有研發TCC分布式事務框架的話,那一般就會選用開源的框架。
這里筆者給大家推薦幾個比較不錯的框架,都是咱們國內自己開源出去的:ByteTCC,tcc-transaction,himly。
大家有興趣的可以去他們的github地址,學習一下如何使用,以及如何跟Spring Cloud、Dubbo等服務框架整合使用。
只要把那些框架整合到你的系統里,很容易就可以實現上面那種奇妙的TCC分布式事務的效果了。
這種方案的應用場景:
這種方案說實話幾乎很少用人使用,我們用的也比較少,但是也有使用的場景。因為這個事務回滾實際上是嚴重依賴於你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之惡心。
比如說我們,一般來說跟錢相關的,跟錢打交道的,支付、交易相關的場景,我們會用TCC,嚴格嚴格保證分布式事務要么全部成功,要么全部自動回滾,嚴格保證資金的正確性,在資金上出現問題
比較適合的場景:這個就是除非你是真的一致性要求太高,是你系統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了,自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否ok,不ok就執行補償/回滾代碼。
而且最好是你的各個業務執行的時間都比較短。
但是說實話,一般盡量別這么搞,自己手寫回滾邏輯,或者是補償邏輯,實在太惡心了,那個業務代碼很難維護。