1. 程式人生 > 程式設計 >分散式事務之深入理解什麼是2PC、3PC及TCC協議?

分散式事務之深入理解什麼是2PC、3PC及TCC協議?

前言

在上一篇文章《教你一手如何基於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的具體原理圖如?: