面試官問我:分布式事務(wù)是什么?


事務(wù)其實大家應(yīng)該不陌生,尤其是對于程序員來說,如果你連事務(wù)都沒聽說過,沒關(guān)系,因為你遇到了聰明和才智于一體的我,事務(wù)其實就是為了處理多種混合操作,涉及到多方面業(yè)務(wù)的情景
重點是事務(wù)應(yīng)用的場景就是為了解決多種事務(wù)必須要么同時完成,要么同時不能完成的場景,也就是做到真正意義上的"同生共死"
嚴(yán)格意義上來說事務(wù)其實具有原子性、一致性、隔離性和持久性四種特性,也就是大家老生常談的ACID
原子性(Atomicity),可以理解為一個事務(wù)內(nèi)的所有操作要么都執(zhí)行,要么都不執(zhí)行
一致性(Consistency),可以理解為數(shù)據(jù)是滿足完整性約束的,也就是不會存在中間狀態(tài)的數(shù)據(jù),比如你賬上有400,我賬上有100,你給我打200塊,此時你賬上的錢應(yīng)該是200,我賬上的錢應(yīng)該是300,不會存在我賬上錢加了,你賬上錢沒扣的中間狀態(tài)
隔離性(Isolation),指的是多個事務(wù)并發(fā)執(zhí)行的時候不會互相干擾,即一個事務(wù)內(nèi)部的數(shù)據(jù)對于其他事務(wù)來說是隔離的
持久性(Durability),指的是一個事務(wù)完成了之后數(shù)據(jù)就被永遠(yuǎn)保存下來,之后的其他操作或故障都不會對事務(wù)的結(jié)果產(chǎn)生影響
嚴(yán)格意義上來說事務(wù)其實具有原子性、一致性、隔離性和持久性四種特性,也就是大家老生常談的ACID
其實在我們印象中,應(yīng)該對這個事務(wù)再熟悉不過了,大家都知道事務(wù)就是為了使得一些數(shù)據(jù)庫層面的更新操作要么全部成功,要么全部失敗。
不知道大家學(xué)過Redis沒有,如果學(xué)過Redis的其實可能會有疑問,因為Redis的事務(wù)不能保證所有操作要么都執(zhí)行,要么都不執(zhí)行,但是也叫做事務(wù)。Redis其實在官網(wǎng)就已經(jīng)說明白了,官網(wǎng)中告訴大家事務(wù)中的某個命令失敗了,之后的命令還是會被處理,Redis不會停止執(zhí)行命令,也就是意味著不會回滾
Redis解釋為什么不支持回滾
他們給出的回的就是首先如果命令出錯那就是語法的錯誤,是屬于個人的編程錯誤,而且這種情況應(yīng)該被檢測出來,而不是在生產(chǎn)環(huán)境出現(xiàn),于是乎Redis為了速度更快不支持回滾操作
感覺很有道理的樣子,但是又有點不對勁

好了,這下大家都知道事務(wù)是啥了,那么我們一起來看看分布式事務(wù)吧


