天天看點

分布式事務實作方法

資料庫事務的概念

在講述分布式事務的概念之前,我們先來回顧下事務相關的一些概念。

事務的基本概念:

就是一個程式執行單元,裡面的操作要麼全部執行成功,要麼全部執行失敗,不允許隻成功一半另外一半執行失敗的事情發生。例如一段事務代碼做了兩次資料庫更新操作,那麼這兩次資料庫操作要麼全部執行成功,要麼全部復原。

事務的基本特性:

我們知道事務有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的事務消息特性實作分布式系統的最終一緻性?》。敬請期待!

繼續閱讀