七種分布式事務的解決方案,一次講給你聽
大家好,我是小富~
什么是分布式事務
分布式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器「分別位于不同的分布式系統(tǒng)的不同節(jié)點之上」。
一個大的操作由N多的小的操作共同完成。而這些小的操作又分布在不同的服務上。針對于這些操作,「要么全部成功執(zhí)行,要么全部不執(zhí)行」。
為什么會有分布式事務?
舉個例子:

轉(zhuǎn)賬是最經(jīng)典的分布式事務場景,假設用戶 A 使用銀行 app 發(fā)起一筆跨行轉(zhuǎn)賬給用戶 B,銀行系統(tǒng)首先扣掉用戶 A 的錢,然后增加用戶 B 賬戶中的余額。
如果其中某個步驟失敗,此時就有可能會出現(xiàn) 2 種「異?!?/strong>情況:
1.用戶 A 的賬戶扣款成功,用戶 B 賬戶余額增加失敗 2.用戶 A 賬戶扣款失敗,用戶 B 賬戶余額增加成功。
對于銀行系統(tǒng)來說,以上 2 種情況都是「不允許發(fā)生」,此時就需要事務來保證轉(zhuǎn)賬操作的成功。
在「單體應用」中,我們只需要貼上@Transactional注解就可以開啟事務來保證整個操作的「原子性」。
但是看似以上簡單的操作,在實際的應用架構(gòu)中,不可能是單體的服務,我們會把這一系列操作交給「N個服務」去完成,也就是拆分成為「分布式微服務架構(gòu)」。

比如下訂單服務,扣庫存服務等等,必須要「保證不同服務狀態(tài)結(jié)果的一致性」,于是就出現(xiàn)了分布式事務。
分布式理論
CAP定理
在一個分布式系統(tǒng)中,以下三點特性無法同時滿足,「魚與熊掌不可兼得」
一致性(C):
在分布式系統(tǒng)中的所有數(shù)據(jù)備份,「在同一時刻是否擁有同樣的值」。(等同于所有節(jié)點訪問同一份最新的數(shù)據(jù)副本)
可用性(A):
在集群中一部分節(jié)點「故障」后,集群整體「是否還能響應」客戶端的讀寫請求。(對數(shù)據(jù)更新具備高可用性)
分區(qū)容錯性(P):
即使出現(xiàn)「單個組件無法可用,操作依然可以完成」。
具體地講在分布式系統(tǒng)中,在任何數(shù)據(jù)庫設計中,一個Web應用「至多只能同時支持上面的兩個屬性」。顯然,任何橫向擴展策略都要依賴于數(shù)據(jù)分區(qū)。因此,設計人員必須在一致性與可用性之間做出選擇。
BASE理論
在分布式系統(tǒng)中,我們往往追求的是可用性,它的重要程序比一致性要高,那么如何實現(xiàn)高可用性呢?
前人已經(jīng)給我們提出來了另外一個理論,就是BASE理論,它是用來對CAP定理進行進一步擴充的。BASE理論指的是:
「Basically Available(基本可用)」 「Soft state(軟狀態(tài))」 「Eventually consistent(最終一致性)」
BASE理論是對CAP中的一致性和可用性進行一個權(quán)衡的結(jié)果,理論的核心思想就是:我們無法做到強一致,但每個應用都可以根據(jù)自身的業(yè)務特點,采用適當?shù)姆绞絹硎瓜到y(tǒng)達到最終一致性(Eventual consistency)。
分布式事務解決方案
兩階段提交(2PC)
熟悉mysql的同學對兩階段提交應該頗為熟悉,mysql的事務就是通過「日志系統(tǒng)」來完成兩階段提交的。
兩階段協(xié)議可以用于單機集中式系統(tǒng),由事務管理器協(xié)調(diào)多個資源管理器;也可以用于分布式系統(tǒng),「由一個全局的事務管理器協(xié)調(diào)各個子系統(tǒng)的局部事務管理器完成兩階段提交」。

