分布式事務(wù)

事務(wù)大家應(yīng)該都不陌生,ACID也是老生常談了,但是在講分布式事務(wù)之前,我們還是復(fù)習(xí)下事務(wù)的四大特性:

原子性(atomicity):一個事務(wù)是一個不可分割的工作單位,事務(wù)中包括的操作要么都做,要么都不做,不可能停滯在中間某個環(huán)節(jié)。事務(wù)在執(zhí)行過程中發(fā)生錯誤,會被回滾到事務(wù)開始前的狀態(tài),就像這個事務(wù)從來沒有執(zhí)行過一樣。
一致性(consistency):事務(wù)必須是使數(shù)據(jù)庫從一個一致性狀態(tài)變到另一個一致性狀態(tài),即不會存在中間狀態(tài),與原子性是密切相關(guān)的。
隔離性(isolation):一個事務(wù)的執(zhí)行不能被其他事務(wù)干擾。即一個事務(wù)內(nèi)部的操作及使用的數(shù)據(jù)對并發(fā)的其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。
持久性(durability):持久性也稱永久性(permanence),指一個事務(wù)一旦提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就應(yīng)該是永久性的。接下來的其他操作或故障不應(yīng)該對其有任何影響。

其中隔離性會包含幾個隔離級別,介紹之前我們先看幾個概念,好明白事務(wù)隔離級別要實際解決的問題。

臟讀:所謂臟讀,就是指事務(wù)A讀到了事務(wù)B還沒有提交的數(shù)據(jù),比如銀行取錢,事務(wù)A開啟事務(wù),此時開啟事務(wù)B-->取走100元,這時再看事務(wù)A,事務(wù)A讀取的肯定是數(shù)據(jù)庫里面的原始數(shù)據(jù),因為事務(wù)B取走了100塊錢,并沒有提交,數(shù)據(jù)庫里面的賬務(wù)余額肯定還是原始余額,這就是臟讀。
不可重復(fù)讀:所謂不可重復(fù)讀,就是指在一個事務(wù)里面讀取了兩次某個數(shù)據(jù),讀出來的數(shù)據(jù)不一致。還是以銀行取錢為例,事務(wù)A開啟事務(wù)-->查出銀行卡余額為1000元,此時開啟事務(wù)B-->事務(wù)B取走100元-->提交,數(shù)據(jù)庫里面余額變?yōu)?00元,此時再看事務(wù)A,事務(wù)A再查一次查出賬戶余額為900元,這樣對事務(wù)A而言,在同一個事務(wù)內(nèi)兩次讀取賬戶余額數(shù)據(jù)不一致,這就是不可重復(fù)讀。
幻讀:所謂幻讀,就是指在一個事務(wù)里面的操作中發(fā)現(xiàn)了未被操作的數(shù)據(jù)。比如學(xué)生信息,事務(wù)A開啟事務(wù)-->修改所有學(xué)生當(dāng)天簽到狀況為false,此時開啟事務(wù)B-->事務(wù)B插入了一條學(xué)生數(shù)據(jù),此時切換回事務(wù)A,事務(wù)A提交的時候發(fā)現(xiàn)了一條自己沒有修改過的數(shù)據(jù),這就是幻讀,就好像發(fā)生了幻覺一樣?;米x出現(xiàn)的前提是并發(fā)的事務(wù)中有事務(wù)發(fā)生了插入、刪除操作。

事務(wù)隔離級別,就是為了解決上面幾種問題而誕生的。為什么要有事務(wù)隔離級別,因為事務(wù)隔離級別越高,在并發(fā)下會產(chǎn)生的問題就越少,但同時付出的性能消耗也將越大,因此很多時候必須在并發(fā)性和性能之間做一個權(quán)衡。所以設(shè)立了幾種事務(wù)隔離級別,以便讓不同的項目可以根據(jù)自己項目的并發(fā)情況選擇合適的事務(wù)隔離級別,對于在事務(wù)隔離級別之外會產(chǎn)生的并發(fā)問題,在代碼中做補償。SQL 標(biāo)準(zhǔn)定義了四種隔離級別,這四種隔離級別分別是:

讀未提交(READ UNCOMMITTED):能夠讀取到?jīng)]有被提交的數(shù)據(jù),所以很明顯這個級別的隔離機制無法解決臟讀、不可重復(fù)讀、幻讀中的任何一種,因此很少使用。
讀提交 (READ COMMITTED):能夠讀到那些已經(jīng)提交的數(shù)據(jù),自然能夠防止臟讀,但是無法限制不可重復(fù)讀和幻讀。
可重復(fù)讀 (REPEATABLE READ):在數(shù)據(jù)讀出來之后加鎖,類似"select * from XXX for update",明確數(shù)據(jù)讀取出來就是為了更新用的,所以要加一把鎖,防止別人修改它。REPEATABLE_READ的意思也類似,讀取了一條數(shù)據(jù),這個事務(wù)不結(jié)束,別的事務(wù)就不可以改這條記錄,這樣就解決了臟讀、不可重復(fù)讀的問題,但是幻讀的問題不能完全避免(雖然大部分情況下不會存在)。
串行化 (SERIALIZABLE):最高的事務(wù)隔離級別,不管多少事務(wù),挨個運行完一個事務(wù)的所有子事務(wù)之后才可以執(zhí)行另外一個事務(wù)里面的所有子事務(wù),這樣就解決了臟讀、不可重復(fù)讀和幻讀的問題了。

從上往下,隔離強度逐漸增強,性能逐漸變差??芍貜?fù)讀是 MySQL 的默認(rèn)級別。下表展示 4 種隔離級別對這三個問題的解決程度。

隔離級別 臟讀 不可重復(fù)度 幻讀
讀未提交 可能 可能 可能
讀提交 不可能 可能 可能
可重復(fù)讀 不可能 不可能 可能
串行化 不可能 不可能 不可能

復(fù)習(xí)完事務(wù)的基礎(chǔ)知識,我們來看分布式事務(wù),顧名思義分布式事務(wù)就是要在分布式系統(tǒng)中實現(xiàn)事務(wù),保證在分布式系統(tǒng)中不同節(jié)點之間的數(shù)據(jù)一致性。分布式事務(wù)的實現(xiàn)有很多種,最具有代表性的是由Oracle Tuxedo系統(tǒng)提出的XA分布式事務(wù)協(xié)議。XA中大致分為兩部分:事務(wù)管理器和本地資源管理器。其中本地資源管理器往往由數(shù)據(jù)庫實現(xiàn),比如Oracle、DB2這些商業(yè)數(shù)據(jù)庫都實現(xiàn)了XA接口(Mysql5開始,innoDB引擎也實現(xiàn)了),而事務(wù)管理器作為全局的調(diào)度者,負(fù)責(zé)各個本地資源的提交和回滾。

XA協(xié)議包含兩階段提交(2PC)和三階段提交(3PC)兩種實現(xiàn),先來看2PC。

2PC

2PC(Two-phase commit protocol),中文叫二階段提交。 二階段提交是一種強一致性設(shè)計,分別為prepare階段和commitJ2PC 引入一個事務(wù)協(xié)調(diào)者的角色來協(xié)調(diào)管理各參與者的提交和回滾。

第一階段,作為事務(wù)協(xié)調(diào)者的節(jié)點會首先向所有的參與者節(jié)點發(fā)送Prepare請求。在接到Prepare請求之后,每一個參與者節(jié)點會各自執(zhí)行與事務(wù)有關(guān)的數(shù)據(jù)更新,如果參與者執(zhí)行成功,暫時不提交事務(wù),而是向事務(wù)協(xié)調(diào)節(jié)點返回“完成”消息。

第二階段,如果事務(wù)協(xié)調(diào)節(jié)點在之前所收到都是正向返回,那么它將會向所有事務(wù)參與者發(fā)出Commit請求,接到Commit請求之后,事務(wù)參與者節(jié)點會各自進行本地的事務(wù)提交,并釋放鎖資源。當(dāng)本地事務(wù)完成提交后,將會向事務(wù)協(xié)調(diào)者返回“完成”消息,當(dāng)事務(wù)協(xié)調(diào)者接收到所有事務(wù)參與者的“完成”反饋,整個分布式事務(wù)完成。

那么失敗情況呢?在XA的第一階段,如果某個事務(wù)參與者反饋失敗消息,說明該節(jié)點的本地事務(wù)執(zhí)行不成功,必須回滾。在第二階段,事務(wù)協(xié)調(diào)節(jié)點向所有的事務(wù)參與者發(fā)送Abort請求。接收到Abort請求之后,各個事務(wù)參與者節(jié)點需要在本地進行事務(wù)的回滾操作。