剛才說的事務(wù)都是屬于單體程序中,單機中這樣是沒問題的,通過普通的事務(wù)操作就可以來解決;當(dāng)我們的系統(tǒng)逐漸變大,日益變強的同時,并發(fā)量和系統(tǒng)都隨之而增加,當(dāng)涉及到多個系統(tǒng)之間的配合來完成一個事務(wù)的時候,這就比較難辦了,因為無法直接通過一個系統(tǒng)的數(shù)據(jù)庫來完成
假設(shè)現(xiàn)在有訂單系統(tǒng)、扣款系統(tǒng)、積分系統(tǒng),這是屬于三個系統(tǒng),也就是分別在不同的數(shù)據(jù)庫中,但是我需要保證三個系統(tǒng)中的服務(wù)要么全部成功、要么全部失敗,其實像這種設(shè)計到多個庫、多個系統(tǒng)之間的事務(wù)操作,也就是分布式事務(wù)了
分布式事務(wù)其實說簡單也簡單,其實就是有多個本地事務(wù)組合而成,對于分布式事務(wù)而言幾乎滿足不了ACID,其實對于單機事務(wù)大多是情況下也是無法全部滿足ACID的,否則哪里來的四種隔離級別?所以更別說分布在不同數(shù)據(jù)庫、不同系統(tǒng)之間的分布式事務(wù)了
分布式事務(wù)大致可以分為六種,但是其實這六種又可以按照三種思想來分類,接下來一起看看吧
2PC和3PC是一種強一致性事務(wù),不過還是有數(shù)據(jù)的不一致、阻塞等風(fēng)險,而且只能應(yīng)用在數(shù)據(jù)庫層面;而TCC是一種補償性事務(wù)的思想,適用的范圍應(yīng)該是比較廣,不過這種補償性機制一般對業(yè)務(wù)的侵入性比較大,每一個操作都需要實現(xiàn)對應(yīng)的三種方法;還有一種思想就是努力實現(xiàn)最終一致性事務(wù),有本地消息、事務(wù)消息、和最大努力通知這三種方法,都是實現(xiàn)最終一致性事務(wù),因此適用于于一些對于時間不敏感的業(yè)務(wù)
大致了解了這三類,接下來來細(xì)細(xì)學(xué)習(xí)每一種吧
2PC二階段提交:準(zhǔn)備階段、提交階段
2PC,又叫做二階段提交,二階段指的是準(zhǔn)備階段和提交兩個階段
二階段提交屬于一種強一致性的設(shè)計,2PC引入一個事務(wù)協(xié)調(diào)者的角色來協(xié)調(diào)管理各參與者的提交和回滾機制,我們來看下具體流程
準(zhǔn)備階段協(xié)調(diào)者會向各個參與者發(fā)送準(zhǔn)備的命令,這個準(zhǔn)備其實就是準(zhǔn)備環(huán)境,可以理解成提交之前的準(zhǔn)備工作
同步的等待所有的資源的響應(yīng)之后,就到了萬事俱備,只欠提交的狀態(tài)了
提交階段,提交階段并不一定是提交事務(wù),也有可能是回滾事務(wù),如果第一階段都準(zhǔn)備成功,則第二階段的提交就是提交事務(wù);同理如果第一階段未全部準(zhǔn)備成功,則第二階段提交的就是回滾事務(wù)了。假設(shè)第一階段都準(zhǔn)備成功,則協(xié)調(diào)者向所有參與者發(fā)送提交命令,然后接下來等待所有參與者都成功之后,返回事務(wù)執(zhí)行成功
假設(shè)第一階段有部分參與者返回失敗的話,那么協(xié)調(diào)者則會向所有參與者都發(fā)送回滾事務(wù)的請求,即類似上圖,向全部參與者發(fā)送回滾事務(wù)
說到這里其實有些小伙伴已經(jīng)開始有疑問了,我知道了第一階段有失敗的如何處理了,但是如果第二階段出現(xiàn)失敗了咋整呢
其實這里分了兩種情況,分別是第二階段執(zhí)行的是提交階段、第二階段執(zhí)行的是回滾操作,這兩種情況的處理方式其實是一樣的,都是屬于不斷地重試,直到重試成功;對于提交來說,可以根據(jù)業(yè)務(wù)場景,執(zhí)行一定次數(shù)的重試之后,嘗試回滾;但是對于回滾操作,總不能執(zhí)行成功操作吧
所以,如果第二階段是回滾操作有失敗,當(dāng)失敗次數(shù)達(dá)到一定次數(shù)的時候,最好的方法就是人工介入了
提交流程大致也分析的差不多了,接下來一起看看細(xì)節(jié)部分,2PC可以看成同步阻塞協(xié)議,同步阻塞的等待所有參與者的第一階段都有響應(yīng)之后,才會進(jìn)行第二階段的操作;對于Java基礎(chǔ)很熟悉的小伙伴是不是很快想起來Java并發(fā)包中的一個工具類CountDownLatch,以及功能類似的CyclicBarrier,忘記的趕緊回憶下
其實2PC中對于這里的同步阻塞是有超時機制的,協(xié)調(diào)者等待參與者的響應(yīng)超時的情況下,會默認(rèn)失敗,然后協(xié)調(diào)者直接向所有參與者發(fā)起回滾的命令,知道這次事務(wù)失敗
上面這些都是基于參與者的角度來考慮的,那如果協(xié)調(diào)者出問題了呢
協(xié)調(diào)者如果是單點的,出現(xiàn)故障之后,可能會出現(xiàn)一些系統(tǒng)的問題,我們從流程的角度分析下:
準(zhǔn)備階段命令未發(fā)出,協(xié)調(diào)者故障,事務(wù)還沒開始,問題不大;
準(zhǔn)備階段命令發(fā)出了,協(xié)調(diào)者故障,事務(wù)開始了,無論參與者都是成功還是失敗,最終情況都很糟糕,因為參與者無法等到下一步的指令了,也就是卡碟了,不僅事務(wù)無法執(zhí)行,還會鎖定一些公用資源而阻塞其它系統(tǒng);準(zhǔn)備階段命令發(fā)出,全部成功,第二階段執(zhí)行提交階段命令發(fā)出,這種情況也是不行的,因為也可能因為分區(qū)和網(wǎng)絡(luò)阻塞,某些參與者未收到提交命令,理想情況下如果參與者一次性全部收到提交命令,但是參與者有可能提交失敗,這樣還是需要重試,此時協(xié)調(diào)者掛了,也是不行
準(zhǔn)備階段命令發(fā)出,部分失敗,第二階段回滾命令發(fā)出,其實和上面情況類似,也是會出現(xiàn)各式各樣的問題
既然單點協(xié)調(diào)者不行,那就來個多個的吧,通過選舉機制再選一個新協(xié)調(diào)者
如果都處于第一階段,其實都還好,事務(wù)還沒提交,直接都會滾就好了;如果處于第二階段,假設(shè)參與者都沒掛,此時新協(xié)調(diào)者可以向所有參與者來進(jìn)一步確認(rèn)他們自身的情況來推斷下一步該如何操作,如果個別參與者掛了,就比較尷尬了。比如協(xié)調(diào)者發(fā)送了回滾的命令,此時第一個參與者收到了并執(zhí)行了,然后協(xié)調(diào)者和第一個參與者都掛掉了,此時其它參與者都沒收到請求,然后新協(xié)調(diào)者來了,它詢問了其它的參與者都回答OK,但是它不知道其中第一個參與者掛了,此時要是按照全部OK來處理,直接發(fā)送提交命令,就糟糕了,這不是我們想要的結(jié)果
其實雖然2PC協(xié)議上沒說,但是在實現(xiàn)的時候我們需要靈活的讓協(xié)調(diào)者將自己發(fā)過的請求在哪些地方都記一下,也就類似于日志記錄,這樣新的協(xié)調(diào)者來的時候就不、知道此時該不該發(fā)了
即使協(xié)調(diào)者知道自己應(yīng)該發(fā)提交還是回滾請求,但是在參與者也一起掛了的情況下也是沒用的,因為協(xié)調(diào)者無法知道參與者在掛之前有沒有提交事務(wù),其實這里最靠譜的方法,就是對每一步都進(jìn)行相應(yīng)的日志記錄,重要的步驟最好還是強綁定日志記錄的,否則操作成功了,日志記錄失敗那也很糟糕,總之就是要考慮各種極端的情況,盡最大努力去做到每個細(xì)節(jié)都考慮到
2PC是一種盡量保證強一致性的分布式事務(wù),因為它是同步阻塞的,而同步阻塞就意味著在某些情況下會出現(xiàn)鎖定資源的情況,而且單點一旦出現(xiàn)故障,就會造成資源鎖定的情況
以下代碼取自 <<Distributed System: Principles and Paradigms>>
協(xié)調(diào)者:
write START_2PC to local log; //開始事務(wù)
multicast VOTE_REQUEST to all participants; //廣播通知參與者投票
while not all votes have been collected {
wait for any incoming vote;
if timeout { //協(xié)調(diào)者超時
write GLOBAL_ABORT to local log; //寫日志
multicast GLOBAL_ABORT to all participants; //通知事務(wù)中斷
exit;
}
record vote;
}//如果所有參與者都o(jì)k
if all participants sent VOTE_COMMIT and coordinator votes COMMIT {
write GLOBAL_COMMIT to local log;
multicast GLOBAL_COMMIT to all participants;
} else {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
}參與者:
write INIT to local log; //寫日志
wait for VOTE_REQUEST from coordinator;
if timeout { //等待超時
write VOTE_ABORT to local log;
exit;
}
if participant votes COMMIT {
write VOTE_COMMIT to local log; //記錄自己的決策
send VOTE_COMMIT to coordinator;wait for DECISION from coordinator;
if timeout {
multicast DECISION_REQUEST to other participants; //超時通知
wait until DECISION is received; /* remain blocked*/
write DECISION to local log;
}
if DECISION == GLOBAL_COMMIT
write GLOBAL_COMMIT to local log;
else if DECISION == GLOBAL_ABORT
write GLOBAL_ABORT to local log;
} else {
write VOTE_ABORT to local log;
send VOTE_ABORT to coordinator;
}每個參與者維護(hù)一個線程處理其它參與者的DECISION_REQUEST請求:
while true {
wait until any incoming DECISION_REQUEST is received;
read most recently recorded STATE from the local log;
if STATE == GLOBAL_COMMIT
send GLOBAL_COMMIT to requesting participant;
else if STATE == INIT or STATE == GLOBAL_ABORT;
send GLOBAL_ABORT to requesting participant;
else
skip; /* participant remains blocked */
}
3PC三階段提交:準(zhǔn)備階段、預(yù)提交階段、提交階段
3PC其實就是2PC的升級版,相比于2PC,參與者也引入了超時機制,并且還新增了一個階段使得參與者可以利用這一階段來統(tǒng)一各自的狀態(tài)
3PC分為三個階段:準(zhǔn)備階段、預(yù)提交階段、提交階段??雌饋砀袷前?PC中的提交階段分為了預(yù)提交和提交的兩個階段, 但是這里的準(zhǔn)備階段其實就是詢問參與者的自身狀況,就是問你現(xiàn)在的狀況如何,負(fù)載是不是超載,還可以再接受新的任務(wù)嗎
而預(yù)提交階段其實就是類似于2PC的準(zhǔn)備階段,就是除了事務(wù)的提交該做的都做了,就是之前的準(zhǔn)備工作,但是在3PC中叫做預(yù)提交階段