這個協(xié)議有「兩個角色」,
A節(jié)點是事務的協(xié)調(diào)者,B和C是事務的參與者。
事務的提交分成兩個階段
第一個階段是「投票階段」
1.協(xié)調(diào)者首先將命令「寫入日志」 2. 「發(fā)一個prepare命令」給B和C節(jié)點這兩個參與者 3.B和C收到消息后,根據(jù)自己的實際情況,「判斷自己的實際情況是否可以提交」 4.將處理結(jié)果「記錄到日志」系統(tǒng) 5.將結(jié)果「返回」給協(xié)調(diào)者

第二個階段是「決定階段」
當A節(jié)點收到B和C參與者所有的確認消息后
「判斷」所有協(xié)調(diào)者「是否都可以提交」 如果可以則「寫入日志」并且發(fā)起commit命令 有一個不可以則「寫入日志」并且發(fā)起abort命令 參與者收到協(xié)調(diào)者發(fā)起的命令,「執(zhí)行命令」 將執(zhí)行命令及結(jié)果「寫入日志」 「返回結(jié)果」給協(xié)調(diào)者
可能會存在哪些問題?
「單點故障」:一旦事務管理器出現(xiàn)故障,整個系統(tǒng)不可用
「數(shù)據(jù)不一致」:在階段二,如果事務管理器只發(fā)送了部分 commit 消息,此時網(wǎng)絡發(fā)生異常,那么只有部分參與者接收到 commit 消息,也就是說只有部分參與者提交了事務,使得系統(tǒng)數(shù)據(jù)不一致。
「響應時間較長」:整個消息鏈路是串行的,要等待響應結(jié)果,不適合高并發(fā)的場景
「不確定性」:當事務管理器發(fā)送 commit 之后,并且此時只有一個參與者收到了 commit,那么當該參與者與事務管理器同時宕機之后,重新選舉的事務管理器無法確定該條消息是否提交成功。
三階段提交(3PC)
三階段提交又稱3PC,相對于2PC來說增加了CanCommit階段和超時機制。如果段時間內(nèi)沒有收到協(xié)調(diào)者的commit請求,那么就會自動進行commit,解決了2PC單點故障的問題。
但是性能問題和不一致問題仍然沒有根本解決。下面我們還是一起看下三階段流程的是什么樣的?
第一階段:「CanCommit階段」這個階段所做的事很簡單,就是協(xié)調(diào)者詢問事務參與者,你是否有能力完成此次事務。
如果都返回yes,則進入第二階段 有一個返回no或等待響應超時,則中斷事務,并向所有參與者發(fā)送abort請求 第二階段:「PreCommit階段」此時協(xié)調(diào)者會向所有的參與者發(fā)送PreCommit請求,參與者收到后開始執(zhí)行事務操作,并將Undo和Redo信息記錄到事務日志中。參與者執(zhí)行完事務操作后(此時屬于未提交事務的狀態(tài)),就會向協(xié)調(diào)者反饋“Ack”表示我已經(jīng)準備好提交了,并等待協(xié)調(diào)者的下一步指令。
第三階段:「DoCommit階段」在階段二中如果所有的參與者節(jié)點都可以進行PreCommit提交,那么協(xié)調(diào)者就會從“預提交狀態(tài)”轉(zhuǎn)變?yōu)椤疤峤粻顟B(tài)”。然后向所有的參與者節(jié)點發(fā)送"doCommit"請求,參與者節(jié)點在收到提交請求后就會各自執(zhí)行事務提交操作,并向協(xié)調(diào)者節(jié)點反饋“Ack”消息,協(xié)調(diào)者收到所有參與者的Ack消息后完成事務。相反,如果有一個參與者節(jié)點未完成PreCommit的反饋或者反饋超時,那么協(xié)調(diào)者都會向所有的參與者節(jié)點發(fā)送abort請求,從而中斷事務。
補償事務(TCC)
TCC其實就是采用的補償機制,其核心思想是:「針對每個操作,都要注冊一個與其對應的確認和補償(撤銷)操作」。它分為三個階段:
「Try,Confirm,Cancel」
Try階段主要是對「業(yè)務系統(tǒng)做檢測及資源預留」,其主要分為兩個階段 Confirm 階段主要是對「業(yè)務系統(tǒng)做確認提交」,Try階段執(zhí)行成功并開始執(zhí)行 Confirm階段時,默認 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。 Cancel 階段主要是在業(yè)務執(zhí)行錯誤,需要回滾的狀態(tài)下執(zhí)行的業(yè)務取消,「預留資源釋放」。
比如下一個訂單減一個庫存:

執(zhí)行流程:
Try階段:訂單系統(tǒng)將當前訂單狀態(tài)設置為支付中,庫存系統(tǒng)校驗當前剩余庫存數(shù)量是否大于1,然后將可用庫存數(shù)量設置為庫存剩余數(shù)量-1, 如果Try階段「執(zhí)行成功」,執(zhí)行Confirm階段,將訂單狀態(tài)修改為支付成功,庫存剩余數(shù)量修改為可用庫存數(shù)量 如果Try階段「執(zhí)行失敗」,執(zhí)行Cancel階段,將訂單狀態(tài)修改為支付失敗,可用庫存數(shù)量修改為庫存剩余數(shù)量
TCC 事務機制相比于上面介紹的2PC,解決了其幾個缺點:
1.「解決了協(xié)調(diào)者單點」,由主業(yè)務方發(fā)起并完成這個業(yè)務活動。業(yè)務活動管理器也變成多點,引入集群。 2.「同步阻塞」:引入超時,超時后進行補償,并且不會鎖定整個資源,將資源轉(zhuǎn)換為業(yè)務邏輯形式,粒度變小。 3.「數(shù)據(jù)一致性」,有了補償機制之后,由業(yè)務活動管理器控制一致性
總之,TCC 就是通過代碼人為實現(xiàn)了兩階段提交,不同的業(yè)務場景所寫的代碼都不一樣,并且很大程度的「增加」了業(yè)務代碼的「復雜度」,因此,這種模式并不能很好地被復用。
本地消息表

