淺談分散式事務及解決方案

2023-10-17 15:01:47

1 背景

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

1.1 事務的基本概念

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

1.2 事務的基本特性

我們知道事務有4個非常重要的特性,即我們常說的(ACID)。

  • Atomicity(原子性):一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被恢復(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
  • Consistency(一致性):在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。
  • Isolation(隔離性):資料庫允許多個並行事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並行執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(Serializable)。
  • Durability(永續性):事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失

2 分散式事務

其實分散式事務從實質上看與資料庫事務的概念是一致的,既然是事務也就需要滿足事務的基本特性(ACID),只是分散式事務相對於本地事務而言其表現形式有很大的不同。

本地事務的時代,如果需要同時運算元據庫的多條記錄,而這些操作可以放到一個事務中,那麼我們可以通過資料庫提供的事務機制就可以實現。

而隨著微服務架構的推進,原本一個本地邏輯執行單元,被拆分到了多個獨立的微服務中,這些微服務又分別操作了不同的資料庫和表。

比如下個指派個體的運輸任務,下運輸任務的同時要生成需求、計劃、任務,還要去呼叫詢價服務,投保服務,那麼,一旦產生任何一個服務異常,都會產生事務性的問題。雖然對於我們現有邏輯來說,可以由運營作廢,但未來自動化之後呢?

分散式事務是為了解決微服務架構(形式都是分散式系統)中不同節點之間的資料一致性問題。這個一致性問題本質上解決的也是傳統事務需要解決的問題,即一個請求在多個微服務呼叫鏈中,所有服務的資料處理要麼全部成功,要麼全部回滾。當然分散式事務問題的形式可能與傳統事務會有比較大的差異,但是問題本質是一致的,都是要求解決資料的一致性問題。

而分散式事務的實現方式有很多種,最具有代表性的是由Oracle Tuxedo系統提出的XA分散式事務協定。XA協定包括兩階段提交(2PC)和三階段提交(3PC)兩種實現,接下來我們分別來介紹下這兩種實現方式的原理。

3 兩階段提交(2PC)

兩階段提交又稱2PC(two-phase commit protocol),2PC是一個非常經典的強一致、中心化的原子提交協定。這裡所說的中心化是指協定中有兩個角色:一個是分散式事務協調者(coordinator)和N個參與者(participant)。

3.1 2PC執行原理

兩階段提交,顧名思義就是要進行兩個階段的提交:第一階段,準備階段(投票階段);第二階段,提交階段(執行階段)。

3.1.1 準備階段(Prepare phase)

  1. 分散式事務的發起方,向分散式事務協調者(Coordinator,也可以叫事務管理TransactionManager)傳送請求,
  2. Coordinator分別向參與者(Participant)A、參與者(Participant)B分別傳送事務預處理請求,稱之為Prepare,有些資料也叫」Vote Request」。
  3. 此時這些參與者節點一般來說就會開啟本地資料庫事務,然後開始執行資料庫本地事務,每個資料庫參與者在本地執行事務並寫原生的Undo/Redo紀錄檔(Undo紀錄檔是記錄修改前的資料,用於資料庫回滾,Redo紀錄檔是記錄修改後的資料,用於提交事務後寫入資料檔案),但在執行完成後並不會立馬提交資料庫本地事務,而是先向Coordinator進行「Vote Commit」的反饋,告知處理結果。
  4. 如果所有的參與者都向協調者做了「Vote Commit」的反饋的話,那麼流程進入第二個階段。

3.1.2 提交階段(commit phase)

1)如果所有參與者均反饋的是成功,協調者就會向所有參與者傳送「全域性提交確認通知(global_commit)」,參與者Participant就會完成自身本地資料庫事務的提交,並將提交結果回覆「ack」訊息給協調者Coordinator,然後協調者Coordinator就會向呼叫方返回分散式事務處理完成的結果。如果有任何一個參與者返回失敗,則回滾事務。

2)如果參與者向協調者反饋「Vote_Abort」訊息,即返回了失敗的訊息。此時分散式事務協調者Coordinator就會向所有的參與者Participant發起事務回滾的訊息(「global_rollback」),此時各個參與者就會回滾本地事務,釋放資源,並且向協調者傳送「ack」確認訊息,協調者就會向呼叫方返回分散式事務處理失敗的結果。

以上就是兩階段提交的基本過程了,那麼按照這個兩階段提交協定,分散式系統的資料一致性問題就能解決麼?

3.2 2PC存在的問題

其實,2PC只是通過增加了事務協調者(Coordinator)的角色來通過2個階段的處理流程來解決分散式系統中一個事務需要跨多個服務的資料一致性問題。

以下幾點是XA-兩階段提交協定中會遇到的一些問題:

  • 效能問題:2PC中的所有的參與者節點都為事務阻塞型,當某一個參與者節點出現通訊超時,其餘參與者都會被動阻塞佔用資源不能釋放。
  • 協調者單點故障問題:由於嚴重的依賴協調者,一旦協調者發生故障,而此時參與者還都處於鎖定資源的狀態,無法完成事務commit操作。雖然協調者出現故障後,會重新選舉一個協調者,可無法解決因前一個協調者宕機導致的參與者處於阻塞狀態的問題。
  • 網路閃斷導致腦裂:第二階段中協調者向參與者傳送commit命令之後,一旦此時發生網路抖動,導致一部分參與者接收到了commit請求並執行,可其他未接到commit請求的參與者無法執行事務提交。進而導致整個分散式系統出現了資料不一致。

4 三階段提交(3PC)

三階段提交又稱3PC,在2PC的基礎上增加了CanCommit階段,並引入了超時機制。一旦事務參與者遲遲沒有收到協調者的Commit請求,就會自動進行本地commit,這樣相對有效地解決了協調者單點故障的問題。

4.1 3PC執行原理

4.1.1 CanCommit階段

  1. 協調者向參與者發出CanCommit ,進行事務詢問操作,所有參與者都反饋yes後,才能進入下一個階段。(這一個階段時不鎖表,不像2pc 第一個階段就開始鎖表,3pc的階段一是為了先排除個別參與者不具備提交事務能力的前提下,而避免鎖表。)簡單來說就是檢查下自身狀態的健康性。
  2. 有任何一個參與者反饋的結果是No,整個分散式事務就會中斷,協調者就會向所有的參與者傳送「abort」請求。

4.1.2 PreCommit階段

  1. 在階段一中,如果所有的參與者都返回Yes的話,那麼就會進入PreCommit階段進行事務預提交。此時分散式事務協調者會向所有的參與者傳送PreCommit請求,參與者收到後開始執行事務操作,並將Undo和Redo資訊記錄到事務紀錄檔中。參與者執行完事務操作後(此時屬於未提交事務的狀態),就會向協調者反饋「Ack」表示已經準備好提交,並等待協調者的下一步指令。
  2. 有任何一個參與者反饋的結果是No,或協調者在等待參與者節點反饋的過程中超時(2PC中只有協調者可以超時,參與者沒有超時機制)。整個分散式事務就會中斷,協調者就會向所有的參與者傳送「abort」請求。

4.1.3 DoCommit階段

  1. 在階段二中如果所有的參與者都可以進行PreCommit提交,那麼協調者就會從「預提交狀態」->「提交狀態」。然後向所有的參與者傳送」doCommit」請求,參與者在收到提交請求後,執行事務提交操作,並向協調者反饋「Ack」訊息,協調者收到所有參與者的Ack訊息後完成事務。
  2. 同樣,如果有一個參與者節點未完成PreCommit的反饋或者反饋超時,那麼協調者都會向所有的參與者節點傳送abort請求,從而中斷事務。

相比較2PC而言,3PC對於協調者(Coordinator)和參與者(Participant)都設定了超時時間,解決了參與者在長時間無法與協調者節點通訊(協調者掛掉了)的情況下,無法釋放資源的問題,因為參與者自身擁有超時機制會在超時後,自動進行本地commit從而進行釋放資源。而這種機制也側面降低了整個事務的阻塞時間和範圍。
另外,通過CanCommit、PreCommit、DoCommit三個階段的設計,相較於2PC而言,多設定了一個緩衝階段保證了在最後提交階段之前各參與節點的狀態是一致的。

3PC的缺點:

3PC在去除阻塞的同時也引入了新的問題,那就是參與者接收到precommit訊息後,如果出現網路分割區,此時協調者所在的節點和參與者無法進行正常的網路通訊,在這種情況下,該參與者依然會進行事務的提交,這必然出現資料的不一致性。

5 補償事務(TCC)

TCC與2PC、3PC一樣,只是分散式事務的一種實現方案。

