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