消息生產(chǎn)方,需要額外建一個消息表,并「記錄消息發(fā)送狀態(tài)」。消息表和業(yè)務數(shù)據(jù)要在一個事務里提交,也就是說他們要在一個數(shù)據(jù)庫里面。然后消息會經(jīng)過MQ發(fā)送到消息的消費方。
如果消息發(fā)送失敗,會進行重試發(fā)送。 消息消費方,需要「處理」這個「消息」,并完成自己的業(yè)務邏輯。
如果是「業(yè)務上面的失敗」,可以給生產(chǎn)方「發(fā)送一個業(yè)務補償消息」,通知生產(chǎn)方進行回滾等操作。 此時如果本地事務處理成功,表明已經(jīng)處理成功了 如果處理失敗,那么就會重試執(zhí)行。 生產(chǎn)方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發(fā)送一遍。
消息事務
消息事務的原理是將兩個事務「通過消息中間件進行異步解耦」,和上述的本地消息表有點類似,但是是通過消息中間件的機制去做的,其本質(zhì)就是'將本地消息表封裝到了消息中間件中'。
執(zhí)行流程:
發(fā)送prepare消息到消息中間件 發(fā)送成功后,執(zhí)行本地事務 如果事務執(zhí)行成功,則commit,消息中間件將消息下發(fā)至消費端 如果事務執(zhí)行失敗,則回滾,消息中間件將這條prepare消息刪除 消費端接收到消息進行消費,如果消費失敗,則不斷重試
這種方案也是實現(xiàn)了「最終一致性」,對比本地消息表實現(xiàn)方案,不需要再建消息表,「不再依賴本地數(shù)據(jù)庫事務」了,所以這種方案更適用于高并發(fā)的場景。目前市面上實現(xiàn)該方案的「只有阿里的 RocketMQ」。
最大努力通知
最大努力通知的方案實現(xiàn)比較簡單,適用于一些最終一致性要求較低的業(yè)務。
執(zhí)行流程:
系統(tǒng) A 本地事務執(zhí)行完之后,發(fā)送個消息到 MQ; 這里會有個專門消費 MQ 的服務,這個服務會消費 MQ 并調(diào)用系統(tǒng) B 的接口; 要是系統(tǒng) B 執(zhí)行成功就 ok 了;要是系統(tǒng) B 執(zhí)行失敗了,那么最大努力通知服務就定時嘗試重新調(diào)用系統(tǒng) B, 反復 N 次,最后還是不行就放棄。
Sagas 事務模型
Saga事務模型又叫做長時間運行的事務
其核心思想是「將長事務拆分為多個本地短事務」,由Saga事務協(xié)調(diào)器協(xié)調(diào),如果正常結(jié)束那就正常完成,如果「某個步驟失敗,則根據(jù)相反順序一次調(diào)用補償操作」。
Seata框架中一個分布式事務包含3種角色:
「Transaction Coordinator (TC)」:事務協(xié)調(diào)器,維護全局事務的運行狀態(tài),負責協(xié)調(diào)并驅(qū)動全局事務的提交或回滾。「Transaction Manager (TM)」:控制全局事務的邊界,負責開啟一個全局事務,并最終發(fā)起全局提交或全局回滾的決議。「Resource Manager (RM)」:控制分支事務,負責分支注冊、狀態(tài)匯報,并接收事務協(xié)調(diào)器的指令,驅(qū)動分支(本地)事務的提交和回滾。
seata框架「為每一個RM維護了一張UNDO_LOG表」,其中保存了每一次本地事務的回滾數(shù)據(jù)。
具體流程:1.首先TM 向 TC 申請「開啟一個全局事務」,全局事務「創(chuàng)建」成功并生成一個「全局唯一的 XID」。
2.XID 在微服務調(diào)用鏈路的上下文中傳播。
3.RM 開始執(zhí)行這個分支事務,RM首先解析這條SQL語句,「生成對應的UNDO_LOG記錄」。下面是一條UNDO_LOG中的記錄,UNDO_LOG表中記錄了分支ID,全局事務ID,以及事務執(zhí)行的redo和undo數(shù)據(jù)以供二階段恢復。
4.RM在同一個本地事務中「執(zhí)行業(yè)務SQL和UNDO_LOG數(shù)據(jù)的插入」。在提交這個本地事務前,RM會向TC「申請關(guān)于這條記錄的全局鎖」。
如果申請不到,則說明有其他事務也在對這條記錄進行操作,因此它會在一段時間內(nèi)重試,重試失敗則回滾本地事務,并向TC匯報本地事務執(zhí)行失敗。
6.RM在事務提交前,「申請到了相關(guān)記錄的全局鎖」,然后直接提交本地事務,并向TC「匯報本地事務執(zhí)行成功」。此時全局鎖并沒有釋放,全局鎖的釋放取決于二階段是提交命令還是回滾命令。
7.TC根據(jù)所有的分支事務執(zhí)行結(jié)果,向RM「下發(fā)提交或回滾」命令。
RM如果「收到TC的提交命令」,首先「立即釋放」相關(guān)記錄的全局「鎖」,然后把提交請求放入一個異步任務的隊列中,馬上返回提交成功的結(jié)果給 TC。異步隊列中的提交請求真正執(zhí)行時,只是刪除相應 UNDO LOG 記錄而已。
RM如果「收到TC的回滾命令」,則會開啟一個本地事務,通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄。將 UNDO LOG 中的后鏡與當前數(shù)據(jù)進行比較,
如果不同,說明數(shù)據(jù)被當前全局事務之外的動作做了修改。這種情況,需要根據(jù)配置策略來做處理。 如果相同,根據(jù) UNDO LOG 中的前鏡像和業(yè)務 SQL 的相關(guān)信息生成并執(zhí)行回滾的語句并執(zhí)行,然后提交本地事務達到回滾的目的,最后釋放相關(guān)記錄的全局鎖。
總結(jié)
本文介紹了分布式事務的一些基礎理論,并對常用的分布式事務方案進行了講解。
分布式事務本身就是一個技術(shù)難題,業(yè)務中具體使用哪種方案還是需要不同的業(yè)務特點自行選擇,但是我們也會發(fā)現(xiàn),分布式事務會大大的提高流程的復雜度,會帶來很多額外的開銷工作,「代碼量上去了,業(yè)務復雜了,性能下跌了」。
所以,當我們真實開發(fā)的過程中,能不使用分布式事務就不使用。
