java中的分布式事務(wù)解決方式
背景
分布式事務(wù),后端開發(fā)中比較常見啦。因?yàn)樵诿嬖嚨臅r(shí)候,總是有interviewers讓我給他普及一下分布式事務(wù),雖然我會的也不多呀但是還是淺淺說一說;
今天心血來潮,好好地總結(jié)一下分布式事務(wù),希望每一位后端工程師都能徹底理解分布式事務(wù)。
什么是分布式事務(wù)?
答:既然是分布式,首先必然是分布式系統(tǒng)中的一個(gè)概念啦。單體應(yīng)用沒這個(gè)東西,也不需要這個(gè)東西。本地事務(wù)就夠啦,Spring給我們提供的注解@Transactional, InnoDB引擎會為我們保證事務(wù)的ACID特性。但是分布式系統(tǒng)中,目前大多數(shù)互聯(lián)網(wǎng)公司都在用分布式系統(tǒng),微服務(wù)架構(gòu)等。所以,學(xué)好分布式事務(wù)太有必要。
廢話不多說,直接上原理。
總結(jié)來說,分布式事務(wù)涉及了多個(gè)獨(dú)立的數(shù)據(jù)源(數(shù)據(jù)庫)或者參與者的事務(wù)操作,這些數(shù)據(jù)源分布在不同的計(jì)算機(jī)或網(wǎng)絡(luò)中;分布式事務(wù)確保在不同節(jié)點(diǎn)之間的多個(gè)操作要么全部成功,要么全部失敗。
分布式事務(wù)解決方案
2PC(XA協(xié)議)
兩階段提交協(xié)議,也叫XA協(xié)議。主要包含兩個(gè)階段,第一個(gè)階段是預(yù)備階段,第二個(gè)階段是提交階段。
2PC協(xié)議首先有分事務(wù)協(xié)調(diào)者角色和事務(wù)參與者。協(xié)調(diào)者是事先指定好的一個(gè)節(jié)點(diǎn)。參與者是一些涉及到數(shù)據(jù)庫操作的表,暫時(shí)可以這樣理解。這些多個(gè)參與者一般是分布在不同的節(jié)點(diǎn)上。
- 準(zhǔn)備階段。協(xié)調(diào)者向所有參與者發(fā)送事務(wù)準(zhǔn)備請求,參與者執(zhí)事務(wù)操作,并回復(fù)協(xié)調(diào)者準(zhǔn)備就緒的消息;如果多個(gè)參與者中有一個(gè)參與者未準(zhǔn)備就緒或者發(fā)生錯(cuò)誤,那么協(xié)調(diào)者會發(fā)送中止請求。只有所有參與者都回復(fù)準(zhǔn)備就緒,才會進(jìn)入第二階段。
- 提交階段。所有參與者都已經(jīng)準(zhǔn)備就緒,協(xié)調(diào)者分別發(fā)送提交的消息,參與者收到消息以后,執(zhí)行事務(wù)的提交操作,并向協(xié)調(diào)者回復(fù)提交完成。協(xié)調(diào)者收到了所有事務(wù)參與者提交完成的消息后,整個(gè)分布式事務(wù)才算提交完成。如果有一個(gè)參與者未能提交或者發(fā)生錯(cuò)誤,那么協(xié)調(diào)者會向所有參與者發(fā)送中止請求,進(jìn)行事務(wù)的回滾操作。
如何評價(jià)2PC?
1、2PC有單點(diǎn)故障的問題。一旦事務(wù)協(xié)調(diào)者故障(因?yàn)槭鞘褂玫搅四硞€(gè)節(jié)點(diǎn)嘛),那么整個(gè)事務(wù)將無法繼續(xù)進(jìn)行,陷入故障。
2、數(shù)據(jù)不一致。如果協(xié)調(diào)者在發(fā)送提交信息時(shí),只有部分參與者收到了消息,并執(zhí)行了提交,此時(shí)網(wǎng)絡(luò)異常,就導(dǎo)致只有部分參與者執(zhí)行了事物的提交,另一部分則沒有提交,從而造成一個(gè)數(shù)據(jù)不一致性。
3、阻塞風(fēng)險(xiǎn)。如果準(zhǔn)備階段,有一個(gè)參與者無法響應(yīng)或者失敗,那么整個(gè)系統(tǒng)都會陷入阻塞狀態(tài),等待超時(shí)處理。
4、性能問題。整個(gè)鏈路是串行的,響應(yīng)時(shí)間較長,不適合高并發(fā)的場景。
3PC
三階段提交又稱3PC,相對于2PC來說增加了CanCommit階段和超時(shí)機(jī)制。如果某段時(shí)間內(nèi)沒有收到協(xié)調(diào)者的commit請求,那么就會自動進(jìn)行commit,解決了2PC單點(diǎn)故障的問題。
但是性能問題和數(shù)據(jù)不一致性問題還是沒解決。3PC的步驟是這樣的:
- 1、詢問節(jié)點(diǎn)。CanCommit, 首先詢問參與者,是否有能力完成此次事務(wù)?如果都返回yes,則進(jìn)入第二階段有一個(gè)返回no或等待響應(yīng)超時(shí),則中斷事務(wù),并向所有參與者發(fā)送abort請求。
- 2、準(zhǔn)備階段;同2PC。需要注意的是,參與者收到消息后開始執(zhí)行事務(wù)操作,會首先將Undo和Redo信息記錄到事務(wù)日志中。參與者執(zhí)行完事務(wù)操作后,向協(xié)調(diào)者反饋ACK, 表示已經(jīng)準(zhǔn)備好提交了。
- 3、提交階段。同2PC。
TCC
TCC ,Try, Confirm, Cancel; 其實(shí)是采用的補(bǔ)償機(jī)制,其核心思想是:針對每個(gè)操作,都要注冊一個(gè)與其對應(yīng)的確認(rèn)和補(bǔ)償(撤銷)操作。
它分為三個(gè)階段:
- 1、Try, 對業(yè)務(wù)系統(tǒng)做檢測及資源預(yù)留;
- 2、Confirm主要對業(yè)務(wù)系統(tǒng)做確認(rèn)提交;try階段執(zhí)行成功,并開始執(zhí)行confirm,默認(rèn)是不會出錯(cuò)的;只要Try成功,那么Confirm 一定會成功。
- 3、Cancle, 主要是在業(yè)務(wù)執(zhí)行錯(cuò)誤,需要回滾的狀態(tài)下執(zhí)行的業(yè)務(wù)取消,對預(yù)留資源釋放!
舉個(gè)例子,假入 Bob 要向 Smith 轉(zhuǎn)賬,思路大概是:我們有一個(gè)本地方法,里面依次調(diào)用
1、首先在 Try 階段,要先調(diào)用遠(yuǎn)程接口把 Smith 和 Bob 的錢給凍結(jié)起來。凍結(jié)可以理解為一種特殊的扣減,以放在后續(xù)轉(zhuǎn)賬的時(shí)候,金額不夠轉(zhuǎn)。并發(fā)訪問的時(shí)候,凍結(jié)操作需要加分布式鎖,避免我在執(zhí)行凍結(jié)扣減的時(shí)候,此時(shí)的金額發(fā)生了變化,導(dǎo)致凍結(jié)失敗或者不一致性??蹨p以后,生成一條凍結(jié)交易記錄,表示該凍結(jié)操作成功。
2、在 Confirm 階段,執(zhí)行遠(yuǎn)程調(diào)用的轉(zhuǎn)賬的操作,轉(zhuǎn)賬成功;成功后查詢出凍結(jié)交易記錄,將凍結(jié)狀態(tài)變更為已解凍;
3、如果第2步執(zhí)行成功,那么轉(zhuǎn)賬成功,如果第二步執(zhí)行失敗,則調(diào)用遠(yuǎn)程凍結(jié)接口對應(yīng)的解凍接口,將數(shù)據(jù)恢復(fù)為凍結(jié)前的樣子。
Try部分完成業(yè)務(wù)的準(zhǔn)備工作,confirm部分完成業(yè)務(wù)的提交,cancel部分完成事務(wù)的回滾?;驹砣缦聢D所示:
如上圖所示,每個(gè)分支事務(wù)都需要實(shí)現(xiàn)Try,Confirm,Cancel接口。TCC事務(wù)開始時(shí),業(yè)務(wù)應(yīng)用會向事務(wù)協(xié)調(diào)器注冊啟動事務(wù)。之后業(yè)務(wù)應(yīng)用會調(diào)用所有服務(wù)的Try接口,完成一階段準(zhǔn)備。之后事務(wù)協(xié)調(diào)器會根據(jù)Try接口返回情況,決定調(diào)用Confirm接口或者Cancel接口。如果接口調(diào)用失敗,會進(jìn)行重試。
總結(jié)的說,所有分支的Try操作成功,會進(jìn)入到Confirm; 有一個(gè)分支未成功,已經(jīng)執(zhí)行的操作都要回滾。從而恢復(fù)到嘗試階段之前的狀態(tài),以確保數(shù)據(jù)的一致性。
TCC的優(yōu)點(diǎn):
- 1、降低了鎖的粒度,減少了并發(fā)沖突;從而提高了吞吐量;每個(gè)業(yè)務(wù)執(zhí)行操作都有相應(yīng)的補(bǔ)償操作,不需要人工干預(yù)進(jìn)行補(bǔ)償。
- 不足之處也很明顯,
- 2、對業(yè)務(wù)有一定的入侵;改造成本高,代碼冗長;
- 3、實(shí)現(xiàn)難度較大。需要按照網(wǎng)絡(luò)狀態(tài)、系統(tǒng)故障等不同的失敗原因?qū)崿F(xiàn)不同的回滾策略。為了滿足一致性的要求,confirm和cancel接口必須實(shí)現(xiàn)冪等。
本地消息表+MQ最終一致性事務(wù)
本地消息表的思想是將分布式事務(wù)拆分為本地事務(wù)來處理,通過引入一個(gè)額外的消息表,存儲消息的處理狀態(tài),從而實(shí)現(xiàn)消息可靠性和一致性。
如上圖中的訂單系統(tǒng)和庫存系統(tǒng)是兩個(gè)獨(dú)立的系統(tǒng),所以數(shù)據(jù)庫也是獨(dú)立的。如果我們想要保證下訂單以及訂單中商品庫存扣減的原子性,就必然要使用分布式事務(wù)。使用本地消息表解決分布式事務(wù)的思路是這樣。首先,訂單系統(tǒng)正常進(jìn)行訂單業(yè)務(wù)數(shù)據(jù)存DB,同時(shí)將這個(gè)消息實(shí)體記錄存入消息表內(nèi),消息實(shí)體至少要有消息的id以及消息的處理狀態(tài);然后向MQ發(fā)送這條消息。
訂單系統(tǒng)不斷往MQ中生產(chǎn)消息,庫存系統(tǒng)去消費(fèi)這個(gè)MQ中的消息,取到消息以后,執(zhí)行庫存相關(guān)業(yè)務(wù)操作,保存至DB,最終返回一個(gè)成功處理的響應(yīng)給MQ。訂單系統(tǒng)發(fā)完消息以后,也會去讀取該消息的響應(yīng)處理消息,讀到以后,如果是成功處理,那么就更新訂單庫中本地消息表中的該消息狀態(tài),更新為已完成。
整個(gè)簡單的思路是這樣,需要注意的是,我們要保證下訂單和訂單消息存入消息表是一個(gè)原子性的,使用本地事務(wù)處理,要么都成功,要么都失敗。
第二,如果最終消息表中的消息都是已完成就沒什么問題,但是有時(shí)候, 由于數(shù)據(jù)庫網(wǎng)絡(luò)等不穩(wěn)定因素導(dǎo)致消息的狀態(tài)還是未完成。這種問題,我們必須要解決。
可以使用定時(shí)任務(wù),定時(shí)任務(wù)周期性去輪詢本地消息表中的消息,如果某消息是未完成,那么可以觸發(fā)重試操作,繼續(xù)往MQ中發(fā)送這條消息,交給庫存系統(tǒng)處理。
如果1步驟訂單消息在保存的時(shí)候失敗,那么直接觸發(fā)回滾即可。1和2是同時(shí)成功,同時(shí)回滾的。如果3失敗了,由于本地消息表中已經(jīng)存在消息體,這時(shí)只需要輪詢消息重新通過消息中間件發(fā)送一次。
第三,如果庫存系統(tǒng)中在業(yè)務(wù)處理上失敗了,此時(shí)再重試已無效,可以發(fā)消息給事務(wù)主動方訂單系統(tǒng)回滾事務(wù)。
如果庫存系統(tǒng)已經(jīng)消費(fèi)了消息,訂單系統(tǒng)需要回滾事務(wù)的話,需要發(fā)消息通知庫存進(jìn)行回滾事務(wù)。
本地消息表+MQ由于消息是異步發(fā)送,實(shí)際上是一種最終一致性方案,即滿足base理論,所以在一些要求實(shí)時(shí)性的業(yè)務(wù)場景就不那么適用的,適用于實(shí)時(shí)性不高,能接受最終一致性的場景。
優(yōu)缺點(diǎn)總結(jié)
優(yōu)點(diǎn)
1、簡單可靠。通過本地消息表,將消息先存儲到本地?cái)?shù)據(jù)庫中,再進(jìn)行異步發(fā)送,可以保證消息的可靠性和一致性。
2、高可用性。相對XA協(xié)議,由于消息是異步發(fā)送的,所以即使其他分支事務(wù)出現(xiàn)了服務(wù)不可用或者故障,也不會影響當(dāng)前事務(wù)的提交,保證了系統(tǒng)的高可用。
3、提升性能:將消息發(fā)送過程異步化,減少了事務(wù)的等待時(shí)間,提升了系統(tǒng)的性能。
缺點(diǎn)
1、需要維護(hù)額外的消息表,增加了系統(tǒng)的復(fù)雜性。
2、依賴于數(shù)據(jù)庫。由于本地消息表依賴于數(shù)據(jù)庫,如果數(shù)據(jù)庫出現(xiàn)故障或性能問題,會對整個(gè)系統(tǒng)的可用性和性能產(chǎn)生影響。
3、業(yè)務(wù)耦合。本地消息表與業(yè)務(wù)耦合在一起,難于做成通用性,不可獨(dú)立伸縮。
4、無法保證實(shí)時(shí)性:由于消息發(fā)送是異步的,無法保證消息的實(shí)時(shí)性,存在一定的延遲。
最后,對于實(shí)時(shí)性要求較高、對數(shù)據(jù)一致性要求更嚴(yán)格的場景,可能需要考慮使用分布式事務(wù)管理框架或消息中間件等更復(fù)雜的方案。
Seata
Seata的設(shè)計(jì)思路是將一個(gè)分布式事務(wù)理解為一個(gè)全局事務(wù),全局事務(wù)下面掛著若干個(gè)分支事務(wù)。每個(gè)分支事務(wù)又相當(dāng)于是本地事務(wù),滿足ACID的特性,因此我們操作分布式事務(wù)就像操作本地事務(wù)一樣。Seata 內(nèi)部定義了 3個(gè)模塊來處理全局事務(wù)和分支事務(wù)的關(guān)系和處理過程,這三個(gè)組件分別是:
- Transaction Coordinator (TC): 事務(wù)協(xié)調(diào)器,維護(hù)全局事務(wù)的運(yùn)行狀態(tài),負(fù)責(zé)協(xié)調(diào)并驅(qū)動全局事務(wù)的提交或回滾。
- Transaction Manager (TM): 控制全局事務(wù)的邊界,負(fù)責(zé)開啟一個(gè)全局事務(wù),并最終發(fā)起全局提交或全局回滾的決議。
- Resource Manager (RM): 控制分支事務(wù),負(fù)責(zé)分支注冊、狀態(tài)匯報(bào),并接收事務(wù)協(xié)調(diào)器的指令,驅(qū)動分支(本地)事務(wù)的提交和回滾。
通過這三個(gè)模塊,完成全局事務(wù)的執(zhí)行流程,執(zhí)行步驟如下:
首先,TM向TC申請一個(gè)全局事務(wù),TC創(chuàng)建一個(gè)全局事務(wù),并返回一個(gè)XID,XID是全局事務(wù)的唯一標(biāo)識。然后,RM向TC注冊分支事務(wù),該分支歸屬于該XID的全局事務(wù)。RM注冊本地事務(wù),會完成一系列的本地事務(wù)操作,用于全局事務(wù)的提交或者回滾。
最后,TM將全局事務(wù)提交或者回滾的決策發(fā)送TC,TC調(diào)度 XID 全局事務(wù)下的分支事務(wù)完成提交或者回滾。
一般的,我們都使用Seate的AT模式,其實(shí)也是分為兩個(gè)階段,類似于XA協(xié)議的方式。第一階段是準(zhǔn)備階段,第二階段是分布式事務(wù)的提交或者回滾。
第一階段
分支事務(wù)主要利用RM模塊中的JDBC代理,在業(yè)務(wù)數(shù)據(jù)提交時(shí),自動攔截業(yè)務(wù)SQL,把業(yè)務(wù)數(shù)據(jù)在更新前后的數(shù)據(jù)鏡像組織成回滾日志,進(jìn)而生成undoLog; 然后利用本地事務(wù)的特性,將業(yè)務(wù)SQL和undolog寫入同一個(gè)事務(wù)中,一起提交到數(shù)據(jù)庫中,保證業(yè)務(wù)SQL必定存在回滾日志,最后對分支事務(wù)狀態(tài)向TC進(jìn)行上報(bào)。
第二階段
1、TM決議全局事務(wù)提交。首先通知TC全局事務(wù)提交,說明各個(gè)RM分支事務(wù)已經(jīng)完成了第一階段;此時(shí),TC會異步調(diào)度各個(gè)RM分支事務(wù)刪除對應(yīng)的undolog日志。這個(gè)過程是異步的,所以速度也比較快。
2、TM決議全局事務(wù)回滾。同樣,首先通知TC全局事務(wù)回滾,RM會收到TC發(fā)送的回滾請求,RM會通過XID找到對應(yīng)的undolog日志,利用本地事務(wù) ACID 特性,執(zhí)行回滾日志完成回滾操作,并刪除 undo log 日志,最后向TC進(jìn)行回滾結(jié)果上報(bào)。
業(yè)務(wù)對以上所有的流程都無感知,業(yè)務(wù)完全不關(guān)心全局事務(wù)的具體提交和回滾,而且最重要的一點(diǎn)是 Seata 將兩段式提交的同步協(xié)調(diào)分解到各個(gè)分支事務(wù)中了,分支事務(wù)與普通的本地事務(wù)無任何差異,這意味著我們使用 Seata 后,分布式事務(wù)就像使用本地事務(wù)一樣。
然后想說一下,將數(shù)據(jù)庫層的事務(wù)協(xié)調(diào)機(jī)制交給了中間件層 Seata , Seata為什么是個(gè)中間層,這是因?yàn)?,XA協(xié)議依賴的是數(shù)據(jù)庫層面來保障事務(wù)的一致性,也即是說 XA 的各個(gè)分支事務(wù)是在數(shù)據(jù)庫層面上驅(qū)動的。而Seata在數(shù)據(jù)庫做了一層代理層,所以我們使用 Seata 時(shí),使用的數(shù)據(jù)源實(shí)際上用的是Seata自帶的數(shù)據(jù)源代理 DataSourceProxy。這個(gè)代理層具體體現(xiàn)在RM模塊。
其主要作用是解析 SQL,把業(yè)務(wù)數(shù)據(jù)在更新前后的數(shù)據(jù)鏡像組織成回滾日志,并將 undo log 日志插入 undo log 表中,保證每條更新數(shù)據(jù)的業(yè)務(wù) sql 都有對應(yīng)的回滾日志存在。這樣做的好處是:本地事務(wù)執(zhí)行完以后,可以立即釋放資源,然后向 TC上報(bào)分支狀態(tài)。TM決議全局提交時(shí),也不需要同步協(xié)調(diào)處理。
總接的來說,Seate有如下優(yōu)點(diǎn):
- 1、較高的性能表現(xiàn)。它極大減少了分支事務(wù)對資源的鎖定時(shí)間,完美避免了 XA 協(xié)議需要同步協(xié)調(diào)導(dǎo)致資源鎖定時(shí)間過長的問題。
- 2、易集成。 Seata 提供了與各種主流框架和中間件的集成支持,方便在現(xiàn)有系統(tǒng)中集成和使用。
- 3、支持多種存儲后端: Seata 支持多種存儲后端,可以根據(jù)實(shí)際需求選擇合適的存儲方式。
- 4、靈活配置: Seata 提供了豐富的配置選項(xiàng),可以根據(jù)需求進(jìn)行靈活配置,滿足不同的分布式事務(wù)需求。
但是因?yàn)樵谑褂脮r(shí)需要部署Seata服務(wù)端,集成Seata客戶端,所以也存在一些缺點(diǎn),比如:
- 部署和維護(hù)成本: 部署和維護(hù)分布式事務(wù)解決方案需要一定的成本和精力,特別是在大規(guī)模系統(tǒng)中。
- 依賴性: 引入 Seata 可能會增加系統(tǒng)的依賴性,需要謹(jǐn)慎評估是否真正需要使用分布式事務(wù)解決方案。
- 配置復(fù)雜性: 配置 Seata 可能需要一定的復(fù)雜性,特別是針對復(fù)雜的分布式系統(tǒng)和場景,需要仔細(xì)配置各種參數(shù)。
所以,可按需看業(yè)務(wù)情況是否真的需要使用Seate實(shí)現(xiàn)分布式事務(wù)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Sa-Token記住我模式實(shí)現(xiàn)七天免登錄
這篇文章主要為大家介紹了Sa-Token記住我模式實(shí)現(xiàn)七天免登錄示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Java?web開發(fā)環(huán)境的搭建超完整步驟
這篇文章主要介紹了如何安裝和配置IDEA?2020.1.1?X64版本軟件,包括創(chuàng)建Java?Web項(xiàng)目、配置Tomcat、部署Tomcat?API以及創(chuàng)建和配置Servlet,通過這些步驟,新手可以快速搭建起Javaweb開發(fā)環(huán)境,需要的朋友可以參考下2024-11-11java8 多個(gè)list對象用lambda求差集操作
這篇文章主要介紹了java8 多個(gè)list對象用lambda求差集操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Feign如何解決服務(wù)之間調(diào)用傳遞token
這篇文章主要介紹了Feign如何解決服務(wù)之間調(diào)用傳遞token,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03springboot(thymeleaf)中th:field和th:value的區(qū)別及說明
這篇文章主要介紹了springboot(thymeleaf)中th:field和th:value的區(qū)別及說明,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10在Java中實(shí)現(xiàn)線程安全的單例模式的常見方式
單例模式是一種常用的軟件設(shè)計(jì)模式,它確保一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)全局訪問點(diǎn),在多線程環(huán)境下,確保單例模式的線程安全性是非常重要的,因?yàn)槎鄠€(gè)線程可能會同時(shí)嘗試創(chuàng)建實(shí)例,導(dǎo)致實(shí)例不唯一的問題,本文介紹了在Java中實(shí)現(xiàn)線程安全的單例模式有幾種常見的方式2024-09-09Simple Java Mail郵件發(fā)送實(shí)現(xiàn)過程解析
這篇文章主要介紹了Simple Java Mail郵件發(fā)送實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11