面試官:聊一下分布式事務(wù)!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
推薦:https://www.xttblog.com/?p=5160
概要
在微服務(wù)架構(gòu)盛行的情況下,在分布式的多個(gè)服務(wù)中保證業(yè)務(wù)的一致性,即分布式事務(wù)就顯得尤為重要。本文將講述分布式事務(wù)及其解決方案,有XA協(xié)議、TCC和Saga事務(wù)模型、本地消息表、事務(wù)消息和阿里開源的Seata。
分布式事務(wù)
聊什么是分布式事務(wù)前,先聊一下我們熟悉的單機(jī)事務(wù)。所謂單機(jī)事務(wù)是相對(duì)分布式事務(wù)來(lái)說(shuō)的,即數(shù)據(jù)庫(kù)事務(wù)。大家都知道數(shù)據(jù)庫(kù)事務(wù)有ACID這四個(gè)特性:
A(Atomicity):指單個(gè)事務(wù)中的操作要不都執(zhí)行,要不都不執(zhí)行
C(Consistency):指事務(wù)前后數(shù)據(jù)的完整性必須保持一致
I(Isolation):指多個(gè)事務(wù)對(duì)數(shù)據(jù)可見性的規(guī)則
D(Durability):指事務(wù)提交后,就會(huì)被永久存儲(chǔ)下來(lái)
既然數(shù)據(jù)庫(kù)事務(wù)有這四個(gè)特性的,那么分布式事務(wù)也不例外,應(yīng)該具備這四個(gè)特性。
在微服務(wù)架構(gòu)下,服務(wù)之間通過(guò)RPC遠(yuǎn)程調(diào)用,相對(duì)單機(jī)事務(wù)來(lái)說(shuō),多了“網(wǎng)絡(luò)通信”這一不確定因素,使得本來(lái)服務(wù)的調(diào)用只有“成功”和“失敗”這兩種返回結(jié)果,變?yōu)椤俺晒Α薄ⅰ笆 焙汀拔粗比N返回結(jié)果。系統(tǒng)之間的通信可靠性從單一系統(tǒng)中的可靠變成了微服務(wù)架構(gòu)之間的不可靠,分布式事務(wù)其實(shí)就是在不可靠的通信下實(shí)現(xiàn)事務(wù)的特性。一般因?yàn)榫W(wǎng)絡(luò)導(dǎo)致的異常可能有機(jī)器宕機(jī)、網(wǎng)絡(luò)異常、消息丟失、消息亂序、數(shù)據(jù)錯(cuò)誤、不可靠的TCP、存儲(chǔ)數(shù)據(jù)丟失、其他異常等等。
分布式事務(wù)方案
2PC/3PC
2PC即二階段提交) :
二階段提交(英語(yǔ):Two-phase Commit)是指在計(jì)算機(jī)網(wǎng)絡(luò)以及數(shù)據(jù)庫(kù)領(lǐng)域內(nèi),為了使基于分布式系統(tǒng)架構(gòu)下的所有節(jié)點(diǎn)在進(jìn)行事務(wù)提交時(shí)保持一致性而設(shè)計(jì)的一種算法。通常,二階段提交也被稱為是一種協(xié)議(Protocol)。
2PC是一種協(xié)議,它的作用保證在分布式系統(tǒng)中每個(gè)節(jié)點(diǎn)要不都提交事務(wù),要么都取消事務(wù)。這個(gè)跟ACID中的A原子性的定義很像。
2PC引入一個(gè)第三方的節(jié)點(diǎn)協(xié)調(diào)者,即Coordinator,其他參與事務(wù)的節(jié)點(diǎn)為參與者,即Participants。協(xié)調(diào)者統(tǒng)籌整個(gè)事務(wù)行為,負(fù)責(zé)通知參與者進(jìn)行Commit還是Rollback操作。
2PC的過(guò)程比較簡(jiǎn)單,分為兩個(gè)階段:
準(zhǔn)備階段
協(xié)調(diào)者分別給每個(gè)參與者發(fā)送Prepare消息,每個(gè)參與者收到消息后,進(jìn)行“預(yù)提交”操作(不是實(shí)際的提交操作),把操作的結(jié)果(成功或失敗)返回給協(xié)調(diào)者。
提交階段
協(xié)調(diào)者根據(jù)準(zhǔn)備階段收到的參與者的返回結(jié)果進(jìn)行判斷,如果所有的參與者都返回成功,那么分別給每個(gè)參與者發(fā)送Commit消息,否則發(fā)送Rollback消息。

2PC是一個(gè)強(qiáng)一致性協(xié)議,同時(shí)它在實(shí)際應(yīng)用中還存在幾個(gè)問(wèn)題
同步阻塞,2PC的兩個(gè)階段中,協(xié)調(diào)者和參與者的通信都是同步的,這會(huì)導(dǎo)致整個(gè)事務(wù)的長(zhǎng)時(shí)間阻塞
Coordinator的單點(diǎn)問(wèn)題
數(shù)據(jù)不一致,在Commit階段,可能存在只有部分參與者收到Commit消息(或處理成功)的情況
3PC
3PC即三階段提交,它比2PC多了一個(gè)階段,即把原來(lái)2PC的準(zhǔn)備階段拆分成CanCommit和PreCommit兩個(gè)階段,同時(shí)
引入超時(shí)機(jī)制來(lái)解決2PC的同步阻塞問(wèn)題。

但是在我看來(lái)3PC并沒(méi)有解決2PC的根本問(wèn)題,它只是在2PC的基礎(chǔ)上做了一些優(yōu)化,它增加了一個(gè)階段(也增加了1個(gè)RTT)來(lái)提高對(duì)方可用性的概率,這本質(zhì)跟TCP的三次握手一樣,同樣也改為四次握手,五次握手等等。
XA
XA是一種基于2PC協(xié)議實(shí)現(xiàn)的規(guī)范。在2PC中沒(méi)有明確資源是什么,以及資源是怎么提交的等等,而XA就是數(shù)據(jù)庫(kù)實(shí)現(xiàn)2PC的規(guī)范,已知常用的支持XA的關(guān)系型數(shù)據(jù)庫(kù)有Mysql、Oracle等。
本地消息表
本地消息表方案應(yīng)該是業(yè)界內(nèi)使用最為廣泛的,因?yàn)樗褂煤?jiǎn)單,成本比較低。
本地消息表的方案最初是由 eBay 提出(完整方案),核心思路是將分布式事務(wù)拆分成本地事務(wù)進(jìn)行處理。
它的處理流程如下:
事務(wù)發(fā)起方把要處理的業(yè)務(wù)事務(wù)和寫消息表這兩個(gè)操作放在同一個(gè)本地事務(wù)里
事務(wù)發(fā)起方有一個(gè)定時(shí)任務(wù)輪詢消息表,把沒(méi)處理的消息發(fā)送到消息中間件
事務(wù)被動(dòng)方從消息中間件獲取消息后,返回成功
事務(wù)發(fā)起方更新消息狀態(tài)為已成功

