前言
上一篇文章《就這?分布式 ID 發(fā)號器實戰(zhàn)》之后,我朋友輝哥在后臺留言讓靚仔聊聊分布式事務(wù),既然輝哥都開口了,那必須得滿足啊,安排!
溫馨提示:文章很干,請多喝水

什么是分布式事務(wù)
什么是事務(wù)想必大多數(shù)朋友應(yīng)該都很清楚了,不清楚的可以看前面的文章《就這?一篇文章讓你讀懂 Spring 事務(wù)》。
分布式事務(wù)就是指事務(wù)的參與者、支持事務(wù)的服務(wù)器、資源服務(wù)器以及事務(wù)管理器分別位于不同的分布式系統(tǒng)的不同節(jié)點之上。
簡單來說,就是一個大的操作由 N 個小操作組成,這些小的操作分布在不同的服務(wù)器上,且屬于不同的應(yīng)用,分布式事務(wù)需要保證這些小操作要么全部成功,要么全部失敗。比如存在一個訂單的微服務(wù),一個庫存的微服務(wù),當(dāng)訂單完成需要同步減少庫存,這時候就要在事務(wù)上確保完整和一致。
相關(guān)理論
關(guān)于事務(wù)的特性(ACID)和隔離級別這里就不再重復(fù)介紹了,可以看前面的文章,這里著重介紹下兩個新的知識:CAP 理論和 BASE 理論。
CAP 理論
- 一致性(Consistency):在分布式系統(tǒng)完成某寫操作后任何讀操作,都應(yīng)該獲取到該寫操作寫入的那個最新的值。相當(dāng)于要求分布式系統(tǒng)中的各節(jié)點時時刻刻保持?jǐn)?shù)據(jù)的一致性。
- 可用性(Availability): 一直可以正常的做讀寫操作。簡單而言就是客戶端一直可以正常訪問并得到系統(tǒng)的正常響應(yīng)。用戶角度來看就是不會出現(xiàn)系統(tǒng)操作失敗或者訪問超時等問題。
- 分區(qū)容錯性(PartitionTolerance):指的分布式系統(tǒng)中的某個節(jié)點或者網(wǎng)絡(luò)分區(qū)出現(xiàn)了故障的時候,整個系統(tǒng)仍然能對外提供滿足一致性和可用性的服務(wù)。也就是說部分故障不影響整體使用。事實上我們在設(shè)計分布式系統(tǒng)是都會考慮到 bug、硬件、網(wǎng)絡(luò)等各種原因造成的故障,所以即使部分節(jié)點或者網(wǎng)絡(luò)出現(xiàn)故障,我們要求整個系統(tǒng)還是要繼續(xù)使用的
CAP 是一個已經(jīng)被證實的理論,在分布式系統(tǒng)中最多只能同時滿足這三項中的兩項,而分區(qū)容錯性是分布式系統(tǒng)必須滿足的,所以在分布式系統(tǒng)中常見的組合就是 CP 和 AP
- CP:放棄可用性,注重一致性和分區(qū)容錯性,其實這就是所謂的強(qiáng)一致性,可能在銀行跨行轉(zhuǎn)賬這種強(qiáng)一致業(yè)務(wù)場景才會用到,具體得根據(jù)業(yè)務(wù)場景做取舍。
- AP:放棄強(qiáng)一致性,注重可用性和分區(qū)容錯性,這是現(xiàn)在絕大多數(shù)分布式業(yè)務(wù)場景的選擇,只要最后能保證最終一致性( BASE 理論)即可。
BASE 理論
基本可用(Basically Available):基本可用是指分布式系統(tǒng)在出現(xiàn)故障的時候,允許損失部分可用性,即保證核心可用。電商大促時,為了應(yīng)對訪問量激增,部分用戶可能會被引導(dǎo)到降級頁面,服務(wù)層也可能只提供降級服務(wù)。這就是損失部分可用性的體現(xiàn)。
-
軟狀態(tài)(Soft State):軟狀態(tài)是指允許系統(tǒng)存在中間狀態(tài),而該中間狀態(tài)不會影響系統(tǒng)整體可用性。分布式存儲中一般一份數(shù)據(jù)至少會有三個副本,允許不同節(jié)點間副本同步的延時就是軟狀態(tài)的體現(xiàn)。MySQL Replication 的異步復(fù)制也是一種體現(xiàn)。
最終一致性(Eventual Consistency):最終一致性是指系統(tǒng)中的所有數(shù)據(jù)副本經(jīng)過一定時間后,最終能夠達(dá)到一致的狀態(tài)。弱一致性和強(qiáng)一致性相反,最終一致性是弱一致性的一種特殊情況。
常見解決方案
1、兩階段提交
兩階段提交(Two-phaseCommit),簡稱為 2PC,兩階段提交是一種強(qiáng)一致性設(shè)計,它引入一個事務(wù)協(xié)調(diào)者的角色來協(xié)調(diào)管理各個參與者(也可稱之為各本地資源)的提交和回滾。
所謂的兩個階段是指:第一階段:準(zhǔn)備階段(投票階段)和第二階段:提交階段(執(zhí)行階段)。
準(zhǔn)備階段(Prepare Phase):首先協(xié)調(diào)器會向所有的參與者發(fā)送準(zhǔn)備提交或者取消提交的請求,然后會收集參與者的決策。
提交階段(Commit Phase):協(xié)調(diào)者會收集所有參與者的決策信息,當(dāng)且僅當(dāng)所有的參與者向協(xié)調(diào)器發(fā)送確認(rèn)消息時協(xié)調(diào)器才會提交請求,否則執(zhí)行回滾或者取消請求。

