分散式事物

LZC發表於2020-12-16

在單個資料庫的情況下,資料事務操作具有 ACID 四個特性,但如果在一個事務中操作多個資料庫,則無法使用資料庫事務來保證一致性。

也就是說,當兩個資料庫運算元據時,可能存在一個資料庫操作成功,而另一個資料庫操作失敗的情況,我們無法通過單個資料庫事務來回滾兩個資料操作。

而分散式事務就是為了解決在同一個事務下,不同節點的資料庫運算元據不一致的問題。在一個事務操作請求多個服務或多個資料庫節點時,要麼所有請求成功,要麼所有請求都失敗回滾回去。通常,分散式事務的實現有多種方式,例如 XA 協議實現的二階提交(2PC)、三階提交 (3PC),以及 TCC 補償性事務。

在瞭解 2PC 和 3PC 之前,我們有必要先來了解下 XA 協議。XA 協議是由 X/Open 組織提出的一個分散式事務處理規範,目前 MySQL 中只有 InnoDB 儲存引擎支援 XA 協議。

XA 規範

在 XA 規範之前,存在著一個 DTP 模型,該模型規範了分散式事務的模型設計。

DTP 規範中主要包含了 AP、RM、TM 三個部分,其中 AP 是應用程式,是事務發起和結束的地方;RM 是資源管理器,主要負責管理每個資料庫的連線資料來源;TM 是事務管理器,負責事務的全域性管理,包括事務的生命週期管理和資源的分配協調等。

XA 則規範了 TM 與 RM 之間的通訊介面,在 TM 與多個 RM 之間形成一個雙向通訊橋樑,從而在多個資料庫資源下保證 ACID 四個特性。

二階提交(2PC)

XA 規範實現的分散式事務屬於二階提交事務,顧名思義就是通過兩個階段來實現事務的提交。

第一階段,應用程式向事務管理器(TM)發起事務請求,而事務管理器則會分別向參與的各個資源管理器(RM)傳送事務預處理請求(Prepare),此時這些資源管理器會開啟本地資料庫事務,然後開始執行資料庫事務,但執行完成後並不會立刻提交事務,而是向事務管理器返回已就緒(Ready)或未就緒(Not Ready)狀態。如果各個參與節點都返回狀態了,就會進入第二階段。

第二階段,如果資源管理器返回的都是就緒狀態,事務管理器則會向各個資源管理器傳送提交(Commit)通知,資源管理器則會完成本地資料庫的事務提交,最終返回提交結果給事務管理器。

在第二階段中,如果任意資源管理器返回了未就緒狀態,此時事務管理器會向所有資源管理器傳送事務回滾(Rollback)通知,此時各個資源管理器就會回滾本地資料庫事務,釋放資源,並返回結果通知。

二階段提交的演算法思路可以概括為:協調者向參與者下發請求事務操作,參與者接收到請求後,進行相關操作並將操作結果通知協調者,協調者根據所有參與者的反饋結果決定各參與者是要提交操作還是撤銷操作。

二階事務提交也存在一些缺陷。

第一,在整個流程中,我們會發現各個資源管理器節點存在阻塞,只有當所有的節點都準備完成之後,事務管理器才會發出進行全域性事務提交的通知,這個過程如果很長,則會有很多資源管理器節點長時間佔用資源,從而影響整個節點的效能。只有協調者有超時機制,在第二階段中,如果資源管理器(RM)一直沒有收到事務管理器(TM)的“回滾”或者“提交”操作,那麼資源管理器會一直阻塞。

第二,仍然存在資料不一致的可能性。例如,在最後通知提交全域性事務時,由於網路故障,部分節點有可能收不到通知,由於這部分節點沒有提交事務,就會導致資料不一致的情況出現。

第三,單點故障。一旦事務管理器發生故障,整個系統都處於停滯狀態。尤其是在提交階段,一旦事務管理器發生故障,資源管理器會由於等待事物管理器的訊息,而一直鎖定事務資源,導致整個系統被阻塞。

三階提交(3PC)

三階段提交協議就有 CanCommit、PreCommit、DoCommit 三個階段,下面我們來看一下這個三個階段。

第一,CanCommit 階段。

協調者向參與者傳送請求操作(CanCommit 請求),詢問參與者是否可以執行事務提交操作,然後等待參與者的響應;參與者收到 CanCommit 請求之後,回覆 Yes,表示可以順利執行事務;否則回覆 No。

第二,PreCommit 階段。

協調者根據參與者第一階段(CanCommit)的回覆情況,來決定是否可以進行 PreCommit 操作(預提交階段)。

  1. 如果所有參與者回覆的都是“Yes”,協調者向參與者傳送 PreCommit 請求,進入預提交階段。

  2. 參與者接收到 PreCommit 請求後執行事務操作,並將 Undo 和 Redo 資訊記錄到事務日誌中。如果參與者成功執行了事務操作,則返回 ACK 響應,同時開始等待最終指令。

  3. 假如任何一個參與者向協調者傳送了“No”訊息,或者協調者等待超時之後都沒有收到參與者的響應,就執行中斷事務的操作:協調者向所有參與者傳送“Abort”訊息,參與者收到“Abort”訊息之後,則執行事務的中斷操作;

    如果參與者超時後仍未收到協調者的PreCommit訊息,也會執行事務的中斷操作。

預提交階段保證了在最後提交階段(DoCmmit 階段)之前所有參與者的狀態是一致的。

第三,DoCommit 階段。

DoCmmit 階段進行真正的事務提交,根據 PreCommit 階段協調者傳送的訊息,進入執行提交階段或事務中斷階段。

執行提交階段:若協調者接收到所有參與者傳送的 Ack 響應,則向所有參與者傳送 DoCommit 訊息,開始執行階段。參與者接收到 DoCommit 訊息之後,正式提交事務。完成事務提交之後,釋放所有鎖住的資源,並向協調者傳送 Ack 響應。協調者接收到所有參與者的 Ack 響應之後,完成事務。

事務中斷階段:協調者向所有參與者傳送 Abort 請求。參與者接收到 Abort 訊息之後,利用其在 PreCommit 階段記錄的 Undo 資訊執行事務的回滾操作,釋放所有鎖住的資源,並向協調者傳送 Ack 訊息。協調者接收到參與者反饋的 Ack 訊息之後,執行事務的中斷,並結束事務。

3PC 把 2PC 的準備階段分為了準備階段和預處理階段,在第一階段只是詢問各個資源節點是否可以執行事務,而在第二階段,所有的節點反饋可以執行事務,才開始執行事務操作,最後在第三階段執行提交或回滾操作。並且在事務管理器和資源管理器中都引入了超時機制,如果在第三階段,資源管理器一直無法收到來自事物管理器的提交或回滾請求,它就會在超時之後,繼續提交事務。

所以 3PC 可以通過超時機制,避免事物管理器掛掉所造成的長時間阻塞問題,但其實這樣還是無法解決在最後提交全域性事務時,由於網路故障無法通知到一些節點的問題,特別是回滾通知,這樣會導致事務等待超時從而預設提交。

事務補償機制(TCC)

以上這種基於 XA 規範實現的事務提交,由於阻塞等效能問題,有著比較明顯的低效能、低吞吐的特性。所以很難滿足系統的併發效能。

除了效能問題,JTA 只能解決同一服務下操作多資料來源的分散式事務問題,換到微服務架構下,可能存在同一個事務操作,分別在不同服務上連線資料來源,提交資料庫操作。

而 TCC 正是為了解決以上問題而出現的一種分散式事務解決方案。TCC 採用最終一致性的方式實現了一種柔性分散式事務,與 XA 規範實現的二階事務不同的是,TCC 的實現是基於服務層實現的一種二階事務提交。

TCC 分為三個階段,即 Try、Confirm、Cancel 三個階段。

  • Try 階段:主要嘗試執行業務,執行各個服務中的 Try 方法,主要包括預留操作;
  • Confirm 階段:確認 Try 中的各個方法執行成功,然後通過 TM 呼叫各個服務的 Confirm 方法,這個階段是提交階段;
  • Cancel 階段:當在 Try 階段發現其中一個 Try 方法失敗,例如預留資源失敗、程式碼異常等,則會觸發 TM 呼叫各個服務的 Cancel 方法,對全域性事務進行回滾,取消執行業務。

以上執行只是保證 Try 階段執行成功或失敗的提交和回滾操作,你肯定會想到,如果在 Confirm 和 Cancel 階段出現異常情況,那 TCC 該如何處理呢?此時 TCC 會不停地重試呼叫失敗的 Confirm 或 Cancel 方法,直到成功為止。

但 TCC 補償性事務也有比較明顯的缺點,那就是對業務的侵入性非常大。

首先,我們需要在業務設計的時候考慮預留資源;然後,我們需要編寫大量業務性程式碼,例如 Try、Confirm、Cancel 方法;最後,我們還需要為每個方法考慮冪等性。這種事務的實現和維護成本非常高,但綜合來看,這種實現是目前大家最常用的分散式事務解決方案。

可靠訊息最終一致性

2PC 和 3PC 核心思想均是以集中式的方式實現分散式事務,這兩種方法都存在兩個共同的缺點,一是,同步執行,效能差;二是,資料不一致問題。為了解決這兩個問題,通過分散式訊息來確保事務最終一致性的方案便出現了。

在 eBay 的分散式系統架構中,架構師解決一致性問題的核心思想就是:將需要分散式處理的事務通過訊息或者日誌的方式非同步執行,訊息或日誌可以存到本地檔案、資料庫或訊息佇列中,再通過業務規則進行失敗重試。

基於分散式訊息的最終一致性方案的事務處理,引入了一個訊息中介軟體(在本案例中,我們採用 Message Queue,MQ,訊息佇列),用於在多個應用之間進行訊息傳遞。實際使用中,阿里就是採用 RocketMQ 機制來支援訊息事務。

本地事務與訊息傳送的原子性問題

本地事務與訊息傳送的原子性問題:事務發起方在本地事務執行成功後訊息必須發出去,否則就丟棄訊息。即實
現本地事務和訊息傳送的原子性,要麼都成功,要麼都失敗。本地事務與訊息傳送的原子性問題是實現可靠訊息最
終一致性方案的關鍵問題。
第一種方案:先傳送訊息,再運算元據庫:

begin transaction;
// 1. 傳送MQ
// 2. 資料庫操作
commit transation;

這種情況無法保證資料庫操作與傳送訊息的一致性,因為可能會存在訊息傳送成功但是資料庫操作失敗

第二種方案:先運算元據庫,再傳送訊息

begin transaction;
// 1. 資料庫操作
// 2. 傳送MQ
commit transation;

這種情況看起來好像沒有問題,如果訊息傳送失敗並丟擲異常,資料庫事物就可以回滾。

如果是傳送訊息超時異常,此時丟擲異常就會回滾資料庫事物,但其實訊息是已經傳送成功了的,這也會導致資料不一致的情況

解決方案

本地訊息表方案

本地訊息表這個方案最初是eBay提出的,此方案的核心是通過本地事務保證資料業務操作和訊息的一致性,然後
通過定時任務將訊息傳送至訊息中介軟體,待確認訊息傳送給消費方成功再將訊息刪除。

我們“發訊息”這個過程,目的往往是通知另外一個系統或者模組去更新資料,訊息佇列中的“事務”,主要解決的是訊息生產者和訊息消費者的資料一致性問題。

拿我們熟悉的電商來舉個例子。一般來說,使用者在電商 APP 上購物時,先把商品加到購物車裡,然後幾件商品一起下單,最後支付,完成購物流程,就可以愉快地等待收貨了。

這個過程中有一個需要用到訊息佇列的步驟,訂單系統建立訂單後,發訊息給購物車系統,將已下單的商品從購物車中刪除。因為從購物車刪除已下單商品這個步驟,並不是使用者下單支付這個主要流程中必需的步驟,使用訊息佇列來非同步清理購物車是更加合理的設計。

以上圖為例:共有兩個微服務互動,訂單服務和購物車服務,訂單服務負責添建立訂單,購物車服務負責清理購物車。

互動流程如下:

  1. 建立訂單

    訂單服務在本地事務建立訂單和增加 ”清理購物車訊息日誌“。(訂單表和訊息表通過本地事務保證一致)
    下邊是虛擬碼

    begin transaction;
    // 1. 儲存訂單
    // 2. 儲存清理購物車訊息日誌
    commit transation;

    這種情況下,本地儲存訂單操作與儲存清理購物車訊息日誌處於同一個事務中,這兩個操作具備原子性。

  2. 定時任務掃描日誌

    如何保證將訊息傳送給訊息佇列呢?經過第一步訊息已經寫到訊息日誌表中,可以啟動獨立的執行緒,定時對訊息日誌表中的訊息進行掃描併傳送至訊息中介軟體,在訊息中介軟體反饋傳送成功後刪除該訊息日誌,否則等待定時任務下一週期重試。

  3. 消費訊息

    如何保證消費者一定能消費到訊息呢?這裡可以使用MQ的ack(即訊息確認)機制,消費者監聽MQ,如果消費者接收到訊息並且業務處理完成後向MQ傳送ack(即訊息確認),此時說明消費者正常消費訊息完成,MQ將不再向消費者推送訊息,否則消費者會不斷重試向消費者來傳送訊息。

    購物車服務接收到”清理購物車“訊息,開始清理購物車,購物車清理成功後向訊息中介軟體回應ack,否則訊息中介軟體將重複投遞此訊息。由於訊息會重複投遞,購物車服務的”清理購物車“功能需要實現冪等性。

訊息佇列實現分散式事務

事務訊息需要訊息佇列提供相應的功能才能實現,Kafka 和 RocketMQ 都提供了事務相關功能。

回到訂單和購物車這個例子,我們一起來看下如何用訊息佇列來實現分散式事務。