(網(wǎng)圖)
從處理流程來(lái)看,本地消息表方案是一個(gè)基于消息中間件的可靠性來(lái)達(dá)到事務(wù)的最終一致性的方案。
一些分析:
把業(yè)務(wù)處理和寫消息表放在同一個(gè)事務(wù)是為了失敗/異常后可以同時(shí)回滾
為什么不直接發(fā)消息,而是先寫消息表?
試想,如果發(fā)送消息超時(shí)了,即不確定消息中間件收到消息沒(méi),那么你是重試還是拋異常回滾事務(wù)呢?回滾是不行的,因?yàn)榭赡芟⒅虚g件已經(jīng)收到消息,接收方收到消息后做處理,導(dǎo)致雙方數(shù)據(jù)不一致了;重試也是不行的,因?yàn)橛锌赡軙?huì)一直重試失敗,導(dǎo)致事務(wù)阻塞。
基于上述分析,消息的接收方是需要做冪等操作的
本地消息表方案整體來(lái)說(shuō)還是比較簡(jiǎn)單、可用的,但是也有以下缺點(diǎn):
消息數(shù)據(jù)和業(yè)務(wù)數(shù)據(jù)耦合,消息表需要根據(jù)具體的業(yè)務(wù)場(chǎng)景制定,不能公用。就算可以公用消息表,對(duì)于分庫(kù)的業(yè)務(wù)來(lái)說(shuō)每個(gè)庫(kù)都是需要消息表的。
只適用于最終一致的業(yè)務(wù)場(chǎng)景。例如在 A -> B場(chǎng)景下,在不考慮網(wǎng)絡(luò)異常、宕機(jī)等非業(yè)務(wù)異常的情況下,A成功的話,B肯定也會(huì)成功的。
事務(wù)消息
事務(wù)消息是通過(guò)消息中間件來(lái)解耦本地消息表和業(yè)務(wù)數(shù)據(jù)表,適用于所有對(duì)數(shù)據(jù)最終一致性需求的場(chǎng)景。現(xiàn)在支持事務(wù)消息的消息中間件只有RocketMQ,這個(gè)概念最早也是RocketMQ提出的。
通過(guò)事務(wù)消息實(shí)現(xiàn)分布式事務(wù)的流程如下:
發(fā)起方發(fā)送半事務(wù)消息會(huì)給RocketMQ ,此時(shí)消息的狀態(tài)prepare,接受方還不能拉取到此消息
發(fā)起方進(jìn)行本地事務(wù)操作
發(fā)起方給RocketMQ確認(rèn)提交消息,此時(shí)接受方可以消費(fèi)到此消息了

步驟1和3失敗/異常該如何處理:
RocketMQ會(huì)定期掃描還沒(méi)確認(rèn)的消息,回調(diào)給發(fā)送方,詢問(wèn)此次事務(wù)的狀態(tài),根據(jù)發(fā)送方的返回結(jié)果把這條消息進(jìn)行取消還是提交確認(rèn)。
可以看出事務(wù)消息的本質(zhì)的借鑒了二階段提交的思想,它跟本地消息表的做法也很像,事務(wù)消息做的事情其實(shí)就是把消息表的存儲(chǔ)和掃描消息表這兩個(gè)事情放到消息中間件來(lái)做,使得消息表和業(yè)務(wù)表解耦。
TCC
TCC (Try-Confirm-Cancel)事務(wù)模型采用的是補(bǔ)償機(jī)制,其核心思想是:針對(duì)每個(gè)操作,都要注冊(cè)一個(gè)與其對(duì)應(yīng)的確認(rèn)和補(bǔ)償操作。
相當(dāng)于XA來(lái)說(shuō),TCC可以不依賴于資源管理器,即數(shù)據(jù)庫(kù),它是通過(guò)業(yè)務(wù)邏輯來(lái)控制確認(rèn)和補(bǔ)償操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一個(gè)應(yīng)用層面的2PC。
TCC分為三個(gè)階段:
Try階段,對(duì)業(yè)務(wù)資源進(jìn)行檢測(cè)和預(yù)留
Confirm階段,對(duì)Try階段預(yù)留的資源進(jìn)行確認(rèn)提交,Try階段執(zhí)行成功是Confirm階段執(zhí)行成功的前提
Cancel階段,對(duì)Try階段預(yù)留的資源進(jìn)行撤銷或釋放