3PC是首先準(zhǔn)備階段并不會直接執(zhí)行事務(wù),而是先去詢問此時的參與者是否有條件可以執(zhí)行這個事務(wù),因此不會直接鎖住資源,而預(yù)提交階段的引入則是為了起到了一個統(tǒng)狀態(tài)的作用,在預(yù)處理階段表面所有參與者都已經(jīng)回應(yīng)了
其實這也多引入了一個階段,因此性能會差一些,而且絕大部分的情況下資源也都是沒問題的,也就是可用的,這樣等于每次明知可用但是還是得詢問一次
當(dāng)然,這其中哪一個階段的參與者返回失敗都會宣布事務(wù)失敗,這個2PC也是一樣的,當(dāng)然到最后的提交階段和2PC一樣都是只要是提交請求也就只能通過不斷的重試咯
我們上面說過2PC是同步阻塞的,協(xié)調(diào)者掛在了提交請求還未發(fā)出去的時候是最尷尬的,所有參與者都已經(jīng)鎖定了資源并且阻塞的等待著,于是引入了超時機制,參與者則不用直接干干的等著了,如果是等待提交命令超時,那么參與者就會提交事務(wù)了,因為到了這一階段大概率都是提交的,如果是等待預(yù)提交超時,接下來也沒啥影響
這里其實有一個問題,然后超時機制會帶來數(shù)據(jù)不一致的問題,就是在等待提交命令的時候超時,那么參與者自動提交事務(wù)了,但是呢,也可能執(zhí)行的是回滾機制,這樣一來數(shù)據(jù)便出現(xiàn)了不一致了
3PC的引入是為了解決提交階段2PC協(xié)調(diào)者和其中的部分參與者都掛了的情況下,然后之后的新選舉的協(xié)調(diào)者不知道當(dāng)前應(yīng)該是該提交還是回滾的問題,新協(xié)調(diào)者來的時候發(fā)現(xiàn)有一個參與者處于預(yù)提交或者提交階段,那么表明所以參與者都已經(jīng)經(jīng)過確認(rèn)了,所以此時執(zhí)行的就是提交命令了
3PC就是通過引入預(yù)提交階段來是的參與者之間的狀態(tài)得到真正的統(tǒng)一,也就是留了一個階段讓大家都同步,但是這也是只能讓協(xié)調(diào)者知道如何做,并不能保證這樣做一定是對的,這其實和上面的2PC的分析一直,因為掛了的參與者到底有沒有執(zhí)行事務(wù)是無法斷定的,所以說呢,3PC通過預(yù)提交階段可以減少故障時候的復(fù)雜性,但是并不能保證數(shù)據(jù)真正的一致,處理掛了的那個參與者也恢復(fù)了
一句話總結(jié):3PC相比于2PC做了一定的參與者超時機制的改進(jìn),并且增加了預(yù)提交階段,可以使故障恢復(fù)之后的協(xié)調(diào)者的決策復(fù)雜度降低,但整體的交互過程會變得更長,性能會有所下降,而且還會出現(xiàn)數(shù)據(jù)不一致的情況
TCC:Try-Confirm-Cancel
TCC屬于業(yè)務(wù)層面的分布式事務(wù),分布式事務(wù)不僅僅包含數(shù)據(jù)庫層面的操作,還包括業(yè)務(wù)層面的操作,這時候TCC就要排上用場了
TCC指的就是Try、Confirm、Cancel三個步驟,Try指的是預(yù)留,指的是資源的預(yù)留和鎖定;Confirm指的就是確認(rèn)操作,這一步其實就是屬于真正的執(zhí)行了,真正的消耗資源來進(jìn)行相應(yīng)的業(yè)務(wù)提交操作;Cancel指的是撤銷操作,可以理解為把預(yù)留階段的動作銷毀了,就是一個回滾操作
從思想上來看,其實是和2PC、3PC是類似的,都是先試探性的執(zhí)行,先試探性的鎖定資源,如果每一個參與者都沒問題了,就可以執(zhí)行真正的操作了,提交或者回滾