5.1 TCC原理:

TCC(Try-Confirm-Cancel)又稱補償事務。其核心思想是:」針對每個操作都要註冊一個與其對應的確認和補償(復原操作)」。它分為三個操作:

  • Try階段:主要是對業務系統做檢測及資源預留,比如說凍結庫存。
  • Confirm階段:確認執行業務操作。
  • Cancel階段:取消執行業務操作。

TCC事務的處理流程與2PC兩階段提交類似,不過2PC通常都是在跨庫的DB層面,而TCC本質上就是一個應用層面的2PC,需要通過業務邏輯來實現。這種分散式事務的實現方式的優勢在於,可以讓應用自己定義資料庫操作的粒度,使得降低鎖衝突、提高吞吐量成為可能。

而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網路狀態、系統故障等不同的失敗原因實現不同的回滾策略。為了滿足一致性的要求,confirm和cancel介面還必須實現冪等。

TCC的具體原理圖如下:

5.2 注意事項:

1.業務操作分兩階段完成:

接入TCC前,業務操作只需要一步就能完成,但是在接入TCC之後,需要考慮如何將其分成2階段完成,把資源的檢查和預留放在一階段的Try操作中進行,把真正的業務操作的執行放在二階段的Confirm操作中進行;
TCC服務要保證第一階段Try操作成功之後,二階段Confirm操作一定能成功;

2.允許空回滾;

事務協調器在呼叫TCC服務的一階段Try操作時,可能會出現因為丟包而導致的網路超時,此時事務協調器會觸發二階段回滾,呼叫TCC服務的Cancel操作;
TCC服務在未收到Try請求的情況下收到Cancel請求,這種場景被稱為空回滾;TCC服務在實現時應當允許空回滾的執行;

3.防懸掛控制;

事務協調器在呼叫TCC服務的一階段Try操作時,可能會出現因網路擁堵而導致的超時,此時事務協調器會觸發二階段回滾,呼叫TCC服務的Cancel操作;在此之後,擁堵在網路上的一階段Try封包被TCC服務收到,出現了二階段Cancel請求比一階段Try請求先執行的情況;
使用者在實現TCC服務時,應當允許空回滾,但是要拒絕執行空回滾之後到來的一階段Try請求;

4.冪等控制:

無論是網路封包重傳,還是異常事務的補償執行,都會導致TCC服務的Try、Confirm或者Cancel操作被重複執行;使用者在實現TCC服務時,需要考慮冪等控制,即Try、Confirm、Cancel 執行次和執行多次的業務結果是一樣的;

5.業務資料可見性控制;

TCC服務的一階段Try操作會做資源的預留,在二階段操作執行之前,如果其他事務需要讀取被預留的資源資料,那麼處於中間狀態的業務資料該如何向用戶展示,需要業務在實現時考慮清楚;通常的設計原則是「寧可不展示、少展示,也不多展示、錯展示」;

6.業務資料並行存取控制;

TCC服務的一階段Try操作預留資源之後,在二階段操作執行之前,預留的資源都不會被釋放;如果此時其他分散式事務修改這些業務資源,會出現分散式事務的並行問題;
使用者在實現TCC服務時,需要考慮業務資料的並行控制,儘量將邏輯鎖粒度降到最低,以最大限度的提高分散式事務的並行性;

6 Hmily

Hmily (How much I love you)
高效能分散式事務tcc開源框架。基於java語言來開發(JDK1.8),支援dubbo,springcloud,motan等rpc框架進行分散式事務。

框架特性

  • 支援巢狀事務(Nested transaction support).
  • 採用disruptor框架進行事務紀錄檔的非同步讀寫,與RPC框架的效能毫無差別。
  • 支援SpringBoot-starter 專案啟動,使用簡單。
  • RPC框架支援 : dubbo,motan,springcloud。
  • 本地事務儲存支援 : redis,mongodb,zookeeper,file,mysql。
  • 事務紀錄檔序列化支援 :java,hessian,kryo,protostuff。
  • 採用Aspect AOP 切面思想與Spring無縫整合,天然支援叢集。
  • 內建經典的分散式事務場景demo工程,並有swagger-ui視覺化介面可以快速體驗。

6.1 Hmily原理及流程圖

原理圖:

流程圖:

7 參考文獻

作者:京東物流 宋樂

來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源