對(duì)比7種分布式事務(wù)方案,還是偏愛(ài)阿里開(kāi)源的Seata,真香!
前言
這篇文章主要介紹一些目前主流的幾種分布式解決方案以及阿里開(kāi)源的一站式分布式解決方案Seata。
文章有點(diǎn)長(zhǎng),耐心看完,看完你還不懂分布式事務(wù),歡迎來(lái)捶我......
文章目錄如下:

什么是分布式事務(wù)?
分布式對(duì)應(yīng)的是單體架構(gòu),互聯(lián)網(wǎng)早起單體架構(gòu)是非常流行的,好像是一個(gè)家族企業(yè),大家在一個(gè)家里勞作,單體架構(gòu)如下圖:

但是隨著業(yè)務(wù)的復(fù)雜度提高,大家族人手不夠,此時(shí)不得不招人,這樣逐漸演變出了分布式服務(wù),互相協(xié)作,每個(gè)服務(wù)負(fù)責(zé)不同的業(yè)務(wù),架構(gòu)如下圖:

因此需要服務(wù)與服務(wù)之間的遠(yuǎn)程協(xié)作才能完成事務(wù),這種分布式系統(tǒng)環(huán)境下由不同的服務(wù)之間通過(guò)網(wǎng)絡(luò)遠(yuǎn)程協(xié)作完成事務(wù)稱(chēng)之為分布式事務(wù),例如用戶(hù)注冊(cè)送積分 事務(wù)、創(chuàng)建訂單減庫(kù)存事務(wù),銀行轉(zhuǎn)賬事務(wù)等都是分布式事務(wù)。
典型的場(chǎng)景就是微服務(wù)架構(gòu) 微服務(wù)之間通過(guò)遠(yuǎn)程調(diào)用完成事務(wù)操作。比如:訂單微服務(wù)和庫(kù)存微服務(wù),下單的同時(shí)訂單微服務(wù)請(qǐng)求庫(kù)存微服務(wù)減庫(kù)存。簡(jiǎn)言之:跨JVM進(jìn)程產(chǎn)生分布式事務(wù)。
什么是CAP原則?
CAP原則又叫CAP定理,同時(shí)又被稱(chēng)作布魯爾定理(Brewer's theorem),指的是在一個(gè)分布式系統(tǒng)中,不可能同時(shí)滿(mǎn)足以下三點(diǎn)。

一致性(Consistency)
指強(qiáng)一致性,在寫(xiě)操作完成后開(kāi)始的任何讀操作都必須返回該值,或者后續(xù)寫(xiě)操作的結(jié)果。
也就是說(shuō),在一致性系統(tǒng)中,一旦客戶(hù)端將值寫(xiě)入任何一臺(tái)服務(wù)器并獲得響應(yīng),那么之后client從其他任何服務(wù)器讀取的都是剛寫(xiě)入的數(shù)據(jù)
一致性保證了不管向哪臺(tái)服務(wù)器寫(xiě)入數(shù)據(jù),其他的服務(wù)器能實(shí)時(shí)同步數(shù)據(jù)
可用性(Availability)
可用性(高可用)是指:每次向未崩潰的節(jié)點(diǎn)發(fā)送請(qǐng)求,總能保證收到響應(yīng)數(shù)據(jù)(允許不是最新數(shù)據(jù))
分區(qū)容忍性(Partition tolerance)
分布式系統(tǒng)在遇到任何網(wǎng)絡(luò)分區(qū)故障的時(shí)候,仍然能夠?qū)ν馓峁M(mǎn)足一致性和可用性的服務(wù),也就是說(shuō),服務(wù)器A和B發(fā)送給對(duì)方的任何消息都是可以放棄的,也就是說(shuō)A和B可能因?yàn)楦鞣N意外情況,導(dǎo)致無(wú)法成功進(jìn)行同步,分布式系統(tǒng)要能容忍這種情況。除非整個(gè)網(wǎng)絡(luò)環(huán)境都發(fā)生了故障。
為什么只能在A和C之間做出取舍?
分布式系統(tǒng)中,必須滿(mǎn)足 CAP 中的 P,此時(shí)只能在 C/A 之間作出取舍。
如果選擇了CA,舍棄了P,說(shuō)白了就是一個(gè)單體架構(gòu)。
一致性有幾種分類(lèi)?
CAP理論告訴我們只能在C、A之間選擇,在分布式事務(wù)的最終解決方案中一般選擇犧牲一致性來(lái)獲取可用性和分區(qū)容錯(cuò)性。
這里的 “犧牲一致性” 并不是完全放棄數(shù)據(jù)的一致性,而是放棄強(qiáng)一致性而換取弱一致性。
一致性可以分為以下三種:
強(qiáng)一致性 弱一致性 最終一致性
強(qiáng)一致性
系統(tǒng)中的某個(gè)數(shù)據(jù)被成功更新后,后續(xù)任何對(duì)該數(shù)據(jù)的讀取操作都將得到更新后的值。
也稱(chēng)為:原子一致性(Atomic Consistency)、線(xiàn)性一致性(Linearizable Consistency)
簡(jiǎn)言之,在任意時(shí)刻,所有節(jié)點(diǎn)中的數(shù)據(jù)是一樣的。例如,對(duì)于關(guān)系型數(shù)據(jù)庫(kù),要求更新過(guò)的數(shù)據(jù)能被后續(xù)的訪(fǎng)問(wèn)都能看到,這是強(qiáng)一致性。
總結(jié):
一個(gè)集群需要對(duì)外部提供強(qiáng)一致性,所以只要集群內(nèi)部某一臺(tái)服務(wù)器的數(shù)據(jù)發(fā)生了改變,那么就需要等待集群內(nèi)其他服務(wù)器的數(shù)據(jù)同步完成后,才能正常的對(duì)外提供服務(wù)。 保證了強(qiáng)一致性,務(wù)必會(huì)損耗可用性。
弱一致性
系統(tǒng)中的某個(gè)數(shù)據(jù)被更新后,后續(xù)對(duì)該數(shù)據(jù)的讀取操作可能得到更新后的值,也可能是更改前的值。
但即使過(guò)了不一致時(shí)間窗口這段時(shí)間后,后續(xù)對(duì)該數(shù)據(jù)的讀取也不一定是最新值。
所以說(shuō),可以理解為數(shù)據(jù)更新后,如果能容忍后續(xù)的訪(fǎng)問(wèn)只能訪(fǎng)問(wèn)到部分或者全部訪(fǎng)問(wèn)不到,則是弱一致性。
例如12306買(mǎi)火車(chē)票,雖然最后看到還剩下幾張余票,但是只要選擇購(gòu)買(mǎi)就會(huì)提示沒(méi)票了,這就是弱一致性。
最終一致性
是弱一致性的特殊形式,存儲(chǔ)系統(tǒng)保證在沒(méi)有新的更新的條件下,最終所有的訪(fǎng)問(wèn)都是最后更新的值。
不保證在任意時(shí)刻任意節(jié)點(diǎn)上的同一份數(shù)據(jù)都是相同的,但是隨著時(shí)間的遷移,不同節(jié)點(diǎn)上的同一份數(shù)據(jù)總是在向趨同的方向變化。
簡(jiǎn)單說(shuō),就是在一段時(shí)間后,節(jié)點(diǎn)間的數(shù)據(jù)會(huì)最終達(dá)到一致?tīng)顟B(tài)。
總結(jié)
弱一致性即使過(guò)了不一致時(shí)間窗口,后續(xù)的讀取也不一定能保證一致,而最終一致過(guò)了不一致窗口后,后續(xù)的讀取一定一致。
什么是Base理論?
BASE理論是對(duì)CAP中的一致性和可用性進(jìn)行一個(gè)權(quán)衡的結(jié)果,理論的核心思想就是:我們無(wú)法做到強(qiáng)一致,但每個(gè)應(yīng)用都可以根據(jù)自身的業(yè)務(wù)特點(diǎn),采用適當(dāng)?shù)姆绞絹?lái)使系統(tǒng)達(dá)到最終一致性。
BA(Basic Available)基本可用
整個(gè)系統(tǒng)在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時(shí)間內(nèi)仍然能夠返回一個(gè)明確的結(jié)果。這里是屬于基本可用。
基本可用和高可用的區(qū)別:
“一定時(shí)間”可以適當(dāng)延長(zhǎng) 當(dāng)舉行大促(比如秒殺)時(shí),響應(yīng)時(shí)間可以適當(dāng)延長(zhǎng) 給部分用戶(hù)返回一個(gè)降級(jí)頁(yè)面 給部分用戶(hù)直接返回一個(gè)降級(jí)頁(yè)面,從而緩解服務(wù)器壓力。但要注意,返回降級(jí)頁(yè)面仍然是返回明確結(jié)果。
S(Soft State)柔性狀態(tài)
稱(chēng)為柔性狀態(tài),是指允許系統(tǒng)中的數(shù)據(jù)存在中間狀態(tài),并認(rèn)為該中間狀態(tài)的存在不會(huì)影響系統(tǒng)的整體可用性,即允許系統(tǒng)不同節(jié)點(diǎn)的數(shù)據(jù)副本之間進(jìn)行數(shù)據(jù)同步的過(guò)程存在延時(shí)。
E(Eventual Consisstency)最終一致性
同一數(shù)據(jù)的不同副本的狀態(tài),可以不需要實(shí)時(shí)一致,但一定要保證經(jīng)過(guò)一定時(shí)間后仍然是一致的。
分布式事務(wù)有哪幾種解決方案?
在分布式架構(gòu)下,每個(gè)節(jié)點(diǎn)只知曉自己操作的失敗或者成功,無(wú)法得知其他節(jié)點(diǎn)的狀態(tài)。當(dāng)一個(gè)事務(wù)跨多個(gè)節(jié)點(diǎn)時(shí),為了保持事務(wù)的原子性與一致性,而引入一個(gè)協(xié)調(diào)者來(lái)統(tǒng)一掌控所有參與者的操作結(jié)果,并指示它們是否要把操作結(jié)果進(jìn)行真正的提交或者回滾(rollback)。
2階段提交(2PC)
二階段提交協(xié)議(Two-phase Commit,即 2PC)是常用的分布式事務(wù)解決方案,即將事務(wù)的提交過(guò)程分為兩個(gè)階段來(lái)進(jìn)行處理。
兩個(gè)階段分別為:
準(zhǔn)備階段 提交階段
參與的角色:
事務(wù)協(xié)調(diào)者(事務(wù)管理器):事務(wù)的發(fā)起者 事務(wù)參與者(資源管理器):事務(wù)的執(zhí)行者
準(zhǔn)備階段(投票階段)
這是兩階段的第一段,這一階段只是準(zhǔn)備階段,由事務(wù)的協(xié)調(diào)者發(fā)起詢(xún)問(wèn)參與者是否可以提交事務(wù),但是這一階段并未提交事務(wù),流程圖如下圖:

協(xié)調(diào)者向所有參與者發(fā)送事務(wù)內(nèi)容,詢(xún)問(wèn)是否可以提交事務(wù),并等待答復(fù) 各參與者執(zhí)行事務(wù)操作,將 undo 和 redo 信息記入事務(wù)日志中(但不提交事務(wù)) 如參與者執(zhí)行成功,給協(xié)調(diào)者反饋同意,否則反饋中止
提交階段
這一段階段屬于2PC的第二階段(提交 執(zhí)行階段),協(xié)調(diào)者發(fā)起正式提交事務(wù)的請(qǐng)求,當(dāng)所有參與者都回復(fù)同意時(shí),則意味著完成事務(wù),流程圖如下:

協(xié)調(diào)者節(jié)點(diǎn)向所有參與者節(jié)點(diǎn)發(fā)出正式提交( commit)的請(qǐng)求。參與者節(jié)點(diǎn)正式完成操作,并釋放在整個(gè)事務(wù)期間內(nèi)占用的資源。 參與者節(jié)點(diǎn)向協(xié)調(diào)者節(jié)點(diǎn)發(fā)送ack完成消息。 協(xié)調(diào)者節(jié)點(diǎn)收到所有參與者節(jié)點(diǎn)反饋的ack完成消息后,完成事務(wù)。
但是如果任意一個(gè)參與者節(jié)點(diǎn)在第一階段返回的消息為終止,或者協(xié)調(diào)者節(jié)點(diǎn)在第一階段的詢(xún)問(wèn)超時(shí)之前無(wú)法獲取所有參與者節(jié)點(diǎn)的響應(yīng)消息時(shí),那么這個(gè)事務(wù)將會(huì)被回滾,回滾的流程圖如下:

協(xié)調(diào)者節(jié)點(diǎn)向所有參與者節(jié)點(diǎn)發(fā)出回滾操作( rollback)的請(qǐng)求。參與者節(jié)點(diǎn)利用階段1寫(xiě)入的undo信息執(zhí)行回滾,并釋放在整個(gè)事務(wù)期間內(nèi)占用的資源。 參與者節(jié)點(diǎn)向協(xié)調(diào)者節(jié)點(diǎn)發(fā)送ack回滾完成消息。 協(xié)調(diào)者節(jié)點(diǎn)受到所有參與者節(jié)點(diǎn)反饋的ack回滾完成消息后,取消事務(wù)。
不管最后結(jié)果如何,第二階段都會(huì)結(jié)束當(dāng)前事務(wù)。
二階段提交的事務(wù)正常提交的完整流程如下圖:

二階段提交事務(wù)回滾的完整流程如下圖:

舉個(gè)百米賽跑的例子來(lái)具體描述下2PC的流程:學(xué)校運(yùn)動(dòng)會(huì),有三個(gè)同學(xué),分別是A,B,C,2PC流程如下:
裁判:A同學(xué)準(zhǔn)備好了嗎?準(zhǔn)備進(jìn)入第一賽道.... 裁判:B同學(xué)準(zhǔn)備好了嗎?準(zhǔn)備進(jìn)入第一賽道.... 裁判:C同學(xué)準(zhǔn)備好了嗎?準(zhǔn)備進(jìn)入第一賽道.... 如果有任意一個(gè)同學(xué)沒(méi)準(zhǔn)備好,則裁判下達(dá)回滾指令 如果裁判收到了所有同學(xué)的OK回復(fù),則再次下令跑...... 裁判:1,2,3 跑............ A同學(xué)沖刺到終點(diǎn),匯報(bào)給裁判 B,C同學(xué)沖刺失敗,匯報(bào)給裁判
2PC的缺點(diǎn)
二階段提交看起來(lái)確實(shí)能夠提供原子性的操作,但是不幸的是,二階段提交還是有幾個(gè)缺點(diǎn)的:
性能問(wèn)題:執(zhí)行過(guò)程中,所有參與節(jié)點(diǎn)都是事務(wù)阻塞型的。當(dāng)參與者占有公共資源時(shí),其他第三方節(jié)點(diǎn)訪(fǎng)問(wèn)公共資源不得不處于阻塞狀態(tài)。 可靠性問(wèn)題:參與者發(fā)生故障。協(xié)調(diào)者需要給每個(gè)參與者額外指定超時(shí)機(jī)制,超時(shí)后整個(gè)事務(wù)失敗。協(xié)調(diào)者發(fā)生故障。參與者會(huì)一直阻塞下去。需要額外的備機(jī)進(jìn)行容錯(cuò)。 數(shù)據(jù)一致性問(wèn)題:二階段無(wú)法解決的問(wèn)題:協(xié)調(diào)者在發(fā)出 commit消息之后宕機(jī),而唯一接收到這條消息的參與者同時(shí)也宕機(jī)了。那么即使協(xié)調(diào)者通過(guò)選舉協(xié)議產(chǎn)生了新的協(xié)調(diào)者,這條事務(wù)的狀態(tài)也是不確定的,沒(méi)人知道事務(wù)是否被已經(jīng)提交。實(shí)現(xiàn)復(fù)雜:犧牲了可用性,對(duì)性能影響較大,不適合高并發(fā)高性能場(chǎng)景。
2PC的優(yōu)點(diǎn)
盡量保證了數(shù)據(jù)的強(qiáng)一致,適合對(duì)數(shù)據(jù)強(qiáng)一致要求很高的關(guān)鍵領(lǐng)域。(其實(shí)也不能100%保證強(qiáng)一致)
3階段提交(3PC)
三階段提交協(xié)議,是二階段提交協(xié)議的改進(jìn)版本,三階段提交有兩個(gè)改動(dòng)點(diǎn)。
在協(xié)調(diào)者和參與者中都引入超時(shí)機(jī)制 在第一階段和第二階段中插入一個(gè)準(zhǔn)備階段。保證了在最后提交階段之前各參與節(jié)點(diǎn)的狀態(tài)是一致的。
也就是說(shuō),除了引入超時(shí)機(jī)制之外,3PC把2PC的準(zhǔn)備階段再次一分為二,這樣三階段提交就有CanCommit、PreCommit、DoCommit三個(gè)階段。處理流程如下:

階段一:CanCommit階段
3PC的CanCommit階段其實(shí)和2PC的準(zhǔn)備階段很像。協(xié)調(diào)者向參與者發(fā)送commit請(qǐng)求,參與者如果可以提交就返回Yes響應(yīng),否則返回No響應(yīng)。
事務(wù)詢(xún)問(wèn):協(xié)調(diào)者向所有參與者發(fā)出包含事務(wù)內(nèi)容的 canCommit請(qǐng)求,詢(xún)問(wèn)是否可以提交事務(wù),并等待所有參與者答復(fù)。響應(yīng)反饋:參與者收到 canCommit請(qǐng)求后,如果認(rèn)為可以執(zhí)行事務(wù)操作,則反饋 yes 并進(jìn)入預(yù)備狀態(tài),否則反饋 no。
CanCommit階段流程如下圖:

階段二:PreCommit階段
協(xié)調(diào)者根據(jù)參與者的反應(yīng)情況來(lái)決定是否可以進(jìn)行事務(wù)的PreCommit操作。根據(jù)響應(yīng)情況,有以下兩種可能。
假如所有參與者均反饋 yes,協(xié)調(diào)者預(yù)執(zhí)行事務(wù)。 發(fā)送預(yù)提交請(qǐng)求 :協(xié)調(diào)者向參與者發(fā)送 PreCommit請(qǐng)求,并進(jìn)入準(zhǔn)備階段事務(wù)預(yù)提交 :參與者接收到 PreCommit請(qǐng)求后,會(huì)執(zhí)行事務(wù)操作,并將undo和redo信息記錄到事務(wù)日志中(但不提交事務(wù))響應(yīng)反饋 :如果參與者成功的執(zhí)行了事務(wù)操作,則返回ACK響應(yīng),同時(shí)開(kāi)始等待最終指令。

假如有任何一個(gè)參與者向協(xié)調(diào)者發(fā)送了No響應(yīng),或者等待超時(shí)之后,協(xié)調(diào)者都沒(méi)有接到參與者的響應(yīng),那么就執(zhí)行事務(wù)的中斷。 發(fā)送中斷請(qǐng)求 :協(xié)調(diào)者向所有參與者發(fā)送 abort請(qǐng)求。中斷事務(wù) :參與者收到來(lái)自協(xié)調(diào)者的 abort請(qǐng)求之后(或超時(shí)之后,仍未收到協(xié)調(diào)者的請(qǐng)求),執(zhí)行事務(wù)的中斷。

階段三:doCommit階段
該階段進(jìn)行真正的事務(wù)提交,也可以分為以下兩種情況。
進(jìn)入階段 3 后,無(wú)論協(xié)調(diào)者出現(xiàn)問(wèn)題,或者協(xié)調(diào)者與參與者網(wǎng)絡(luò)出現(xiàn)問(wèn)題,都會(huì)導(dǎo)致參與者無(wú)法接收到協(xié)調(diào)者發(fā)出的 do Commit 請(qǐng)求或 abort 請(qǐng)求。此時(shí),參與者都會(huì)在等待超時(shí)之后,繼續(xù)執(zhí)行事務(wù)提交。
執(zhí)行提交 發(fā)送提交請(qǐng)求 協(xié)調(diào)接收到參與者發(fā)送的ACK響應(yīng),那么他將從預(yù)提交狀態(tài)進(jìn)入到提交狀態(tài)。并向所有參與者發(fā)送 doCommit請(qǐng)求。事務(wù)提交 參與者接收到 doCommit請(qǐng)求之后,執(zhí)行正式的事務(wù)提交。并在完成事務(wù)提交之后釋放所有事務(wù)資源。響應(yīng)反饋 事務(wù)提交完之后,向協(xié)調(diào)者發(fā)送ack響應(yīng)。 完成事務(wù) 協(xié)調(diào)者接收到所有參與者的ack響應(yīng)之后,完成事務(wù)。

中斷事務(wù):任何一個(gè)參與者反饋 no,或者等待超時(shí)后協(xié)調(diào)者尚無(wú)法收到所有參與者的反饋,即中斷事務(wù) 發(fā)送中斷請(qǐng)求 如果協(xié)調(diào)者處于工作狀態(tài),向所有參與者發(fā)出 abort 請(qǐng)求 事務(wù)回滾 參與者接收到abort請(qǐng)求之后,利用其在階段二記錄的undo信息來(lái)執(zhí)行事務(wù)的回滾操作,并在完成回滾之后釋放所有的事務(wù)資源。 反饋結(jié)果 參與者完成事務(wù)回滾之后,向協(xié)調(diào)者反饋ACK消息 中斷事務(wù) 協(xié)調(diào)者接收到參與者反饋的ACK消息之后,執(zhí)行事務(wù)的中斷。

優(yōu)點(diǎn)
相比二階段提交,三階段提交降低了阻塞范圍,在等待超時(shí)后協(xié)調(diào)者或參與者會(huì)中斷事務(wù)。避免了協(xié)調(diào)者單點(diǎn)問(wèn)題,階段 3 中協(xié)調(diào)者出現(xiàn)問(wèn)題時(shí),參與者會(huì)繼續(xù)提交事務(wù)。
缺點(diǎn)
數(shù)據(jù)不一致問(wèn)題依然存在,當(dāng)在參與者收到 preCommit 請(qǐng)求后等待 doCommit 指令時(shí),此時(shí)如果協(xié)調(diào)者請(qǐng)求中斷事務(wù),而協(xié)調(diào)者無(wú)法與參與者正常通信,會(huì)導(dǎo)致參與者繼續(xù)提交事務(wù),造成數(shù)據(jù)不一致。
TCC(事務(wù)補(bǔ)償)
TCC(Try Confirm Cancel)方案是一種應(yīng)用層面侵入業(yè)務(wù)的兩階段提交。是目前最火的一種柔性事務(wù)方案,其核心思想是:針對(duì)每個(gè)操作,都要注冊(cè)一個(gè)與其對(duì)應(yīng)的確認(rèn)和補(bǔ)償(撤銷(xiāo))操作。
TCC分為兩個(gè)階段,分別如下:
第一階段:Try(嘗試),主要是對(duì)業(yè)務(wù)系統(tǒng)做檢測(cè)及資源預(yù)留 (加鎖,鎖住資源) 第二階段:本階段根據(jù)第一階段的結(jié)果,決定是執(zhí)行confirm還是cancel Confirm(確認(rèn)):執(zhí)行真正的業(yè)務(wù)(執(zhí)行業(yè)務(wù),釋放鎖) Cancle(取消):是預(yù)留資源的取消(出問(wèn)題,釋放鎖)

為了方便理解,下面以電商下單為例進(jìn)行方案解析,這里把整個(gè)過(guò)程簡(jiǎn)單分為扣減庫(kù)存,訂單創(chuàng)建 2 個(gè)步驟,庫(kù)存服務(wù)和訂單服務(wù)分別在不同的服務(wù)器節(jié)點(diǎn)上。
假設(shè)商品庫(kù)存為 100,購(gòu)買(mǎi)數(shù)量為 2,這里檢查和更新庫(kù)存的同時(shí),凍結(jié)用戶(hù)購(gòu)買(mǎi)數(shù)量的庫(kù)存,同時(shí)創(chuàng)建訂單,訂單狀態(tài)為待確認(rèn)。
①Try 階段
TCC 機(jī)制中的 Try 僅是一個(gè)初步操作,它和后續(xù)的確認(rèn)一起才能真正構(gòu)成一個(gè)完整的業(yè)務(wù)邏輯,這個(gè)階段主要完成:
完成所有業(yè)務(wù)檢查( 一致性 ) 。 預(yù)留必須業(yè)務(wù)資源( 準(zhǔn)隔離性 ) 。 Try 嘗試執(zhí)行業(yè)務(wù)。

②Confirm / Cancel 階段
根據(jù) Try 階段服務(wù)是否全部正常執(zhí)行,繼續(xù)執(zhí)行確認(rèn)操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作滿(mǎn)足冪等性,如果 Confirm 或 Cancel 操作執(zhí)行失敗,將會(huì)不斷重試直到執(zhí)行完成。
Confirm:當(dāng) Try 階段服務(wù)全部正常執(zhí)行, 執(zhí)行確認(rèn)業(yè)務(wù)邏輯操作,業(yè)務(wù)如下圖:

這里使用的資源一定是 Try 階段預(yù)留的業(yè)務(wù)資源。在 TCC 事務(wù)機(jī)制中認(rèn)為,如果在 Try 階段能正常的預(yù)留資源,那 Confirm 一定能完整正確的提交。
Confirm 階段也可以看成是對(duì) Try 階段的一個(gè)補(bǔ)充,Try+Confirm 一起組成了一個(gè)完整的業(yè)務(wù)邏輯。
Cancel:當(dāng) Try 階段存在服務(wù)執(zhí)行失敗, 進(jìn)入 Cancel 階段,業(yè)務(wù)如下圖:

Cancel 取消執(zhí)行,釋放 Try 階段預(yù)留的業(yè)務(wù)資源,上面的例子中,Cancel 操作會(huì)把凍結(jié)的庫(kù)存釋放,并更新訂單狀態(tài)為取消。
最終一致性保證
TCC 事務(wù)機(jī)制以初步操作(Try)為中心的,確認(rèn)操作(Confirm)和取消操作(Cancel)都是圍繞初步操作(Try)而展開(kāi)。因此,Try 階段中的操作,其保障性是最好的,即使失敗,仍然有取消操作(Cancel)可以將其執(zhí)行結(jié)果撤銷(xiāo)。 Try階段執(zhí)行成功并開(kāi)始執(zhí)行 Confirm階段時(shí),默認(rèn)Confirm階段是不會(huì)出錯(cuò)的。也就是說(shuō)只要Try成功,Confirm一定成功(TCC設(shè)計(jì)之初的定義) 。Confirm與Cancel如果失敗,由TCC框架進(jìn)行==重試==補(bǔ)償 存在極低概率在CC環(huán)節(jié)徹底失敗,則需要定時(shí)任務(wù)或人工介入
方案總結(jié)
TCC 事務(wù)機(jī)制相對(duì)于傳統(tǒng)事務(wù)機(jī)制(X/Open XA),TCC 事務(wù)機(jī)制相比于上面介紹的 XA 事務(wù)機(jī)制,有以下優(yōu)點(diǎn):
性能提升:具體業(yè)務(wù)來(lái)實(shí)現(xiàn)控制資源鎖的粒度變小,不會(huì)鎖定整個(gè)資源。 數(shù)據(jù)最終一致性:基于 Confirm 和 Cancel 的冪等性,保證事務(wù)最終完成確認(rèn)或者取消,保證數(shù)據(jù)的一致性。 可靠性:解決了 XA 協(xié)議的協(xié)調(diào)者單點(diǎn)故障問(wèn)題,由主業(yè)務(wù)方發(fā)起并控制整個(gè)業(yè)務(wù)活動(dòng),業(yè)務(wù)活動(dòng)管理器也變成多點(diǎn),引入集群。
缺點(diǎn):
TCC 的 Try、Confirm 和 Cancel 操作功能要按具體業(yè)務(wù)來(lái)實(shí)現(xiàn),業(yè)務(wù)耦合度較高,提高了開(kāi)發(fā)成本。
本地消息表
本地消息表的方案最初是由 eBay 提出,核心思路是將分布式事務(wù)拆分成本地事務(wù)進(jìn)行處理。
角色:
事務(wù)主動(dòng)方 事務(wù)被動(dòng)方
通過(guò)在事務(wù)主動(dòng)發(fā)起方額外新建事務(wù)消息表,事務(wù)發(fā)起方處理業(yè)務(wù)和記錄事務(wù)消息在本地事務(wù)中完成,輪詢(xún)事務(wù)消息表的數(shù)據(jù)發(fā)送事務(wù)消息,事務(wù)被動(dòng)方基于消息中間件消費(fèi)事務(wù)消息表中的事務(wù)。
這樣可以避免以下兩種情況導(dǎo)致的數(shù)據(jù)不一致性:
業(yè)務(wù)處理成功、事務(wù)消息發(fā)送失敗 業(yè)務(wù)處理失敗、事務(wù)消息發(fā)送成功
整體的流程如下圖:

上圖中整體的處理步驟如下:
①:事務(wù)主動(dòng)方在同一個(gè)本地事務(wù)中處理業(yè)務(wù)和寫(xiě)消息表操作 ②:事務(wù)主動(dòng)方通過(guò)消息中間件,通知事務(wù)被動(dòng)方處理事務(wù)通知事務(wù)待消息。消息中間件可以基于 Kafka、RocketMQ 消息隊(duì)列,事務(wù)主動(dòng)方主動(dòng)寫(xiě)消息到消息隊(duì)列,事務(wù)消費(fèi)方消費(fèi)并處理消息隊(duì)列中的消息。 ③:事務(wù)被動(dòng)方通過(guò)消息中間件,通知事務(wù)主動(dòng)方事務(wù)已處理的消息。 ④:事務(wù)主動(dòng)方接收中間件的消息,更新消息表的狀態(tài)為已處理。
一些必要的容錯(cuò)處理如下:
當(dāng)①處理出錯(cuò),由于還在事務(wù)主動(dòng)方的本地事務(wù)中,直接回滾即可 當(dāng)②、③處理出錯(cuò),由于事務(wù)主動(dòng)方本地保存了消息,只需要輪詢(xún)消息重新通過(guò)消息中間件發(fā)送,事務(wù)被動(dòng)方重新讀取消息處理業(yè)務(wù)即可。 如果是業(yè)務(wù)上處理失敗,事務(wù)被動(dòng)方可以發(fā)消息給事務(wù)主動(dòng)方回滾事務(wù) 如果事務(wù)被動(dòng)方已經(jīng)消費(fèi)了消息,事務(wù)主動(dòng)方需要回滾事務(wù)的話(huà),需要發(fā)消息通知事務(wù)主動(dòng)方進(jìn)行回滾事務(wù)。
優(yōu)點(diǎn)
從應(yīng)用設(shè)計(jì)開(kāi)發(fā)的角度實(shí)現(xiàn)了消息數(shù)據(jù)的可靠性,消息數(shù)據(jù)的可靠性不依賴(lài)于消息中間件,弱化了對(duì) MQ 中間件特性的依賴(lài)。 方案輕量,容易實(shí)現(xiàn)。
缺點(diǎn)
與具體的業(yè)務(wù)場(chǎng)景綁定,耦合性強(qiáng),不可公用。 消息數(shù)據(jù)與業(yè)務(wù)數(shù)據(jù)同庫(kù),占用業(yè)務(wù)系統(tǒng)資源。 業(yè)務(wù)系統(tǒng)在使用關(guān)系型數(shù)據(jù)庫(kù)的情況下,消息服務(wù)性能會(huì)受到關(guān)系型數(shù)據(jù)庫(kù)并發(fā)性能的局限。
MQ事務(wù)方案(可靠消息事務(wù))
基于 MQ 的分布式事務(wù)方案其實(shí)是對(duì)本地消息表的封裝,將本地消息表基于 MQ 內(nèi)部,其他方面的協(xié)議基本與本地消息表一致。
MQ事務(wù)方案整體流程和本地消息表的流程很相似,如下圖:

從上圖可以看出和本地消息表方案唯一不同就是將本地消息表存在了MQ內(nèi)部,而不是業(yè)務(wù)數(shù)據(jù)庫(kù)中。
那么MQ內(nèi)部的處理尤為重要,下面主要基于 RocketMQ 4.3 之后的版本介紹 MQ 的分布式事務(wù)方案。
在本地消息表方案中,保證事務(wù)主動(dòng)方發(fā)寫(xiě)業(yè)務(wù)表數(shù)據(jù)和寫(xiě)消息表數(shù)據(jù)的一致性是基于數(shù)據(jù)庫(kù)事務(wù),RocketMQ 的事務(wù)消息相對(duì)于普通 MQ提供了 2PC 的提交接口,方案如下:
正常情況:事務(wù)主動(dòng)方發(fā)消息

這種情況下,事務(wù)主動(dòng)方服務(wù)正常,沒(méi)有發(fā)生故障,發(fā)消息流程如下:
步驟①:發(fā)送方向 MQ 服務(wù)端(MQ Server)發(fā)送 half 消息。 步驟②:MQ Server 將消息持久化成功之后,向發(fā)送方 ack 確認(rèn)消息已經(jīng)發(fā)送成功。 步驟③:發(fā)送方開(kāi)始執(zhí)行本地事務(wù)邏輯。 步驟④:發(fā)送方根據(jù)本地事務(wù)執(zhí)行結(jié)果向 MQ Server 提交二次確認(rèn)(commit 或是 rollback)。 步驟⑤:MQ Server 收到 commit 狀態(tài)則將半消息標(biāo)記為可投遞,訂閱方最終將收到該消息;MQ Server 收到 rollback 狀態(tài)則刪除半消息,訂閱方將不會(huì)接受該消息。
異常情況:事務(wù)主動(dòng)方消息恢復(fù)

在斷網(wǎng)或者應(yīng)用重啟等異常情況下,圖中 4 提交的二次確認(rèn)超時(shí)未到達(dá) MQ Server,此時(shí)處理邏輯如下:
步驟⑤:MQ Server 對(duì)該消息發(fā)起消息回查。 步驟⑥:發(fā)送方收到消息回查后,需要檢查對(duì)應(yīng)消息的本地事務(wù)執(zhí)行的最終結(jié)果。 步驟⑦:發(fā)送方根據(jù)檢查得到的本地事務(wù)的最終狀態(tài)再次提交二次確認(rèn)。 步驟⑧:MQ Server基于 commit/rollback 對(duì)消息進(jìn)行投遞或者刪除。
優(yōu)點(diǎn)
相比本地消息表方案,MQ 事務(wù)方案優(yōu)點(diǎn)是:
消息數(shù)據(jù)獨(dú)立存儲(chǔ) ,降低業(yè)務(wù)系統(tǒng)與消息系統(tǒng)之間的耦合。 吞吐量大于使用本地消息表方案。
缺點(diǎn)
一次消息發(fā)送需要兩次網(wǎng)絡(luò)請(qǐng)求(half 消息 + commit/rollback 消息) 。 業(yè)務(wù)處理服務(wù)需要實(shí)現(xiàn)消息狀態(tài)回查接口。
最大努力通知
最大努力通知也稱(chēng)為定期校對(duì),是對(duì)MQ事務(wù)方案的進(jìn)一步優(yōu)化。它在事務(wù)主動(dòng)方增加了消息校對(duì)的接口,如果事務(wù)被動(dòng)方?jīng)]有接收到消息,此時(shí)可以調(diào)用事務(wù)主動(dòng)方提供的消息校對(duì)的接口主動(dòng)獲取。
最大努力通知的整體流程如下圖:

在可靠消息事務(wù)中,事務(wù)主動(dòng)方需要將消息發(fā)送出去,并且消息接收方成功接收,這種可靠性發(fā)送是由事務(wù)主動(dòng)方保證的;
但是最大努力通知,事務(wù)主動(dòng)方盡最大努力(重試,輪詢(xún)....)將事務(wù)發(fā)送給事務(wù)接收方,但是仍然存在消息接收不到,此時(shí)需要事務(wù)被動(dòng)方主動(dòng)調(diào)用事務(wù)主動(dòng)方的消息校對(duì)接口查詢(xún)業(yè)務(wù)消息并消費(fèi),這種通知的可靠性是由事務(wù)被動(dòng)方保證的。
最大努力通知適用于業(yè)務(wù)通知類(lèi)型,例如微信交易的結(jié)果,就是通過(guò)最大努力通知方式通知各個(gè)商戶(hù),既有回調(diào)通知,也有交易查詢(xún)接口。
Saga 事務(wù)
Saga 事務(wù)源于 1987 年普林斯頓大學(xué)的 Hecto 和 Kenneth 發(fā)表的如何處理 long lived transaction(長(zhǎng)活事務(wù))論文。
Saga 事務(wù)核心思想是將長(zhǎng)事務(wù)拆分為多個(gè)本地短事務(wù),由 Saga 事務(wù)協(xié)調(diào)器協(xié)調(diào),如果正常結(jié)束那就正常完成,如果某個(gè)步驟失敗,則根據(jù)相反順序一次調(diào)用補(bǔ)償操作。
Saga 事務(wù)基本協(xié)議如下:
每個(gè) Saga 事務(wù)由一系列冪等的有序子事務(wù)(sub-transaction) Ti 組成。 每個(gè) Ti 都有對(duì)應(yīng)的冪等補(bǔ)償動(dòng)作 Ci,補(bǔ)償動(dòng)作用于撤銷(xiāo) Ti 造成的結(jié)果。
TCC事務(wù)補(bǔ)償機(jī)制有一個(gè)預(yù)留(Try)動(dòng)作,相當(dāng)于先報(bào)存一個(gè)草稿,然后才提交;Saga事務(wù)沒(méi)有預(yù)留動(dòng)作,直接提交。
對(duì)于事務(wù)異常,Saga提供了兩種恢復(fù)策略,分別如下:
向后恢復(fù)(backward recovery)
在執(zhí)行事務(wù)失敗時(shí),補(bǔ)償所有已完成的事務(wù),是“一退到底”的方式。如下圖:

從上圖可知事務(wù)執(zhí)行到了支付事務(wù)T3,但是失敗了,因此事務(wù)回滾需要從C3,C2,C1依次進(jìn)行回滾補(bǔ)償。
對(duì)應(yīng)的執(zhí)行順序?yàn)椋篢1,T2,T3,C3,C2,C1
這種做法的效果是撤銷(xiāo)掉之前所有成功的子事務(wù),使得整個(gè) Saga 的執(zhí)行結(jié)果撤銷(xiāo)。
向前恢復(fù)(forward recovery)
也稱(chēng)之為:勇往直前,對(duì)于執(zhí)行不通過(guò)的事務(wù),會(huì)嘗試重試事務(wù),這里有一個(gè)假設(shè)就是每個(gè)子事務(wù)最終都會(huì)成功。
流程如下圖:

適用于必須要成功的場(chǎng)景,事務(wù)失敗了重試,不需要補(bǔ)償。
Saga事務(wù)有兩種不同的實(shí)現(xiàn)方式,分別如下:
命令協(xié)調(diào)(Order Orchestrator) 事件編排(Event Choreographyo)
命令協(xié)調(diào)
中央?yún)f(xié)調(diào)器(Orchestrator,簡(jiǎn)稱(chēng) OSO)以命令/回復(fù)的方式與每項(xiàng)服務(wù)進(jìn)行通信,全權(quán)負(fù)責(zé)告訴每個(gè)參與者該做什么以及什么時(shí)候該做什么。整體流程如下圖:

上圖步驟如下:
事務(wù)發(fā)起方的主業(yè)務(wù)邏輯請(qǐng)求 OSO 服務(wù)開(kāi)啟訂單事務(wù) OSO 向庫(kù)存服務(wù)請(qǐng)求扣減庫(kù)存,庫(kù)存服務(wù)回復(fù)處理結(jié)果。 OSO 向訂單服務(wù)請(qǐng)求創(chuàng)建訂單,訂單服務(wù)回復(fù)創(chuàng)建結(jié)果。 OSO 向支付服務(wù)請(qǐng)求支付,支付服務(wù)回復(fù)處理結(jié)果。 主業(yè)務(wù)邏輯接收并處理 OSO 事務(wù)處理結(jié)果回復(fù)。
中央?yún)f(xié)調(diào)器必須事先知道執(zhí)行整個(gè)訂單事務(wù)所需的流程(例如通過(guò)讀取配置)。如果有任何失敗,它還負(fù)責(zé)通過(guò)向每個(gè)參與者發(fā)送命令來(lái)撤銷(xiāo)之前的操作來(lái)協(xié)調(diào)分布式的回滾。
基于中央?yún)f(xié)調(diào)器協(xié)調(diào)一切時(shí),回滾要容易得多,因?yàn)閰f(xié)調(diào)器默認(rèn)是執(zhí)行正向流程,回滾時(shí)只要執(zhí)行反向流程即可。
事件編排
沒(méi)有中央?yún)f(xié)調(diào)器(沒(méi)有單點(diǎn)風(fēng)險(xiǎn))時(shí),每個(gè)服務(wù)產(chǎn)生并觀察其他服務(wù)的事件,并決定是否應(yīng)采取行動(dòng)。
在事件編排方法中,第一個(gè)服務(wù)執(zhí)行一個(gè)事務(wù),然后發(fā)布一個(gè)事件。該事件被一個(gè)或多個(gè)服務(wù)進(jìn)行監(jiān)聽(tīng),這些服務(wù)再執(zhí)行本地事務(wù)并發(fā)布(或不發(fā)布)新的事件。
當(dāng)最后一個(gè)服務(wù)執(zhí)行本地事務(wù)并且不發(fā)布任何事件時(shí),意味著分布式事務(wù)結(jié)束,或者它發(fā)布的事件沒(méi)有被任何 Saga 參與者聽(tīng)到都意味著事務(wù)結(jié)束。

上圖步驟如下:
事務(wù)發(fā)起方的主業(yè)務(wù)邏輯發(fā)布開(kāi)始訂單事件。 庫(kù)存服務(wù)監(jiān)聽(tīng)開(kāi)始訂單事件,扣減庫(kù)存,并發(fā)布庫(kù)存已扣減事件。 訂單服務(wù)監(jiān)聽(tīng)?zhēng)齑嬉芽蹨p事件,創(chuàng)建訂單,并發(fā)布訂單已創(chuàng)建事件。 支付服務(wù)監(jiān)聽(tīng)訂單已創(chuàng)建事件,進(jìn)行支付,并發(fā)布訂單已支付事件。 主業(yè)務(wù)邏輯監(jiān)聽(tīng)訂單已支付事件并處理。
事件/編排是實(shí)現(xiàn) Saga 模式的自然方式,它很簡(jiǎn)單,容易理解,不需要太多的代碼來(lái)構(gòu)建。如果事務(wù)涉及 2 至 4 個(gè)步驟,則可能是非常合適的。
優(yōu)點(diǎn)
命令協(xié)調(diào)設(shè)計(jì)的優(yōu)點(diǎn)如下:
服務(wù)之間關(guān)系簡(jiǎn)單,避免服務(wù)之間的循環(huán)依賴(lài)關(guān)系,因?yàn)?Saga 協(xié)調(diào)器會(huì)調(diào)用 Saga 參與者,但參與者不會(huì)調(diào)用協(xié)調(diào)器。 程序開(kāi)發(fā)簡(jiǎn)單,只需要執(zhí)行命令/回復(fù)(其實(shí)回復(fù)消息也是一種事件消息),降低參與者的復(fù)雜性。 易維護(hù)擴(kuò)展,在添加新步驟時(shí),事務(wù)復(fù)雜性保持線(xiàn)性,回滾更容易管理,更容易實(shí)施和測(cè)試。
事件/編排設(shè)計(jì)優(yōu)點(diǎn)如下:
避免中央?yún)f(xié)調(diào)器單點(diǎn)故障風(fēng)險(xiǎn)。 當(dāng)涉及的步驟較少服務(wù)開(kāi)發(fā)簡(jiǎn)單,容易實(shí)現(xiàn)。
缺點(diǎn)
命令協(xié)調(diào)設(shè)計(jì)缺點(diǎn)如下:
中央?yún)f(xié)調(diào)器容易處理邏輯容易過(guò)于復(fù)雜,導(dǎo)致難以維護(hù)。 存在協(xié)調(diào)器單點(diǎn)故障風(fēng)險(xiǎn)。
事件/編排設(shè)計(jì)缺點(diǎn)如下:
服務(wù)之間存在循環(huán)依賴(lài)的風(fēng)險(xiǎn)。 當(dāng)涉及的步驟較多,服務(wù)間關(guān)系混亂,難以追蹤調(diào)測(cè)。
由于 Saga 模型中沒(méi)有 Prepare 階段,因此事務(wù)間不能保證隔離性。
當(dāng)多個(gè) Saga 事務(wù)操作同一資源時(shí),就會(huì)產(chǎn)生更新丟失、臟數(shù)據(jù)讀取等問(wèn)題,這時(shí)需要在業(yè)務(wù)層控制并發(fā),例如:在應(yīng)用層面加鎖,或者應(yīng)用層面預(yù)先凍結(jié)資源。
總結(jié)
總結(jié)一下各個(gè)方案的常見(jiàn)的使用場(chǎng)景:
2PC/3PC:依賴(lài)于數(shù)據(jù)庫(kù),能夠很好的提供強(qiáng)一致性和強(qiáng)事務(wù)性,但相對(duì)來(lái)說(shuō)延遲比較高,比較適合傳統(tǒng)的單體應(yīng)用,在同一個(gè)方法中存在跨庫(kù)操作的情況,不適合高并發(fā)和高性能要求的場(chǎng)景。 TCC:適用于執(zhí)行時(shí)間確定且較短,實(shí)時(shí)性要求高,對(duì)數(shù)據(jù)一致性要求高,比如互聯(lián)網(wǎng)金融企業(yè)最核心的三個(gè)服務(wù):交易、支付、賬務(wù)。 本地消息表/MQ 事務(wù):都適用于事務(wù)中參與方支持操作冪等,對(duì)一致性要求不高,業(yè)務(wù)上能容忍數(shù)據(jù)不一致到一個(gè)人工檢查周期,事務(wù)涉及的參與方、參與環(huán)節(jié)較少,業(yè)務(wù)上有對(duì)賬/校驗(yàn)系統(tǒng)兜底。 Saga 事務(wù):由于 Saga 事務(wù)不能保證隔離性,需要在業(yè)務(wù)層控制并發(fā),適合于業(yè)務(wù)場(chǎng)景事務(wù)并發(fā)操作同一資源較少的情況。Saga 相比缺少預(yù)提交動(dòng)作,導(dǎo)致補(bǔ)償動(dòng)作的實(shí)現(xiàn)比較麻煩,例如業(yè)務(wù)是發(fā)送短信,補(bǔ)償動(dòng)作則得再發(fā)送一次短信說(shuō)明撤銷(xiāo),用戶(hù)體驗(yàn)比較差。Saga 事務(wù)較適用于補(bǔ)償動(dòng)作容易處理的場(chǎng)景。
什么是Seata?
上面講了這么多的分布式事務(wù)的理論知識(shí),都沒(méi)看到一個(gè)落地的實(shí)現(xiàn),這不是吹牛逼嗎?
Seata 是一款開(kāi)源的分布式事務(wù)解決方案,致力于提供高性能和簡(jiǎn)單易用的分布式事務(wù)服務(wù)。Seata 將為用戶(hù)提供了 AT、TCC、SAGA 和 XA 事務(wù)模式,為用戶(hù)打造一站式的分布式解決方案。
對(duì)業(yè)務(wù)無(wú)侵入:即減少技術(shù)架構(gòu)上的微服務(wù)化所帶來(lái)的分布式事務(wù)問(wèn)題對(duì)業(yè)務(wù)的侵入 高性能:減少分布式事務(wù)解決方案所帶來(lái)的性能消耗
官方文檔:https://seata.io/zh-cn/index.html
seata的幾種術(shù)語(yǔ):
TC(Transaction Coordinator):事務(wù)協(xié)調(diào)者。管理全局的分支事務(wù)的狀態(tài),用于全局性事務(wù)的提交和回滾。 TM(Transaction Manager):事務(wù)管理者。用于開(kāi)啟、提交或回滾事務(wù)。 RM(Resource Manager):資源管理器。用于分支事務(wù)上的資源管理,向 TC 注冊(cè)分支事務(wù),上報(bào)分支事務(wù)的狀態(tài),接收 TC 的命令來(lái)提交或者回滾分支事務(wù)。
AT模式
seata目前支持多種事務(wù)模式,分別有AT、TCC、SAGA 和 XA ,文章篇幅有限,今天只講常用的AT模式。
AT模式的特點(diǎn)就是對(duì)業(yè)務(wù)無(wú)入侵式,整體機(jī)制分二階段提交(2PC)
一階段:業(yè)務(wù)數(shù)據(jù)和回滾日志記錄在同一個(gè)本地事務(wù)中提交,釋放本地鎖和連接資源。 二階段: 提交異步化,非常快速地完成 回滾通過(guò)一階段的回滾日志進(jìn)行反向補(bǔ)償。
在 AT 模式下,用戶(hù)只需關(guān)注自己的業(yè)務(wù)SQL,用戶(hù)的業(yè)務(wù)SQL 作為一階段,Seata 框架會(huì)自動(dòng)生成事務(wù)的二階段提交和回滾操作。

一個(gè)典型的分布式事務(wù)過(guò)程:
TM 向 TC 申請(qǐng)開(kāi)啟一個(gè)全局事務(wù),全局事務(wù)創(chuàng)建成功并生成一個(gè)全局唯一的 XID; XID 在微服務(wù)調(diào)用鏈路的上下文中傳播; RM 向 TC 注冊(cè)分支事務(wù),將其納入 XID 對(duì)應(yīng)全局事務(wù)的管轄; TM 向 TC 發(fā)起針對(duì) XID 的全局提交或回滾決議; TC 調(diào)度 XID 下管轄的全部分支事務(wù)完成提交或回滾請(qǐng)求。
搭建Seata TC協(xié)調(diào)者
seata的協(xié)調(diào)者其實(shí)就是阿里開(kāi)源的一個(gè)服務(wù),我們只需要下載并且啟動(dòng)它。
下載地址:http://seata.io/zh-cn/blog/download.html
陳某下載的版本是
1.3.0,各位最好和我版本一致,這樣不會(huì)出現(xiàn)莫名的BUG。
下載完成后,直接解壓即可。但是此時(shí)還不能直接運(yùn)行,還需要做一些配置。
創(chuàng)建TC所需要的表
TC運(yùn)行需要將事務(wù)的信息保存在數(shù)據(jù)庫(kù),因此需要?jiǎng)?chuàng)建一些表,找到seata-1.3.0源碼的script\server\db這個(gè)目錄,將會(huì)看到以下SQL文件:

陳某使用的是Mysql數(shù)據(jù)庫(kù),因此直接運(yùn)行mysql.sql這個(gè)文件中的sql語(yǔ)句,創(chuàng)建的三張表如下圖:

修改TC的注冊(cè)中心
找到seata-server-1.3.0\seata\conf這個(gè)目錄,其中有一個(gè)registry.conf文件,其中配置了TC的注冊(cè)中心和配置中心。
默認(rèn)的注冊(cè)中心是file形式,實(shí)際使用中肯定不能使用,需要改成Nacos形式,改動(dòng)的地方如下圖:

需要改動(dòng)的地方如下:
type:改成nacos,表示使用nacos作為注冊(cè)中心 application:服務(wù)的名稱(chēng) serverAddr:nacos的地址 group:分組 namespace:命名空間 username:用戶(hù)名 password:密碼
最后這份文件都會(huì)放在項(xiàng)目源碼的根目錄下,源碼下載方式見(jiàn)文末
修改TC的配置中心
TC的配置中心默認(rèn)使用的也是file形式,當(dāng)然要是用nacos作為配置中心了。
直接修改registry.conf文件,需要改動(dòng)的地方如下圖:

需要改動(dòng)的地方如下:
type:改成nacos,表示使用nacos作為配置中心 serverAddr:nacos的地址 group:分組 namespace:命名空間 username:用戶(hù)名 password:密碼
上述配置修改好之后,在TC啟動(dòng)的時(shí)候?qū)?huì)自動(dòng)讀取nacos的配置。
那么問(wèn)題來(lái)了:TC需要存儲(chǔ)到Nacos中的配置都哪些,如何推送過(guò)去?
在seata-1.3.0\script\config-center中有一個(gè)config.txt文件,其中就是TC所需要的全部配置。
在seata-1.3.0\script\config-center\nacos中有一個(gè)腳本nacos-config.sh則是將config.txt中的全部配置自動(dòng)推送到nacos中,運(yùn)行下面命令(windows可以使用git bash運(yùn)行):
#?-h?主機(jī),你可以使用localhost,-p?端口號(hào)?你可以使用8848,-t?命名空間ID,-u?用戶(hù)名,-p?密碼
$?sh?nacos-config.sh?-h?127.0.0.1?-p?8080?-g?SEATA_GROUP?-t?7a7581ef-433d-46f3-93f9-5fdc18239c65?-u?nacos?-w?nacos
推送成功則可以在Nacos中查詢(xún)到所有的配置,如下圖:

修改TC的數(shù)據(jù)庫(kù)連接信息
TC是需要使用數(shù)據(jù)庫(kù)存儲(chǔ)事務(wù)信息的,那么如何修改相關(guān)配置呢?
上一節(jié)的內(nèi)容已經(jīng)將所有的配置信息都推送到了Nacos中,TC啟動(dòng)時(shí)會(huì)從Nacos中讀取,因此我們修改也需要在Nacos中修改。
需要修改的配置如下:
## 采用db的存儲(chǔ)形式
store.mode=db
## druid數(shù)據(jù)源
store.db.datasource=druid
## mysql數(shù)據(jù)庫(kù)
store.db.dbType=mysql
## mysql驅(qū)動(dòng)
store.db.driverClassName=com.mysql.jdbc.Driver
## TC的數(shù)據(jù)庫(kù)url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用戶(hù)名
store.db.user=root
## 密碼
store.db.password=Nov2014
在nacos中搜索上述的配置,直接修改其中的值,比如修改store.mode,如下圖:

當(dāng)然Seata還支持Redis作為T(mén)C的數(shù)據(jù)庫(kù),只需要改動(dòng)以下配置即可:
store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456
啟動(dòng)TC
按照上述步驟全部配置成功后,則可以啟動(dòng)TC,在seata-server-1.3.0\seata\bin目錄下直接點(diǎn)擊seata-server.bat(windows)運(yùn)行。
啟動(dòng)成功后,在Nacos的服務(wù)列表中則可以看到TC已經(jīng)注冊(cè)進(jìn)入,如下圖:

至此,Seata的TC就啟動(dòng)完成了............
Seata客戶(hù)端搭建(RM)
上述已經(jīng)將Seata的服務(wù)端(TC)搭建完成了,下面就以電商系統(tǒng)為例介紹一下如何編碼實(shí)現(xiàn)分布式事務(wù)。
用戶(hù)購(gòu)買(mǎi)商品的業(yè)務(wù)邏輯。整個(gè)業(yè)務(wù)邏輯由3個(gè)微服務(wù)提供支持:
倉(cāng)儲(chǔ)服務(wù):對(duì)給定的商品扣除倉(cāng)儲(chǔ)數(shù)量。 訂單服務(wù):根據(jù)采購(gòu)需求創(chuàng)建訂單。 帳戶(hù)服務(wù):從用戶(hù)帳戶(hù)中扣除余額。
需要了解的知識(shí):Nacos和openFeign,有不清楚的可以看我的前兩章教程,如下:
倉(cāng)儲(chǔ)服務(wù)搭建
陳某整個(gè)教程使用的都是同一個(gè)聚合項(xiàng)目,關(guān)于Spring Cloud版本有不清楚的可以看我第一篇文章的說(shuō)明。
添加依賴(lài)
新建一個(gè)seata-storage9020項(xiàng)目,新增依賴(lài)如下:

由于使用的springCloud Alibaba依賴(lài)版本是2.2.1.RELEASE,其中自帶的seata版本是1.1.0,但是我們Seata服務(wù)端使用的版本是1.3.0,因此需要排除原有的依賴(lài),重新添加1.3.0的依賴(lài)。
注意:seata客戶(hù)端的依賴(lài)版本必須要和服務(wù)端一致。
創(chuàng)建數(shù)據(jù)庫(kù)
創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)seata-storage,其中新建兩個(gè)表:
storage:庫(kù)存的業(yè)務(wù)表,SQL如下:
CREATE?TABLE?`storage`??(
??`id`?bigint(11)?NOT?NULL?AUTO_INCREMENT,
??`name`?varchar(100)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?NULL?DEFAULT?NULL,
??`num`?bigint(11)?NULL?DEFAULT?NULL?COMMENT?'數(shù)量',
??`create_time`?datetime(0)?NULL?DEFAULT?NULL,
??`price`?bigint(10)?NULL?DEFAULT?NULL?COMMENT?'單價(jià),單位分',
??PRIMARY?KEY?(`id`)?USING?BTREE
)?ENGINE?=?InnoDB?AUTO_INCREMENT?=?2?CHARACTER?SET?=?utf8mb4?COLLATE?=?utf8mb4_general_ci?ROW_FORMAT?=?Compact;
INSERT?INTO?`storage`?VALUES?(1,?'碼猿技術(shù)專(zhuān)欄',?1000,?'2021-10-15?22:32:40',?100);
undo_log:回滾日志表,這是Seata要求必須有的,每個(gè)業(yè)務(wù)庫(kù)都應(yīng)該創(chuàng)建一個(gè),SQL如下:
CREATE?TABLE?`undo_log`??(
??`branch_id`?bigint(20)?NOT?NULL?COMMENT?'branch?transaction?id',
??`xid`?varchar(100)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL?COMMENT?'global?transaction?id',
??`context`?varchar(128)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL?COMMENT?'undo_log?context,such?as?serialization',
??`rollback_info`?longblob?NOT?NULL?COMMENT?'rollback?info',
??`log_status`?int(11)?NOT?NULL?COMMENT?'0:normal?status,1:defense?status',
??`log_created`?datetime(6)?NOT?NULL?COMMENT?'create?datetime',
??`log_modified`?datetime(6)?NOT?NULL?COMMENT?'modify?datetime',
??UNIQUE?INDEX?`ux_undo_log`(`xid`,?`branch_id`)?USING?BTREE
)?ENGINE?=?InnoDB?CHARACTER?SET?=?utf8?COLLATE?=?utf8_general_ci?COMMENT?=?'AT?transaction?mode?undo?table'?ROW_FORMAT?=?Compact;
配置seata相關(guān)配置
對(duì)于Nacos、Mysql數(shù)據(jù)源等相關(guān)信息就省略了,項(xiàng)目源碼中都有。主要講一下seata如何配置,詳細(xì)配置如下:
spring:
??application:
????##?指定服務(wù)名稱(chēng),在nacos中的名字
????name:?seata-storage
##?客戶(hù)端seata的相關(guān)配置
seata:
??##?是否開(kāi)啟seata,默認(rèn)true
??enabled:?true
??application-id:?${spring.application.name}
??##?seata事務(wù)組的名稱(chēng),一定要和config.tx(nacos)中配置的相同
??tx-service-group:?${spring.application.name}-tx-group
??##?配置中心的配置
??config:
????##?使用類(lèi)型nacos
????type:?nacos
????##?nacos作為配置中心的相關(guān)配置,需要和server在同一個(gè)注冊(cè)中心下
????nacos:
??????##?命名空間,需要server端(registry和config)、nacos配置client端(registry和config)保持一致
??????namespace:?7a7581ef-433d-46f3-93f9-5fdc18239c65
??????##?地址
??????server-addr:?localhost:8848
??????##?組,?需要server端(registry和config)、nacos配置client端(registry和config)保持一致
??????group:?SEATA_GROUP
??????##?用戶(hù)名和密碼
??????username:?nacos
??????password:?nacos
??registry:
????type:?nacos
????nacos:
??????##?這里的名字一定要和seata服務(wù)端中的名稱(chēng)相同,默認(rèn)是seata-server
??????application:?seata-server
??????##?需要server端(registry和config)、nacos配置client端(registry和config)保持一致
??????group:?SEATA_GROUP
??????namespace:?7a7581ef-433d-46f3-93f9-5fdc18239c65
??????username:?nacos
??????password:?nacos
??????server-addr:?localhost:8848
以上配置注釋已經(jīng)很清楚,這里著重強(qiáng)調(diào)以下幾點(diǎn):
客戶(hù)端seata中的nacos相關(guān)配置要和服務(wù)端相同,比如地址、命名空間.......... tx-service-group:這個(gè)屬性一定要注意,這個(gè)一定要和服務(wù)端的配置一致,否則不生效;比如上述配置中的,就要在nacos中新增一個(gè)配置 service.vgroupMapping.seata-storage-tx-group=default,如下圖:

注意:
seata-storage-tx-group僅僅是后綴,要記得添加配置的時(shí)候要加上前綴service.vgroupMapping.
扣減庫(kù)存的接口
邏輯很簡(jiǎn)單,這里僅僅是做了減庫(kù)存的操作,代碼如下:

這里的接口并沒(méi)有不同,還是使用@Transactional開(kāi)啟了本地事務(wù),并沒(méi)有涉及到分布式事務(wù)。
到這里倉(cāng)儲(chǔ)服務(wù)搭建好了..............
賬戶(hù)服務(wù)搭建
搭建完了倉(cāng)儲(chǔ)服務(wù),賬戶(hù)服務(wù)搭建很類(lèi)似了。
添加依賴(lài)
新建一個(gè)seata-account9021服務(wù),這里的依賴(lài)和倉(cāng)儲(chǔ)服務(wù)的依賴(lài)相同,直接復(fù)制
創(chuàng)建數(shù)據(jù)庫(kù)
創(chuàng)建一個(gè)seata-account數(shù)據(jù)庫(kù),其中新建了兩個(gè)表:
account:賬戶(hù)業(yè)務(wù)表,SQL如下:
CREATE?TABLE?`account`??(
??`id`?bigint(11)?NOT?NULL,
??`user_id`?varchar(32)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?NULL?DEFAULT?NULL?COMMENT?'用戶(hù)userId',
??`money`?bigint(11)?NULL?DEFAULT?NULL?COMMENT?'余額,單位分',
??`create_time`?datetime(0)?NULL?DEFAULT?NULL,
??PRIMARY?KEY?(`id`)?USING?BTREE
)?ENGINE?=?InnoDB?CHARACTER?SET?=?utf8mb4?COLLATE?=?utf8mb4_general_ci?ROW_FORMAT?=?Compact;
INSERT?INTO?`account`?VALUES?(1,?'abc123',?1000,?'2021-10-19?17:49:53');
undo_log:回滾日志表,同倉(cāng)儲(chǔ)服務(wù)
配置seata相關(guān)配置
Seata相關(guān)配置和倉(cāng)儲(chǔ)服務(wù)相同,只不過(guò)需要在nacos中添加一個(gè)service.vgroupMapping.seata-account-tx-group=default,如下圖:

扣減余額的接口
具體邏輯自己完善,這里我直接扣減余額,代碼如下:

依然沒(méi)有涉及到分布式事務(wù),還是使用@Transactional開(kāi)啟了本地事務(wù),是不是很爽............
訂單服務(wù)搭建(TM)
這里為了節(jié)省篇幅,陳某直接使用訂單服務(wù)作為T(mén)M,下單、減庫(kù)存、扣款整個(gè)流程都在訂單服務(wù)中實(shí)現(xiàn)。
添加依賴(lài)
新建一個(gè)seata-order9022服務(wù),這里需要添加的依賴(lài)如下:
Nacos服務(wù)發(fā)現(xiàn)的依賴(lài) seata的依賴(lài) openFeign的依賴(lài),由于要調(diào)用賬戶(hù)、倉(cāng)儲(chǔ)的微服務(wù),因此需要額外添加一個(gè)openFeign的依賴(lài)
創(chuàng)建數(shù)據(jù)庫(kù)
新建一個(gè)seata_order數(shù)據(jù)庫(kù),其中新建兩個(gè)表,如下:
t_order:訂單的業(yè)務(wù)表
CREATE?TABLE?`t_order`??(
??`id`?bigint(11)?NOT?NULL?AUTO_INCREMENT,
??`product_id`?bigint(11)?NULL?DEFAULT?NULL?COMMENT?'商品Id',
??`num`?bigint(11)?NULL?DEFAULT?NULL?COMMENT?'數(shù)量',
??`user_id`?varchar(32)?CHARACTER?SET?utf8mb4?COLLATE?utf8mb4_general_ci?NULL?DEFAULT?NULL?COMMENT?'用戶(hù)唯一Id',
??`create_time`?datetime(0)?NULL?DEFAULT?NULL,
??`status`?int(1)?NULL?DEFAULT?NULL?COMMENT?'訂單狀態(tài)?1?未付款?2?已付款?3?已完成',
??PRIMARY?KEY?(`id`)?USING?BTREE
)?ENGINE?=?InnoDB?AUTO_INCREMENT?=?7?CHARACTER?SET?=?utf8mb4?COLLATE?=?utf8mb4_general_ci?ROW_FORMAT?=?Compact;
undo_log:回滾日志表,同倉(cāng)儲(chǔ)服務(wù)
配置和seata相關(guān)配置
Seata相關(guān)配置和倉(cāng)儲(chǔ)服務(wù)相同,只不過(guò)需要在nacos中添加一個(gè)service.vgroupMapping.seata-order-tx-group=default,如下圖:

扣減庫(kù)存的接口
這里需要通過(guò)openFeign調(diào)用倉(cāng)儲(chǔ)服務(wù)的接口進(jìn)行扣減庫(kù)存,接口如下:

以上只是簡(jiǎn)單的通過(guò)openFeign調(diào)用,更細(xì)致的配置,比如降級(jí),自己完善.........
扣減余額的接口
這里仍然是通過(guò)openFeign調(diào)用賬戶(hù)服務(wù)的接口進(jìn)行扣減余額,接口如下:

創(chuàng)建訂單的接口
下訂單的接口就是一個(gè)事務(wù)發(fā)起方,作為T(mén)M,需要發(fā)起一個(gè)全局事務(wù),詳細(xì)代碼如下圖:

有什么不同?不同之處就是使用了@GlobalTransactional而不是@Transactional。
@GlobalTransactional是Seata提供的,用于開(kāi)啟才能全局事務(wù),只在TM中標(biāo)注即可生效。
測(cè)試
分別啟動(dòng)seata-account9021、seata-storage9020、seata-order9022,如下圖:

下面調(diào)用下單接口,如下圖:

從控制臺(tái)輸出的日志可以看出,流程未出現(xiàn)任何異常,事務(wù)已經(jīng)提交,如下圖:

果然,查看訂單、余額、庫(kù)存表,數(shù)據(jù)也都是正確的。
但是,這僅僅是流程沒(méi)問(wèn)題,并不能說(shuō)明分布式事務(wù)已經(jīng)配置成功了,因此需要手動(dòng)造個(gè)異常。
在扣減余額的接口睡眠2秒鐘,因?yàn)閛penFeign的超時(shí)時(shí)間默認(rèn)是1秒,這樣肯定是超時(shí)異常了,如下圖:

此時(shí),調(diào)用創(chuàng)建訂單的接口,控制臺(tái)日志輸出如下圖:

發(fā)現(xiàn)在扣減余額處理中超時(shí)了,導(dǎo)致了異常.......
此時(shí),看下庫(kù)存的數(shù)據(jù)有沒(méi)有扣減,很高興,庫(kù)存沒(méi)有扣減成功,說(shuō)明事務(wù)已經(jīng)回滾了,分布式事務(wù)成功了。
總結(jié)
Seata客戶(hù)端創(chuàng)建很簡(jiǎn)單,需要注意以下幾點(diǎn)內(nèi)容:
seata客戶(hù)端的版本需要和服務(wù)端保持一致 每個(gè)服務(wù)的數(shù)據(jù)庫(kù)都要?jiǎng)?chuàng)建一個(gè) undo_log回滾日志表客戶(hù)端指定的事務(wù)分組名稱(chēng)要和Nacos相同,比如 service.vgroupMapping.seata-account-tx-group=default前綴: service.vgroupMapping.后綴: {自定義}
項(xiàng)目源碼已經(jīng)上傳,關(guān)注公眾號(hào)
碼猿技術(shù)專(zhuān)欄回復(fù)關(guān)鍵詞9528獲取!
AT模式原理分析
AT模式最大的優(yōu)點(diǎn)就是對(duì)業(yè)務(wù)代碼無(wú)侵入,一切都像在寫(xiě)單體業(yè)務(wù)邏輯一樣。
TC相關(guān)的三張表:
global_table:全局事務(wù)表,每當(dāng)有一個(gè)全局事務(wù)發(fā)起后,就會(huì)在該表中記錄全局事務(wù)的IDbranch_table:分支事務(wù)表,記錄每一個(gè)分支事務(wù)的ID,分支事務(wù)操作的哪個(gè)數(shù)據(jù)庫(kù)等信息lock_table:全局鎖
一階段步驟
TM:seata-order.create()方法執(zhí)行時(shí),由于該方法具有@GlobalTranscational標(biāo)志,該TM會(huì)向TC發(fā)起全局事務(wù),生成XID(全局鎖)RM:StorageService.deduct():寫(xiě)表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結(jié)果RM:AccountService.deduct():寫(xiě)表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結(jié)果RM:OrderService.create():寫(xiě)表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結(jié)果
RM寫(xiě)表的過(guò)程,Seata 會(huì)攔截業(yè)務(wù)SQL,首先解析 SQL 語(yǔ)義,在業(yè)務(wù)數(shù)據(jù)被更新前,將其保存成before image(前置鏡像),然后執(zhí)行業(yè)務(wù)SQL,在業(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ù)清理即可。
正常:TM執(zhí)行成功,通知TC全局提交,TC此時(shí)通知所有的RM提交成功,刪除UNDO_LOG回滾日志

異常:TM執(zhí)行失敗,通知TC全局回滾,TC此時(shí)通知所有的RM進(jìn)行回滾,根據(jù)UNDO_LOG反向操作,使用before image還原業(yè)務(wù)數(shù)據(jù),刪除UNDO_LOG,但在還原前要首先要校驗(yàn)臟寫(xiě),對(duì)比“數(shù)據(jù)庫(kù)當(dāng)前業(yè)務(wù)數(shù)據(jù)”和 “after image”,如果兩份數(shù)據(jù)完全一致就說(shuō)明沒(méi)有臟寫(xiě),可以還原業(yè)務(wù)數(shù)據(jù),如果不一致就說(shuō)明有臟寫(xiě),出現(xiàn)臟寫(xiě)就需要轉(zhuǎn)人工處理。

AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動(dòng)生成,用戶(hù)只需編寫(xiě)業(yè)務(wù) SQL,便能輕松接入分布式事務(wù),AT 模式是一種對(duì)業(yè)務(wù)無(wú)任何侵入的分布式事務(wù)解決方案。
總結(jié)
本文介紹了七種分布式事務(wù)解決方案,以及阿里開(kāi)源的Seata,從入門(mén)到實(shí)現(xiàn),文中如有錯(cuò)誤之處,歡迎留言指正。
本文只介紹了Seata的AT模式,其實(shí)Seata還支持TCC、Saga事務(wù)模式,關(guān)于這一部分內(nèi)容和Seata源碼分析會(huì)在下期文章中介紹。
我是 Guide哥,一個(gè)工作2年有余,接觸編程已經(jīng)6年有余的程序員。大三開(kāi)源 JavaGuide,目前已經(jīng) 100k+ Star。未來(lái)幾年,希望持續(xù)完善 JavaGuide,爭(zhēng)取能夠幫助更多學(xué)習(xí) Java 的小伙伴!共勉!凎!點(diǎn)擊即可了解我的個(gè)人經(jīng)歷。