舉個例子:一個事務(wù)要執(zhí)行A、B、C三個操作,那么先對三個操作執(zhí)行預(yù)留動作,如果所有都預(yù)留成功了那么就執(zhí)行確認(rèn)提交操作,如果其中至少有一個預(yù)留失敗,那就都執(zhí)行撤銷的動作
TCC模型其中還有一個事務(wù)管理者的角色,用來記錄TCC有關(guān)的全局事務(wù)操作的狀態(tài),并且準(zhǔn)備提交或者回滾事務(wù),其實這個是比較容易理解的,難點在于業(yè)務(wù)上的定義
怎么說呢,TCC這種是對業(yè)務(wù)的侵入較大和業(yè)務(wù)緊耦合,需要根據(jù)相應(yīng)的特定的業(yè)務(wù)場景和業(yè)務(wù)邏輯來設(shè)定的響應(yīng)操作,其實還有一點需要注意的是,撤銷和確認(rèn)的操作的執(zhí)行的就是需要重試,就是需要保證操作的冪等性
TCC相對來說,適用的范圍應(yīng)該是更廣的,但是這個是有一個缺點的,就是這個和業(yè)務(wù)是耦合的,需要大量的開發(fā),因為都是在業(yè)務(wù)上的實現(xiàn),等同于每個場景都需要三個方法來實現(xiàn),就是嵌入業(yè)務(wù),所以TCC是可以跨業(yè)務(wù)系統(tǒng)、跨數(shù)據(jù)庫來實現(xiàn)事務(wù)
本地消息表
本地消息表,就是利用了各個系統(tǒng)的本地事務(wù)來實現(xiàn)分布式事務(wù),這個呢,其實很簡單的道理,其實就是會有一張存放本地消息的表,一般都是放在數(shù)據(jù)庫中,然后在執(zhí)行業(yè)務(wù)的時候,必須把業(yè)務(wù)的真正的執(zhí)行操作和相應(yīng)的這個操作的消息放入到消息表中這個操作,存放到同一個事務(wù)中,就是只要操作成功了,就必須保證該消息也成功的放入到本地的消息表中了
接下來調(diào)用下一個操作的時候,如果下一個操作調(diào)用成功了,就可以直接把消息的狀態(tài)改成已成功,調(diào)用失敗也沒有關(guān)系,我們可以寫一個定時任務(wù)來讀取本地的消息表,然后篩選出未執(zhí)行成功的消息再調(diào)用對應(yīng)的服務(wù),服務(wù)更新成功了,再改變消息的狀態(tài)
其實這里也是需要重試機制,重試就得保證對應(yīng)服務(wù)的方法是冪等的,而且一般重試也會有最大的次數(shù),超過最大次數(shù)的時候可以人工介入
本地消息表實現(xiàn)的是業(yè)務(wù)的最終一致性,需要能夠容忍數(shù)據(jù)暫時不一致的情況
消息事務(wù)
其實消息事務(wù),最典型的就是屬于RocketMQ中的實現(xiàn)了,而且應(yīng)用的場景也是比較多的

RocketMQ的機制就是先給Broker發(fā)送事務(wù)消息,也就是半消息,半消息指的是這個消息對消費者來說不可見,然后發(fā)送成功后,發(fā)送之后會繼續(xù)執(zhí)行本地事務(wù)
第二步就是根據(jù)本地事務(wù)的執(zhí)行結(jié)果向Broker發(fā)送Commit和Rollback命令,如果一直不發(fā)送,RocketMQ的發(fā)送方會提供一個反查事務(wù)狀態(tài)的接口,用來反查相應(yīng)的事務(wù)的結(jié)果到底是成功還是回滾
其實這也就是個超時機制,在一段時間內(nèi)沒有收到任何的操作請求,那么Broker就會通過相應(yīng)的結(jié)果查出該事務(wù)是否成功執(zhí)行呢,是Commit還是Rollback
如果是Commit,則broker就會發(fā)送這個消息到訂閱方,然后再做對應(yīng)的操作,做完了之后就可以消費這個消息,如果是Rollback則訂閱方即收不到這個消息,等同于事務(wù)沒有執(zhí)行過
最大努力通知
其實最大努力通知我個人認(rèn)為是一種思想,像上面的本地消息表、事務(wù)消息也是屬于最大努力通知類型的

本地消息表會有后臺任務(wù)定時查看未完成的任務(wù)的消息,然后去調(diào)用對應(yīng)的服務(wù),進(jìn)行多次重試,當(dāng)多次失敗的時候就需要引入人工,這也是屬于最大努力
事務(wù)消息也是屬于類似,半消息被Commit之后就會發(fā)送到消費端了,如果消費端一直不消費或者消費不了則會一直重試,如果重試次數(shù)達(dá)到一定數(shù)量,該消息變回進(jìn)入到私信隊列,也是屬于盡最大努力通知吧
這應(yīng)該是屬于一種思想,盡最大努力的達(dá)到事務(wù)的最終一致,適用于對時間不敏感的業(yè)務(wù)場景


好了,以上就是全部內(nèi)容了,我是小魚仙,你們的學(xué)習(xí)成長小伙伴
我希望有一天能夠靠寫字養(yǎng)活自己,現(xiàn)在還在磨練,這個時間可能會有很多年,感謝你們做我最初的讀者和傳播者。請大家相信,只要給我一份愛,我終究會還你們一頁情的。
再次感謝大家能夠讀到這里,我后面會持續(xù)的更新技術(shù)文章以及一些記錄生活的靈魂文章,如果覺得不錯的,覺得【小仙】有點東西的話,求點贊、關(guān)注、分享三連
哦,對了!后續(xù)的更新文章我都會及時放到這里,歡迎大家點擊觀看,都是干貨文章啊,建議收藏,以后隨時翻閱查看
https://github.com/DayuMM2021/Java