看上去TCC跟2PC/3PC可能有點(diǎn)像,但是TCC強(qiáng)調(diào)的是補(bǔ)償,而且對(duì)于對(duì)資源的“預(yù)留”,“確認(rèn)”,“釋放”,TCC并沒(méi)有明確說(shuō)要如何做,這個(gè)具體是要業(yè)務(wù)來(lái)定義的。
例如在轉(zhuǎn)賬的場(chǎng)景,“預(yù)留”操作可能就是對(duì)賬號(hào)里的部分資金進(jìn)行凍結(jié),這樣這個(gè)資金只能是當(dāng)前事務(wù)才能用,別的事務(wù)用不了。
另外,對(duì)于異常的場(chǎng)景,TCC也沒(méi)有說(shuō)要怎么做,因?yàn)門ry、Confirm、Cancel都是業(yè)務(wù)定義的,這三個(gè)階段中發(fā)生了異常,那么就由業(yè)務(wù)來(lái)做相應(yīng)的處理。一般都有以下幾種處理:
如果Try成功了,那么Confirm階段異常了就一直重試,直到成功
Try、Confirm、Cancel三個(gè)階段都有相應(yīng)的資源及事務(wù)日志,應(yīng)用根據(jù)日志(異步)來(lái)做重試或補(bǔ)償
TCC的實(shí)現(xiàn)依賴底層數(shù)據(jù)庫(kù),異常后直接利用數(shù)據(jù)庫(kù)的事務(wù)機(jī)制回滾
其中現(xiàn)在使用比較多的TCC框架ByteTCC、tcc-transaction的原理都是基于第三點(diǎn)
同時(shí),在實(shí)現(xiàn)TCC時(shí)要注意以下三個(gè)問(wèn)題
允許空回滾
在Try沒(méi)有真正執(zhí)行的情況下,觸發(fā)了Cancel操作,這時(shí)要允許Cancel成功
防懸掛控制
Cancel操作比Try操作先執(zhí)行(網(wǎng)絡(luò)延遲原因),后面的Try操作不能執(zhí)行成功
冪等控制
TCC其實(shí)是把控制事務(wù)的邏輯放在業(yè)務(wù)應(yīng)用層面,而非資源管理器,這樣實(shí)現(xiàn)起來(lái)就會(huì)相對(duì)靈活很多,但相對(duì)對(duì)數(shù)據(jù)一致性的保證可能沒(méi)那么強(qiáng)(具體看怎么實(shí)現(xiàn)Try),整體來(lái)說(shuō)TCC還有以下缺點(diǎn):
對(duì)于Confirm和Cancel階段失敗后要完全靠業(yè)務(wù)應(yīng)用自己去處理
每個(gè)業(yè)務(wù)都需要實(shí)現(xiàn)Try、Confirm、Cancel三個(gè)接口,代碼量比較多
如果是基于現(xiàn)有的業(yè)務(wù)想使用TCC會(huì)比較困難。一是對(duì)于原來(lái)的接口要拆分為三個(gè)接口,入侵性比較大;二是因?yàn)橐觥邦A(yù)留”資源的操作,有可能需要對(duì)原來(lái)的業(yè)務(wù)模型進(jìn)行改造。
Saga
Saga事務(wù)模型又叫做長(zhǎng)時(shí)間運(yùn)行的事務(wù)(Long-running-transaction), 它是由普林斯頓大學(xué)的H.Garcia-Molina等人提出,它描述的是另外一種在沒(méi)有兩階段提交的的情況下解決分布式系統(tǒng)中復(fù)雜的業(yè)務(wù)事務(wù)問(wèn)題。Saga的論文。
該模型其核心思想就是拆分分布式系統(tǒng)中的長(zhǎng)事務(wù)為多個(gè)短事務(wù),或者叫多個(gè)本地事務(wù),然后由 Saga工作流引擎負(fù)責(zé)協(xié)調(diào),如果整個(gè)流程正常結(jié)束,那么就算是業(yè)務(wù)成功完成,如果在這過(guò)程中實(shí)現(xiàn)失敗,那么Saga工作流引擎就會(huì)以相反的順序調(diào)用補(bǔ)償操作,重新進(jìn)行業(yè)務(wù)回滾。
Saga也是一種補(bǔ)償協(xié)議,在 Saga 模式下,分布式事務(wù)內(nèi)有多個(gè)參與者,每一個(gè)參與者都是一個(gè)沖正補(bǔ)償服務(wù),需要用戶根據(jù)業(yè)務(wù)場(chǎng)景實(shí)現(xiàn)其正向操作和逆向回滾操作。