2PC 存在的問題:
- 同步阻塞:所有的參與者都是事務(wù)同步阻塞型的。當(dāng)參與者占有公共資源時,其他第三方節(jié)點訪問公共資源不得不處于阻塞狀態(tài)。
- 單點故障:一旦協(xié)調(diào)者發(fā)生故障,系統(tǒng)不可用。
- 數(shù)據(jù)不一致:當(dāng)協(xié)調(diào)者發(fā)送 commit 之后,有的參與者收到 commit 消息,事務(wù)執(zhí)行成功,有的沒有收到,處于阻塞狀態(tài),這段時間會產(chǎn)生數(shù)據(jù)不一致。
- 不確定性:當(dāng)協(xié)調(diào)者發(fā)送 commit 之后,并且此時只有一個參與者收到了 commit,那么當(dāng)該參與者與協(xié)調(diào)器同時宕機(jī)之后,重新選舉的協(xié)調(diào)器無法確定該條消息是否提交成功。
2PC 的優(yōu)勢在于對業(yè)務(wù)沒有侵入,可以利用數(shù)據(jù)庫自身機(jī)制來進(jìn)行事務(wù)的提交和回滾。
常見的基于 2PC 的具體落地方案有:JTA(XA 規(guī)范) 和 Seata( AT 模式)。
2、三階段提交
三階段提交(Three-phase commit),簡稱為 3PC,是 2PC 的改進(jìn)版本。同時在協(xié)調(diào)者和參與者都引入了超時機(jī)制,還在 2PC 中的準(zhǔn)備階段和提交階段中間增加了一個預(yù)提交階段。
- 準(zhǔn)備階段(CanCommit):協(xié)調(diào)者向各個參與者發(fā)送請求,詢問是否可以執(zhí)行事務(wù),但并不執(zhí)行事務(wù)。
- 預(yù)提交階段(PreCommit):如果從協(xié)調(diào)者得到的反饋是滿足執(zhí)行條件,那么就發(fā)送預(yù)提交請求,并開始執(zhí)行事務(wù);如果從協(xié)調(diào)者得到的反饋是不滿足執(zhí)行條件或者超時,則發(fā)送事務(wù)中斷請求。
- 提交階段(DoCommit):如果預(yù)提交階段發(fā)送的是預(yù)提交請求,那么正常提交事務(wù);如果預(yù)提交階段發(fā)送的是事務(wù)中斷請求,那么直接中斷事務(wù)。