由上述可知,XA兩階段提交解決了分布式事務(wù)的問題并保證了強一致性,但是它也有明顯的不足:
1.性能問題
XA協(xié)議遵循強一致性。在事務(wù)執(zhí)行過程中,所有參與節(jié)點都是事務(wù)阻塞型的。當(dāng)參與者占有公共資源時,其他第三方節(jié)點訪問公共資源不得不處于阻塞狀態(tài)。這樣的過程有著非常明顯的性能問題。
2.協(xié)調(diào)者單點故障問題
事務(wù)協(xié)調(diào)者是整個XA模型的核心,一旦事務(wù)協(xié)調(diào)者節(jié)點掛掉,參與者收不到提交或是回滾通知,參與者會一直處于中間狀態(tài)無法完成事務(wù)。(如果是協(xié)調(diào)者掛掉,可以重新選舉一個協(xié)調(diào)者,但是無法解決因為協(xié)調(diào)者宕機導(dǎo)致的參與者處于阻塞狀態(tài)的問題)
3.丟失消息導(dǎo)致的不一致問題
在XA協(xié)議的第二個階段,如果發(fā)生局部網(wǎng)絡(luò)問題,一部分事務(wù)參與者收到了提交消息,另一部分事務(wù)參與者沒收到提交消息,那么就導(dǎo)致了節(jié)點之間數(shù)據(jù)的不一致。

基于這些問題,大家又提出了3PC(三階段提交)的概念,三階段提交協(xié)議在協(xié)調(diào)者和參與者中都引入超時機制,并且把兩階段提交協(xié)議的第一個階段拆分成了兩步:詢問,然后再鎖資源,最后真正提交。三階段提交的三個階段分別為:can_commit,pre_commit,do_commit。can_commit階段與2PC的prepare階段相比,變更成不會直接執(zhí)行事務(wù),而是會先去詢問此時的參與者是否有條件接這個事務(wù),因此不會一來就干活直接鎖資源,使得在某些資源不可用的情況下所有參與者都阻塞著。在do_commit階段,如果參與者無法及時接收到來自協(xié)調(diào)者的do_commit或者abort請求時,會在等待超時之后,繼續(xù)進行事務(wù)的提交。(因為進入第三階段,說明參與者在第二階段已經(jīng)收到了PreCommit請求,可能由于網(wǎng)絡(luò)超時等原因,參與者沒有收到commit或者abort響應(yīng),但是成功提交的幾率很大)。

相對于2PC,3PC主要解決的單點故障問題,并減少阻塞, 因為一旦參與者無法及時收到來自協(xié)調(diào)者的信息之后,他會默認(rèn)執(zhí)行commit,而不會一直持有事務(wù)資源并處于阻塞狀態(tài)。但是這種機制也會導(dǎo)致數(shù)據(jù)一致性問題。因為,由于網(wǎng)絡(luò)原因,協(xié)調(diào)者發(fā)送的abort響應(yīng)沒有及時被參與者接收到,那么參與者在等待超時之后執(zhí)行了commit操作。這樣就和其他接到abort命令并執(zhí)行回滾的參與者之間存在數(shù)據(jù)不一致的情況。

總的來說,3PC通過預(yù)提交階段可以減少協(xié)調(diào)者單點故障的問題,但是不能保證數(shù)據(jù)一致性。而且多引入一個階段也多一個交互,因此性能會更差一些。

除了XA,我們再了解下TCC事務(wù)

TCC
2PC 和 3PC 都是數(shù)據(jù)庫層面的,而 TCC 是業(yè)務(wù)層面的分布式事務(wù),TCC 指的是Try - Confirm - Cancel。

  • Try 指的是預(yù)留,即資源的預(yù)留和鎖定,注意是預(yù)留。
  • Confirm 指的是確認(rèn)操作,這一步其實就是真正的執(zhí)行了。
  • Cancel 指的是撤銷操作,可以理解為把預(yù)留階段的動作撤銷了。

其實從思想上看和 2PC 差不多,都是先試探性的執(zhí)行,如果都可以那就真正的執(zhí)行,如果不行就回滾。比如說一個事務(wù)要執(zhí)行A、B、C三個操作,那么先對三個操作執(zhí)行預(yù)留動作。如果都預(yù)留成功了那么就執(zhí)行確認(rèn)操作,如果有一個預(yù)留失敗那就都執(zhí)行撤銷動作。TCC模型有個事務(wù)管理者的角色,用來記錄TCC全局事務(wù)狀態(tài)并提交或者回滾事務(wù)。

