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

拜托,面試請不要再問我TCC分佈式事務的實現原理!

(給ImportNew加星標,提高Java技能)

 

本文來源:石杉的架構筆記(ID:shishan100)

 

一、寫在前面

 

之前網上看到很多寫分佈式事務的文章,不過大多都是將分佈式事務各種技術方案簡單介紹一下。很多朋友看了不少文章,還是不知道分佈式事務到底怎麼回事,在專案里到底如何使用。

 

所以咱們這篇文章,就用大白話+手工繪圖,並結合一個電商系統的案例實踐,來給大家講清楚到底什麼是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分佈式事務的效果了。

 

如有收穫,請幫忙轉發,謝謝!

 

作者:中華石杉,十餘年BAT架構經驗傾囊相授。

個人微信公眾號:石杉的架構筆記(ID:shishan100)

    赞(0)

    分享創造快樂