相對于 2PC,3PC 主要解決的單點故障問題,并減少阻塞,因為一旦參與者無法及時收到來自協(xié)調(diào)者的信息之后,他會默認(rèn)執(zhí)行 commit。而不會一直持有事務(wù)資源并處于阻塞狀態(tài)。但是這種機(jī)制也會導(dǎo)致數(shù)據(jù)一致性問題,因為,由于網(wǎng)絡(luò)原因,協(xié)調(diào)者發(fā)送的中斷響應(yīng)沒有及時被參與者接收到,那么參與者在等待超時之后執(zhí)行了 commit 操作。這樣就和其他接到中斷命令并執(zhí)行回滾的參與者之間存在數(shù)據(jù)不一致的情況。而且 3PC 整體的交互過程更長,性能也會有所下降。
3PC 目前似乎只存在于理論,還沒有具體落地方案。
3、TCC
2PC 和 3PC 都是依賴于數(shù)據(jù)庫的事務(wù)提交和回滾,但是有時候很多業(yè)務(wù)并不僅僅只涉及到數(shù)據(jù)庫,可能還會發(fā)送短息、消息等等,而 TCC 就是屬于業(yè)務(wù)層面或者說是應(yīng)用層面的分布式事務(wù)。
TCC 方案分為Try-Confirm-Cancel三個階段,屬于補(bǔ)償性分布式事務(wù)。
- Try 階段:完成所有業(yè)務(wù)檢查(一致性),預(yù)留業(yè)務(wù)資源(準(zhǔn)隔離性)
- Confirm 階段:確認(rèn)執(zhí)行業(yè)務(wù)操作,不再做任何業(yè)務(wù)檢查, 只使用Try階段預(yù)留的業(yè)務(wù)資源。
- Cancel 階段:取消Try階段預(yù)留的業(yè)務(wù)資源。

有的朋友可能會問了,Try 成功了會執(zhí)行 Confirm,失敗了會執(zhí)行 Cancel,那 Confirm 階段失敗了怎么辦?這時候只能設(shè)置重試機(jī)制,不斷重試調(diào)失敗的 Confirm,直到成功為止,真有怎么也不成功的,就只能人工介入了。
TCC 需要根據(jù)每個場景和業(yè)務(wù)邏輯來設(shè)計相應(yīng)的操作,所以很大程度增加了業(yè)務(wù)代碼的復(fù)雜度,對業(yè)務(wù)有很大的侵入。
雖說對業(yè)務(wù)有侵入,但是 TCC 沒有資源的阻塞,每一個方法都是直接提交事務(wù)的,如果出錯是通過業(yè)務(wù)層面的 Cancel 來進(jìn)行補(bǔ)償,所以也稱補(bǔ)償性事務(wù)方法。
TCC 要注意的幾個問題:
冪等問題:因為網(wǎng)絡(luò)調(diào)用無法保證請求一定能到達(dá),所以都會有重調(diào)機(jī)制,因此對于 Try、Confirm、Cancel 三個方法都需要冪等實現(xiàn),避免重復(fù)執(zhí)行產(chǎn)生錯誤。
空回滾問題:指的是 Try 方法由于網(wǎng)絡(luò)問題沒收到超時了,此時事務(wù)管理器就會發(fā)出 Cancel 命令,那么需要支持 Cancel 在未執(zhí)行 Try 的情況下能正常的 Cancel。
懸掛問題:這個問題也是指 Try 方法由于網(wǎng)絡(luò)阻塞超時觸發(fā)了事務(wù)管理器發(fā)出了 Cancel 命令,但是執(zhí)行了 Cancel 命令之后 Try 請求到了。所以空回滾之后還得記錄一下,防止 Try 的再調(diào)用。
4、本地消息表
本地消息表分布式事務(wù)解決方案是國外的 eBay 提出的一套方案。其實就是利用了各系統(tǒng)本地的事務(wù)來實現(xiàn)分布式事務(wù),在數(shù)據(jù)庫中存放一張事務(wù)消息表,在執(zhí)行業(yè)務(wù)操作的時候, 將業(yè)務(wù)的執(zhí)行和將消息放入消息表中的操作放在同一個事務(wù)中。
本地事務(wù)執(zhí)行成功之后再調(diào)用其他服務(wù),如果成功了就將消息表里的消息狀態(tài)改為成功,如果失敗了,則由定時任務(wù)去讀取本地事務(wù)表中未成功的消息,再去調(diào)用相應(yīng)的服務(wù),成功后再次修改狀態(tài)。

這里也要設(shè)置重試機(jī)制,一旦有實在不成功的,還需人工介入。這里要注意的是,也要保證對應(yīng)服務(wù)的方法冪等性。
可以看出,本地消息表實現(xiàn)比較簡單,是一種最大努力通知思想,實現(xiàn)的是最終一致性,容忍了數(shù)據(jù)暫時不一致的情況。
缺點是嚴(yán)重依賴數(shù)據(jù)庫。
5、可靠消息最終一致性方案
在上面的本地消息表方案中,生產(chǎn)者需要額外創(chuàng)建消息表,還需要對本地消息表進(jìn)行輪詢,業(yè)務(wù)負(fù)擔(dān)較重。阿里開源的 RocketMQ 4.3 之后的版本正式支持事務(wù)消息,該事務(wù)消息本質(zhì)上是把本地消息表放到 RocketMQ 上,解決生產(chǎn)端的消息發(fā)送與本地事務(wù)執(zhí)行的原子性問題。
服務(wù) A,先給 Broker (消息中間件) 發(fā)送一個 Half Message(半消息),其實這個半消息已發(fā)送到 Broker 端,但是此消息的狀態(tài)被標(biāo)記為"不能投遞",消費(fèi)者還看不到,處于這種狀態(tài)下的消息稱為半消息。
發(fā)送完 半消息后,服務(wù)A 執(zhí)行業(yè)務(wù)操作(本地事務(wù)),再根據(jù)操作結(jié)果:如果成功,則向 Broker 發(fā)送 一個 Commit 命令,這時半消息就變成了可以被消費(fèi)者消息;如果失敗,則發(fā)送一個 RollBack 命令,該消息則會被刪除。
如果是 Commit 那么服務(wù) B 就能收到這條消息,然后再做對應(yīng)的操作,做完了之后再消費(fèi)這條消息即可。
如果 RocketMQ 沒有收到服務(wù) A 確認(rèn)狀態(tài)的消息,那么半消息 RocketMQ 會自動定時輪詢回調(diào)你的接口,詢問這個處理的處理情況。借助這點,服務(wù)A實現(xiàn)一個回調(diào),根據(jù)實際處理結(jié)果 Commit 或者 Rollback,加強(qiáng)一致性判斷。

在服務(wù) B 執(zhí)行的過程中也可能會失敗,這時也是需要重試,一直執(zhí)行不成功也需要人工介入,同時也需要保證服務(wù) B 方法的冪等性。
6、最大努力通知
最大努力通知型( Best-effort delivery)是最簡單的一種柔性事務(wù),適用于一些最終一致性時間敏感度低的業(yè)務(wù),且被動方處理結(jié)果不影響主動方的處理結(jié)果。典型的使用場景:如銀行通知、商戶通知等。
就本地消息表來說會有后臺任務(wù)定時去查看未完成的消息,然后去調(diào)用對應(yīng)的服務(wù),當(dāng)一個消息多次調(diào)用都失敗的時候可以記錄下然后引入人工,或者直接舍棄。這其實算是最大努力了
事務(wù)消息也是一樣,當(dāng)半消息被commit了之后確實就是普通消息了,如果訂閱者一直不消費(fèi)或者消費(fèi)不了則會一直重試,到最后進(jìn)入死信隊列。其實這也算最大努力。
最大努力通知,發(fā)起通知方盡最大的努力將業(yè)務(wù)處理結(jié)果通知為接收通知方,但是可能消息接收不到,此時需要接收通知方主動調(diào)用發(fā)起通知方的接口查詢業(yè)務(wù)處理結(jié)果,通知的可靠性關(guān)鍵在接收通知方。
總結(jié)
其實分布式事務(wù)解決方案還有很多,但是各自還是會存在很多問題,極端情況下也都需要人工去處理,而且大大提高了流程的復(fù)雜度,會帶來很多額外的開銷。
所以謹(jǐn)記,在真實的開發(fā)過程中,能不使用分布式事務(wù)就不要使用!
后面會給大家?guī)矸植际绞聞?wù)的實戰(zhàn),沒點關(guān)注的可以點個關(guān)注,防止走丟了。
END
往期推薦
SpringBoot+Redis 實現(xiàn)消息訂閱發(fā)布
更多精彩推薦,請關(guān)注公眾號【靚仔聊編程】