你可以看到Saga跟TCC很像,但是Saga更加寬松,一致性更弱,在Saga看來(lái),在一階段直接做提交/確認(rèn)操作就好了,有問(wèn)題再做補(bǔ)償。這樣的話,Saga可以擁有比XA和TCC更好的性能(XA、TCC需要鎖定資源或預(yù)留資源),而且Saga強(qiáng)調(diào)通過(guò)事件驅(qū)動(dòng)異步處理,實(shí)現(xiàn)高吞吐。
可以看出Saga是對(duì)TCC的一種“妥協(xié)”,從TCC的三個(gè)接口變?yōu)閮蓚€(gè)接口,一階段直接提交缺少對(duì)資源的隔離(如果一階段提交后,后面發(fā)現(xiàn)需要做補(bǔ)償,但是補(bǔ)償操作執(zhí)行前有另外的事務(wù)更改了數(shù)據(jù),這時(shí)數(shù)據(jù)已經(jīng)變“臟”了,那么這時(shí)該如何處理是一個(gè)問(wèn)題。在TCC沒(méi)有這個(gè)問(wèn)題,因?yàn)橘Y源已經(jīng)被hold住了),因此對(duì)使用者也是比較寬松的,對(duì)于現(xiàn)有業(yè)務(wù)的改造也會(huì)比較簡(jiǎn)單。
Saga實(shí)現(xiàn)分兩種,一種是Saga狀態(tài)機(jī)實(shí)現(xiàn),一種是Saga AOP Proxy實(shí)現(xiàn)。Saga狀態(tài)機(jī)實(shí)現(xiàn),在關(guān)于參與者服務(wù)編排實(shí)現(xiàn)又有集中式和協(xié)同式兩種分支。這點(diǎn)就不展開了。
TCC vs Saga
TCC和Saga都屬于補(bǔ)償型事務(wù)模型,Saga沒(méi)有Try,直接Commit,所有會(huì)產(chǎn)生實(shí)際的事務(wù)痕跡,而補(bǔ)償做的是反向操作。TCC是二階段的廣義實(shí)現(xiàn),利用了數(shù)據(jù)的中間態(tài),Cancel是中間狀態(tài)的數(shù)據(jù)進(jìn)行撤銷,從而不存在數(shù)據(jù)污染問(wèn)題。
使用場(chǎng)景對(duì)比:
TCC 適用于執(zhí)行時(shí)間確定且較短、對(duì)一致性要求比較高、數(shù)據(jù)隔離強(qiáng)的業(yè)務(wù)
Saga 適用于業(yè)務(wù)流程長(zhǎng)、業(yè)務(wù)流程多的業(yè)務(wù),在銀行業(yè)金融機(jī)構(gòu)使用廣泛
TCC 對(duì)現(xiàn)有業(yè)務(wù)改造較大,Saga則相對(duì)少點(diǎn)
Seata
Seata是一個(gè)由阿里做背書的分布式事務(wù)框架,致力于提供高性能和簡(jiǎn)單易用的分布式事務(wù)服務(wù)。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務(wù)模式,為用戶打造一站式的分布式解決方案。
AT模式
AT模式是Seata通過(guò)攔截、解釋用戶的SQL,對(duì)業(yè)務(wù)數(shù)據(jù)進(jìn)行加鎖、回滾等操作的基于二階段協(xié)議的一個(gè)實(shí)現(xiàn)。
它的特點(diǎn)是對(duì)業(yè)務(wù)無(wú)入侵,用戶只需關(guān)注自己的“業(yè)務(wù) SQL”,用戶的 “業(yè)務(wù) SQL” 作為一階段,Seata 框架會(huì)自動(dòng)生成事務(wù)的二階段提交和回滾操作。
在一階段,Seata 會(huì)攔截“業(yè)務(wù) SQL”,首先解析 SQL 語(yǔ)義,找到“業(yè)務(wù) SQL”要更新的業(yè)務(wù)數(shù)據(jù),在業(yè)務(wù)數(shù)據(jù)被更新前,將其保存成“before image”,然后執(zhí)行“業(yè)務(wù) SQL”更新業(yè)務(wù)數(shù)據(jù),在業(yè)務(wù)數(shù)據(jù)更新之后,再將其保存成“after image”,最后生成行鎖。以上操作全部在一個(gè)數(shù)據(jù)庫(kù)事務(wù)內(nèi)完成,這樣保證了一階段操作的原子性。
二階段如果是提交的話,因?yàn)椤皹I(yè)務(wù) SQL”在一階段已經(jīng)提交至數(shù)據(jù)庫(kù), 所以 Seata 框架只需將一階段保存的快照數(shù)據(jù)和行鎖刪掉,完成數(shù)據(jù)清理即可。

TCC模式
Seata的TCC模式跟上面講的TCC事務(wù)模型差不多

Saga模式
Saga模式也是上面講的Saga事務(wù)模型差不多。在Seata中對(duì)服務(wù)的編排引入了狀態(tài)機(jī)引擎, 使得對(duì)業(yè)務(wù)流程的定義更加標(biāo)準(zhǔn)化,提高可讀性,不過(guò)相對(duì)來(lái)說(shuō)配置會(huì)比較復(fù)雜繁瑣。同時(shí)支持注解的方式,這個(gè)在開發(fā)上會(huì)簡(jiǎn)單一點(diǎn),但功能可能少一點(diǎn)。
分布式事務(wù)一致性與Paxos一致性的思考
首先要明確一點(diǎn)的就是對(duì)于上述提到的分布式事務(wù)解決方案,如TCC、Saga、本地消息表等,其本質(zhì)都是2PC。
Paxos算法解決的問(wèn)題是一個(gè)分布式系統(tǒng)如何就某個(gè)值(決議)達(dá)成一致。
咋看起來(lái)2PC和Paxos都是解決關(guān)于“一致性”的問(wèn)題,其實(shí)細(xì)想它們解決的問(wèn)題不在一個(gè)層面。
2PC要求分布式系統(tǒng)中的每個(gè)節(jié)點(diǎn)要不全部成功,要不全部失敗,強(qiáng)調(diào)的是原子性。
Paxos要求多個(gè)副本之間的數(shù)據(jù)一致性,其實(shí)這里用“一致性”并不準(zhǔn)確,應(yīng)該用“共識(shí)(Consensus)”才對(duì)。
例如2PC中的協(xié)調(diào)者單點(diǎn)的問(wèn)題可以用Paxos算法通過(guò)選舉出新的協(xié)調(diào)者來(lái)解決。
總結(jié)
總得看來(lái),分布式事務(wù)的解決方案都很難做到有高一致性的同時(shí),也有高性能,同時(shí)在實(shí)現(xiàn)上也有一定的難度。在業(yè)務(wù)允許的情況下,我們通常處理分布式事務(wù)的一般原則應(yīng)是:業(yè)務(wù)規(guī)避 > 最終一致 > 強(qiáng)一致。
參考資料
https://www.sofastack.tech/blog/sofa-meetup-3-seata-retrospect/
https://juejin.im/post/5b5a0bf9f265da0f6523913b
https://www.zhihu.com/question/275845393
