導讀
在上一篇文章《【分布式事務】基於RocketMQ搭建生產級消息集群?》中給大家介紹了基於RocketMQ如何搭建生產級消息集群。因為本系列文章最終的目的是介紹基於RocketMQ的事物消息來解決分布式系統中的數據一致性問題,所以先給大家率先介紹了RocketMQ消息集群的搭建。
原本是想着在這篇文章中直接介紹RocketMQ的事務消息特性,但是在梳理的過程中作者發現對於分布式事務的概念,可能還會有很多同學不理解或者理解得不是很深刻的地方,而跳過這些基本概念直接去學習上層的實踐可能並不是一件很好的事情,因此在這篇文章中,作者打算重點給大家先介紹下分布式事務相關的基本概念,諸如分布式事務、2PC、3PC、TCC之類的基本問題,之后再單獨去介紹RocketMQ事務消息相關的實踐。
數據庫事務的概念
在講述分布式事務的概念之前,我們先來回顧下事務相關的一些概念。
事務的基本概念:
就是一個程序執行單元,里面的操作要么全部執行成功,要么全部執行失敗,不允許只成功一半另外一半執行失敗的事情發生。例如一段事務代碼做了兩次數據庫更新操作,那么這兩次數據庫操作要么全部執行成功,要么全部回滾。
事務的基本特性:
我們知道事務有4個非常重要的特性,即我們常說的(ACID)。
Atomicity(原子性):是說事務是一個不可分割的整體,所有操作要么全做,要么全不做;只要事務中有一個操作出錯,回滾到事務開始前的狀態的話,那么之前已經執行的所有操作都是無效的,都應該回滾到開始前的狀態。
Consistency(一致性):是說事務執行前后,數據從一個狀態到另一個狀態必須是一致的,比如A向B轉賬( A、B的總金額就是一個一致性狀態),不可能出現A扣了錢,B卻沒收到的情況發生。
Isolation(隔離性): 多個並發事務之間相互隔離,不能互相干擾。關於事務的隔離性,可能不是特別好理解,這里的並發事務是指兩個事務操作了同一份數據的情況;而對於並發事務操作同一份數據的隔離性問題,則是要求不能出現臟讀、幻讀的情況,即事務A不能讀取事務B還沒有提交的數據,或者在事務A讀取數據進行更新操作時,不允許事務B率先更新掉這條數據。而為了解決這個問題,常用的手段就是加鎖了,對於數據庫來說就是通過數據庫的相關鎖機制來保證。
Durablity(持久性):事務完成后,對數據庫的更改是永久保存的,不能回滾。
關於數據庫事務的基本概念大家可以去網上搜一下,這里只是給大家回顧下事務的基本概念及特性,諸如事務並發問題、事務隔離級別等大家如有遺忘可以去回顧下(tips:面試經常會問到的問題哦)。
什么是分布式事務
以上內容我們回顧了下事務的基本概念,那么分布式事務又是個什么概念呢?它與數據庫事務之間又有什么區別呢?
其實分布式事務從實質上看與數據庫事務的概念是一致的,既然是事務也就需要滿足事務的基本特性(ACID),只是分布式事務相對於本地事務而言其表現形式有很大的不同。舉個例子,在一個JVM進程中如果需要同時操作數據庫的多條記錄,而這些操作需要在一個事務中,那么我們可以通過數據庫提供的事務機制(一般是數據庫鎖)來實現。
而隨着這個JVM進程(應用)被拆分成了微服務架構,原本一個本地邏輯執行單元被拆分到了多個獨立的微服務中,這些微服務又分別操作不同的數據庫和表,服務之間通過網絡調用。
舉個例子:服務A收到一筆購物下單請求后,需要調用服務B去支付,支付成功則處理購物訂單為待發貨狀態,否則就需要將購物訂單處理為失敗狀態。(如圖所示)
在上面這個例子中會不會出現服務B支付成功了,但是由於網絡調用的問題沒有通知到服務A,導致用戶付了錢,但是購物訂單無法顯示支付成功的狀態呢?
答案是這種情況是普遍存在的,因為服務B在處理成功后需要向服務A發送網絡請求,而這個過程是極有可能失敗的。那么如何確保“服務A->服務B”這個過程能夠組成一個事務,要么全部成功、要么全部失敗呢?而這就是典型的需要通過分布式事務解決的問題。
分布式事務是為了解決微服務架構(形式都是分布式系統)中不同節點之間的數據一致性問題。這個一致性問題本質上解決的也是傳統事務需要解決的問題,即一個請求在多個微服務調用鏈中,所有服務的數據處理要么全部成功,要么全部回滾。當然分布式事務問題的形式可能與傳統事務會有比較大的差異,但是問題本質是一致的,都是要求解決數據的一致性問題。
而分布式事務的實現方式有很多種,最具有代表性的是由Oracle Tuxedo系統提出的XA分布式事務協議。XA協議包括兩階段提交(2PC)和三階段提交(3PC)兩種實現,接下來我們分別來介紹下這兩種實現方式的原理。
兩階段提交(2PC)
兩階段提交又稱2PC(two-phase commit protocol),2pc是一個非常經典的強一致、中心化的原子提交協議。這里所說的中心化是指協議中有兩類節點:一個是中心化協調者節點(coordinator)和N個參與者節點(partcipant)。
下面我們就以一個盡量貼近實際業務場景的操作來舉例:"假設在一個分布式架構的系統中事務的發起者通過分布式事務協調者(如RocketMQ,在早期RocketMQ版本不提供事務消息特性時,有些公司會自己研發一個基於MQ的可靠消息服務來實現一定的分布式事務的特性)分別向應用服務A、應用服務B發起處理請求,二者在處理的過程中會分別操作自身服務的數據庫,現在要求應用服務A、應用服務B的數據處理操作要在一個事務里"?
在上面這個例子中如果采用兩階段提交來實現分布式事務,那么其運行原理應該是個什么樣的呢?(如👇):
第一階段:請求/表決階段(點擊放大)
既然稱為兩階段提交,說明在這個過程中是大致存在兩個階段的處理流程。第一個階段如👆圖所示,這個階段被稱之為請求/表決階段。是個什么意思呢?
就是在分布式事務的發起方在向分布式事務協調者(Coordinator)發送請求時,Coordinator首先會分別向參與者(Partcipant)節點A、參與這節點(Partcipant)節點B分別發送事務預處理請求,稱之為Prepare,有些資料也叫"Vote Request"。
說的直白點就是問一下這些參與節點"這件事你們能不能處理成功了",此時這些參與者節點一般來說就會打開本地數據庫事務,然后開始執行數據庫本地事務,但在執行完成后並不會立馬提交數據庫本地事務,而是先向Coordinator報告說:“我這邊可以處理了/我這邊不能處理”。
如果所有的參與這節點都向協調者作了“Vote Commit”的反饋的話,那么此時流程就會進入第二個階段了。
第二階段:提交/執行階段(正常流程)
如果所有參與者節點都向協調者報告說“我這邊可以處理”,那么此時協調者就會向所有參與者節點發送“全局提交確認通知(global_commit)”,即你們都可以進行本地事務提交了,此時參與者節點就會完成自身本地數據庫事務的提交,並最終將提交結果回復“ack”消息給Coordinator,然后Coordinator就會向調用方返回分布式事務處理完成的結果。
第二階段:提交/執行階段(異常流程)
相反,在第二階段除了所有的參與者節點都反饋“我這邊可以處理了”的情況外,也會有節點反饋說“我這邊不能處理”的情況發生,此時參與者節點就會向協調者節點反饋“Vote_Abort”的消息。此時分布式事務協調者節點就會向所有的參與者節點發起事務回滾的消息(“global_rollback”),此時各個參與者節點就會回滾本地事務,釋放資源,並且向協調者節點發送“ack”確認消息,協調者節點就會向調用方返回分布式事務處理失敗的結果。
以上就是兩階段提交的基本過程了,那么按照這個兩階段提交協議,分布式系統的數據一致性問題就能得到滿足嗎?
實際上分布式事務是一件非常復雜的事情,兩階段提交只是通過增加了事務協調者(Coordinator)的角色來通過2個階段的處理流程來解決分布式系統中一個事務需要跨多個服務節點的數據一致性問題。但是從異常情況上考慮,這個流程也並不是那么的無懈可擊。
假設如果在第二個階段中Coordinator在接收到Partcipant的"Vote_Request"后掛掉了或者網絡出現了異常,那么此時Partcipant節點就會一直處於本地事務掛起的狀態,從而長時間地占用資源。當然這種情況只會出現在極端情況下,然而作為一套健壯的軟件系統而言,異常Case的處理才是真正考驗方案正確性的地方。
以下幾點是XA-兩階段提交協議中會遇到的一些問題:
-
性能問題。從流程上我們可以看得出,其最大缺點就在於它的執行過程中間,節點都處於阻塞狀態。各個操作數據庫的節點此時都占用着數據庫資源,只有當所有節點准備完畢,事務協調者才會通知進行全局提交,參與者進行本地事務提交后才會釋放資源。這樣的過程會比較漫長,對性能影響比較大。
-
協調者單點故障問題。事務協調者是整個XA模型的核心,一旦事務協調者節點掛掉,會導致參與者收不到提交或回滾的通知,從而導致參與者節點始終處於事務無法完成的中間狀態。
-
丟失消息導致的數據不一致問題。在第二個階段,如果發生局部網絡問題,一部分事務參與者收到了提交消息,另一部分事務參與者沒收到提交消息,那么就會導致節點間數據的不一致問題。
既然兩階段提交有以上問題,那么有沒有其他的方案來解決呢?
三階段提交(3PC)
三階段提交又稱3PC,其在兩階段提交的基礎上增加了CanCommit階段,並引入了超時機制。一旦事務參與者遲遲沒有收到協調者的Commit請求,就會自動進行本地commit,這樣相對有效地解決了協調者單點故障的問題。
但是性能問題和不一致問題仍然沒有根本解決。下面我們還是一起看下三階段流程的是什么樣的?
第一階段:CanCommit階段
這個階段類似於2PC中的第二個階段中的Ready階段,是一種事務詢問操作,事務的協調者向所有參與者詢問“你們是否可以完成本次事務?”,如果參與者節點認為自身可以完成事務就返回“YES”,否則“NO”。而在實際的場景中參與者節點會對自身邏輯進行事務嘗試,其實說白了就是檢查下自身狀態的健康性,看有沒有能力進行事務操作。
第二階段:PreCommit階段
在階段一中,如果所有的參與者都返回Yes的話,那么就會進入PreCommit階段進行事務預提交。此時分布式事務協調者會向所有的參與者節點發送PreCommit請求,參與者收到后開始執行事務操作,並將Undo和Redo信息記錄到事務日志中。參與者執行完事務操作后(此時屬於未提交事務的狀態),就會向協調者反饋“Ack”表示我已經准備好提交了,並等待協調者的下一步指令。
否則,如果階段一中有任何一個參與者節點返回的結果是No響應,或者協調者在等待參與者節點反饋的過程中超時(2PC中只有協調者可以超時,參與者沒有超時機制)。整個分布式事務就會中斷,協調者就會向所有的參與者發送“abort”請求。
第三階段:DoCommit階段
在階段二中如果所有的參與者節點都可以進行PreCommit提交,那么協調者就會從“預提交狀態”-》“提交狀態”。然后向所有的參與者節點發送"doCommit"請求,參與者節點在收到提交請求后就會各自執行事務提交操作,並向協調者節點反饋“Ack”消息,協調者收到所有參與者的Ack消息后完成事務。
相反,如果有一個參與者節點未完成PreCommit的反饋或者反饋超時,那么協調者都會向所有的參與者節點發送abort請求,從而中斷事務。
看到這里,你是不是會疑惑"3PC相對於2PC而言到底優化了什么地方呢?"
相比較2PC而言,3PC對於協調者(Coordinator)和參與者(Partcipant)都設置了超時時間,而2PC只有協調者才擁有超時機制。這解決了一個什么問題呢?這個優化點,主要是避免了參與者在長時間無法與協調者節點通訊(協調者掛掉了)的情況下,無法釋放資源的問題,因為參與者自身擁有超時機制會在超時后,自動進行本地commit從而進行釋放資源。而這種機制也側面降低了整個事務的阻塞時間和范圍。
另外,通過CanCommit、PreCommit、DoCommit三個階段的設計,相較於2PC而言,多設置了一個緩沖階段保證了在最后提交階段之前各參與節點的狀態是一致的。
以上就是3PC相對於2PC的一個提高(相對緩解了2PC中的前兩個問題),但是3PC依然沒有完全解決數據不一致的問題。
補償事務(TCC)
說起分布式事務的概念,不少人都會搞混淆,似乎好像分布式事務就是TCC。實際上TCC與2PC、3PC一樣,只是分布式事務的一種實現方案而已。
TCC(Try-Confirm-Cancel)又稱補償事務。其核心思想是:"針對每個操作都要注冊一個與其對應的確認和補償(撤銷操作)"。它分為三個操作:
-
Try階段:主要是對業務系統做檢測及資源預留。
-
Confirm階段:確認執行業務操作。
-
Cancel階段:取消執行業務操作。
TCC事務的處理流程與2PC兩階段提交類似,不過2PC通常都是在跨庫的DB層面,而TCC本質上就是一個應用層面的2PC,需要通過業務邏輯來實現。這種分布式事務的實現方式的優勢在於,可以讓應用自己定義數據庫操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。
而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。為了滿足一致性的要求,confirm和cancel接口還必須實現冪等。
TCC的具體原理圖如👇:
消息隊列MQ事務
在前面介紹2PC、3PC的時候我們說沒有根本解決性能問題,而如果通過MQ的事務消息來進行異步解耦,並實現系統的數據的最終一致性的話會不會好很多呢?實際上這就是我們下一篇文章要繼續講述的《分布式事務之如何基於RocketMQ的事務消息特性實現分布式系統的最終一致性?》。敬請期待!