首先,訂單系統在訊息佇列上開啟一個事務。然後訂單系統給訊息伺服器傳送一個“半訊息”,這個半訊息不是說訊息內容不完整,它包含的內容就是完整的訊息內容,半訊息和普通訊息的唯一區別是,在事務提交之前,對於消費者來說,這個訊息是不可見的。

半訊息傳送成功後,訂單系統就可以執行本地事務了,在訂單庫中建立一條訂單記錄,並提交訂單庫的資料庫事務。然後根據本地事務的執行結果決定提交或者回滾事務訊息。如果訂單建立成功,那就提交事務訊息,購物車系統就可以消費到這條訊息繼續後續的流程。如果訂單建立失敗,那就回滾事務訊息,購物車系統就不會收到這條訊息。這樣就基本實現了“要麼都成功,要麼都失敗”的一致性要求。

如果你足夠細心,可能已經發現了,這個實現過程中,有一個問題是沒有解決的。如果在第四步提交事務訊息時失敗了怎麼辦?對於這個問題,Kafka 和 RocketMQ 給出了 2 種不同的解決方案。

Kafka 的解決方案比較簡單粗暴,直接丟擲異常,讓使用者自行處理。我們可以在業務程式碼中反覆重試提交,直到提交成功,或者刪除之前建立的訂單進行補償。RocketMQ 則給出了另外一種解決方案。

在 RocketMQ 中的事務實現中,增加了事務反查的機制來解決事務訊息提交失敗的問題。如果 Producer 也就是訂單系統,在提交或者回滾事務訊息時發生網路異常,RocketMQ 的 Broker 沒有收到提交或者回滾的請求,Broker 會定期去 Producer 上反查這個事務對應的本地事務的狀態,然後根據反查結果決定提交或者回滾這個事務。

為了支撐這個事務反查機制,我們的業務程式碼需要實現一個反查本地事務狀態的介面,告知 RocketMQ 本地事務是成功還是失敗。

在我們這個例子中,反查本地事務的邏輯也很簡單,我們只要根據訊息中的訂單 ID,在訂單庫中查詢這個訂單是否存在即可,如果訂單存在則返回成功,否則返回失敗。RocketMQ 會自動根據事務反查的結果提交或者回滾事務訊息。

這個反查本地事務的實現,並不依賴訊息的傳送方,也就是訂單服務的某個例項節點上的任何資料。這種情況下,即使是傳送事務訊息的那個訂單服務節點當機了,RocketMQ 依然可以通過其他訂單服務的節點來執行反查,確保事務的完整性。

綜合上面講的通用事務訊息的實現和 RocketMQ 的事務反查機制,使用 RocketMQ 事務訊息功能實現分散式事務的流程如下圖:

剛性事務,遵循 ACID 原則,具有強一致性。比如,資料庫事務。

柔性事務,其實就是根據不同的業務場景使用不同的方法實現最終一致性,也就是說我們可以根據業務的特性做部分取捨,容忍一定時間內的資料不一致。

總結來講,與剛性事務不同,柔性事務允許一定時間內,資料不一致,但要求最終一致。而柔性事務的最終一致性,遵循的是 BASE 理論。

eBay 公司的工程師 Dan Pritchett 曾提出了一種分散式儲存系統的設計模式——BASE 理論。 BASE 理論包括基本可用(Basically Available)、柔性狀態(Soft State)和最終一致性(Eventual Consistency)。

  • 基本可用:分散式系統出現故障的時候,允許損失一部分功能的可用性,保證核心功能可用。比如,某些電商 618 大促的時候,會對一些非核心鏈路的功能進行降級處理。
  • 在柔性事務中,允許系統存在中間狀態,且這個中間狀態不會影響系統整體可用性。比如,資料庫讀寫分離,寫庫同步到讀庫(主庫同步到從庫)會有一個延時,其實就是一種柔性狀態。
  • 最終一致性:事務在操作過程中可能會由於同步延遲等問題導致不一致,但最終狀態下,所有資料都是一致的。

BASE 理論為了支援大型分散式系統,通過犧牲強一致性,保證最終一致性,來獲得高可用性,是對 ACID 原則的弱化。ACID 與 BASE 是對一致性和可用性的權衡所產生的不同結果,但二者都保證了資料的永續性。ACID 選擇了強一致性而放棄了系統的可用性。與 ACID 原則不同的是,BASE 理論保證了系統的可用性,允許資料在一段時間內可以不一致,最終達到一致狀態即可,也即犧牲了部分的資料一致性,選擇了最終一致性。

二階段提交、三階段提交方法,遵循的是 ACID 原則,而訊息最終一致性方案遵循的就是 BASE 理論。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章