相對于 2PC、3PC ,TCC不需要全局鎖,性能更好。但是開發(fā)量也更大,畢竟都在業(yè)務(wù)上實現(xiàn),對于每一個操作都需要定義三個動作分別對應(yīng)Try - Confirm - Cancel。不過也因為是在業(yè)務(wù)上實現(xiàn)的,所以TCC可以跨數(shù)據(jù)庫、跨不同的業(yè)務(wù)系統(tǒng)來實現(xiàn)事務(wù)。

最后我們再介紹幾種事務(wù)模式,分別是SagaMQ消息表模式

Saga事務(wù)
Saga 是長事務(wù)解決方案,思路是將大事務(wù)拆分若干個小事務(wù),需要為每個子事務(wù)都實現(xiàn)正向操作和補償操作。當(dāng)子事務(wù)執(zhí)行成功時,則繼續(xù)執(zhí)行正向操作,直到成功。當(dāng)正向操作執(zhí)行失敗時,回滾本地事務(wù)的同時,會調(diào)用上一階段的補償操作,直到回到初始狀態(tài)。Saga與TCC類似,同樣沒有全局鎖。但實現(xiàn)起來更簡單。

MQ事務(wù)
RocketMQ 就很好的支持了MQ事務(wù),首先,給 Broker 發(fā)送事務(wù)消息即半消息,半消息不是說一半消息,而是這個消息對消費者來說不可見,然后發(fā)送成功后發(fā)送方再執(zhí)行本地事務(wù);再根據(jù)本地事務(wù)的結(jié)果向 Broker 發(fā)送 Commit 或者 RollBack 命令。并且 RocketMQ 的發(fā)送方會提供一個反查事務(wù)狀態(tài)接口,如果一段時間內(nèi)半消息沒有收到任何操作請求,那么 Broker 會通過反查接口得知發(fā)送方事務(wù)是否執(zhí)行成功,然后執(zhí)行 Commit 或者 RollBack 命令。如果是 Commit 那么訂閱方就能收到這條消息,再做對應(yīng)的操作,做完了之后再消費這條消息即可。如果是 RollBack 那么訂閱方收不到這條消息,等于事務(wù)就沒執(zhí)行過。
可以看出通過 RocketMQ 還是比較容易實現(xiàn)的,但也要考慮mq本身的可用性問題。

最后介紹下本地消息表事務(wù)

本地消息表顧名思義就是會有一張存放本地消息的表,一般都是放在數(shù)據(jù)庫中,然后在執(zhí)行業(yè)務(wù)的時候?qū)I(yè)務(wù)的執(zhí)行和把消息放入消息表中的操作放在同一個事務(wù)中,這樣就能保證消息放入本地表中業(yè)務(wù)肯定是執(zhí)行成功的。然后再去調(diào)用下一個操作,如果下一個操作調(diào)用成功了好說,消息表的消息狀態(tài)可以直接改成已成功。如果調(diào)用失敗也沒事,會有 后臺任務(wù)定時去讀取本地消息表,篩選出還未成功的消息再調(diào)用對應(yīng)的服務(wù),服務(wù)更新成功了再變更消息的狀態(tài)。這時候有可能消息對應(yīng)的操作不成功,因此也需要重試,重試就得保證對應(yīng)服務(wù)的方法是冪等的,而且一般重試會有最大次數(shù),超過最大次數(shù)可以記錄下報警讓人工處理。
可以看到本地消息表其實實現(xiàn)的是最終一致性,容忍了數(shù)據(jù)暫時不一致的情況。

總結(jié)
可以看出 2PC 和 3PC 是一種強一致性事務(wù),不過還是有數(shù)據(jù)不一致,阻塞等風(fēng)險,而且只能用在數(shù)據(jù)庫層面。
TCC和Saga類似,是一種補償性事務(wù)思想,適用的范圍更廣,在業(yè)務(wù)層面實現(xiàn),因此對業(yè)務(wù)的侵入性較大。
本地消息、事務(wù)消息和最大努力通知其實都是最終一致性事務(wù),因此適用于一些對時間不敏感的業(yè)務(wù)。


引用:
https://zhuanlan.zhihu.com/p/183753774
https://www.cnblogs.com/xrq730/p/5087378.html https://blog.csdn.net/kusedexingfu/article/details/103484198
https://blog.csdn.net/bjweimengshu/article/details/79607522

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容