分散式事務的理解和常見解決方案彙總

帶你聊技術發表於2023-03-14


分散式事務的理解和常見解決方案彙總

分散式事務的理解

分散式事務簡介

在網際網路系統裡面,一般都是犧牲強一致性換取系統的高效能和高可用,只需要保證資料的最終一致性,只不過達到最終一致性所需要的時間需要在使用者和產品層面是可以接受的。而金融系統則不同,金融系統一般都需要保持強一致性。

那麼,在我們網際網路場景中,怎麼保證我們的資料最終一致性呢?一般來說,我們會採用分散式事務、分散式鎖等,分散式事務與資料庫事務一樣,同樣需要具有事務最基本的 ACID(原子性、一致性、隔離性、永續性)四個屬性,事務的 ACID 特性,實現的是系統狀態的一致性。一般在支付,或其他需要原子操作的場景下比較常用。

我們怎麼理解分散式事務呢?我們知道,現在的系統,一般都是微服務架構,即便不是微服務架構,那麼一個系統也會有多個服務來組成。那麼業務上的一個完整流程,很有可能就需要分別呼叫散落在各個節點的各個不同服務上的介面,這個時候,如果我們想要保證這個完整流程的資料一致性,那麼就需要保證請求在各個節點的各個服務中的請求,要麼都成功,要麼都失敗。如果採用強一致性方案,基本上就靠“兩階段提交”這樣的方式;如果採用弱一致性方案(最終一致性方案),那麼可選擇方案就會比較多,本文會詳細梳理各個方案的優缺點。

分散式事務 vs 一致性協議(分散式一致性協議)

我們講分散式事務的時候,總是會提及到分散式一致性協議,那麼他們是否一回事呢?答案是否定的。我們細細分析下。

分散式一致性協議針對的是多個節點重複做相同的一件事情,主要處理的是多副本之間的一致性,更像是共識演演算法,比如 Paxos 演演算法、ZAB 協議、Raft 演演算法。分散式一致性協議要處理的邏輯是:

節點1完成任務1
節點2完成任務1
節點3完成任務1
每個節點都要完成同一個任務並且保證節點之間的處理狀態完全一致

而分散式事務則針對的是有幾個不同的任務或者流程,他們要捆綁一起成功或失敗,要麼都成功、要麼都失敗,要保證多個流程的一致性,針對的整體且完整流程的原子性。而 Raft 等共識演演算法、 TCC(Try-Confirm-Cancel)、Gossip 協議等則可以實現分散式事務的一致性。分散式事務要處理的邏輯是:

節點1完成任務1
節點2完成任務2
節點3完成任務3
每個節點完成不同任務並且保證同時成功或者同時失敗

分散式事務 vs 資料庫事務

資料庫事務相對來說,會非常容易實現,資料庫系統會將跨表事務的問題收攏到系統內部來處理,然後系統內部基於 XA 或其他協議,結合 MVCC 事務鎖之類的機制,就可以解決好這個問題。

但是對在業務層面的分散式事務而言,我們前面說到,它涉及到的是散落在各個節點的各個服務之間的一致性,每個服務節點的資料儲存機制和方案都是不固定的,因此就無法採用資料庫事務的解決方案。比如如下一個分散式事務的場景:

分散式事務開啟
  任務流程1:修改資料 A // 可能是 DB 儲存
  任務流程2:修改資料 B // 可能是 nosql 儲存
  任務流程3:呼叫系統內服務介面 C // -- 被調服務可能是類似情況
  任務流程4:呼叫外部門服務介面 D// -- 被調服務可能是類似情況
分散式事務結束

從上面這個流程可以看到,我們無法統一收歸下游其他服務已經內部不同儲存系統之間的事務。因此,針對業務層面的分散式事務的解決方案,一般的做法就是加一層或多層抽象:

  • • 增加外在的事務儲存(主 key 為事務 ID,記錄事務的參與方、進度、子事務等資訊)。

  • • 要求事務參與方遵循某些約束(如本地事務 / 對賬能力),呼叫特定的一些 API 將事務資訊進行上報關聯。

  • • 引入事務協調者,由它來根據參與方上報的資訊做一些事務的驅動邏輯。

分散式事務的關鍵點

參考現有的各種框架級別的解決方案,分散式事務的關鍵點主要包括:

  • • 建立事務一定需要一個唯一主 key ,一般就是事務 ID ,然後基於這個唯一事務 ID ,關聯一些事務資訊的儲存和各個子任務。

  • • 需要有一個協調者來負責跟蹤推進完整個事務,然後各參與者需要遵從一定的規範約束,基於事務 ID ,以冪等、對賬能力為基礎,實現相應的 API。

  • • 一致性要求高的場景,會有對資源做鎖定或預留的做法,最終一致性要求的場景,則只要最終符合預期即可。基於對資源要求的不同,會有一些常見的解決方案,例如多階段協商提交、TCC、事務訊息等。

分散式事務的常見解決方案

分散式事務的常見解決方案主要分為強一致性的解決方案 和 最終一致性的解決方案

強一致性的解決方案

XA 分散式事務協議

XA 分散式事務協議,是由 Oracle Tuxedo 系統提出的,XA中大致分為兩部分:事務管理器和本地資源管理器。其中本地資源管理器往往由資料庫實現,比如Oracle、DB2這些商業資料庫都實現了XA介面,而事務管理器作為全域性的排程者,負責各個本地資源的提交和回滾。XA 協議通常包含兩階段提交(2PC)和三階段提交(3PC)兩種實現。XA 併發效能不太好,無法滿足高併發場景,在網際網路中使用較少。

2PC 兩階段提交

兩階段提交(2PC,Two-phase Commit Protocol)是非常經典的強一致性、中心化的原子提交協議,在各種事務和一致性的解決方案中,都能看到兩階段提交的應用。

3PC 三階段提交

三階段提交協議(3PC,Three-phase_commit_protocol)是在 2PC 之上擴充套件的提交協議,主要是為瞭解決兩階段提交協議的阻塞問題,從原來的兩個階段擴充套件為三個階段,增加了超時機制。

DTS 方案

阿里有一個分散式事務服務 (Distributed Transaction Service, DTS), 是一個分散式事務框架,用來保障在大規模分散式環境下事務的最終一致性。DTS 從架構上分為 xts-client 和 xts-server 兩部分,前者是一個嵌入客戶端應用的 JAR 包,主要負責事務資料的寫入和處理;後者是一個獨立的系統,主要負責異常事務的恢復。

最終一致性的解決方案

TCC 分段提交【較常用】

實現分散式事務,最常用的方法是二階段提交協議和 TCC,這兩個演演算法的適用場景是不同的,二階段提交協議實現的是資料層面的事務,比如 XA 規範採用的就是二階段提交協議;TCC 實現的是業務層面的事務,比如當操作不僅僅是資料庫操作,還涉及其他業務系統的訪問操作時,這時就應該考慮 TCC 了。

TCC 是一個分散式事務的處理模型,將事務過程拆分為 Try、Confirm、Cancel 三個步驟,在保證強一致性的同時,最大限度提高系統的可伸縮性與可用性,又被稱補償事務。它的核心思想是針對每個操作都要註冊一個與其對應的確認操作和補償操作(也就是撤銷操作)

TCC 實現的是業務層面的事務,TCC 可以理解為是一個業務層面的協議,可以當做為一個程式設計模型來看待,因此這個的應用還是非常廣泛的。,TCC 的 3 個操作是需要在業務程式碼中編碼實現的,為了實現一致性,確認操作和補償操作必須是等冪的,因為這 2 個操作可能會失敗重試。

TCC 不依賴於資料庫的事務,而是在業務中實現了分散式事務,這樣能減輕資料庫的壓力,但對業務程式碼的入侵性也更強,實現的複雜度也更高。

MQ(非事務訊息)【較常用】

採用非事務訊息的這種方式比較常見,一個是由於市面上很多這種成熟的非事務訊息的解決方案,一個是由於這些 MQ 的效能和吞吐量都比較好,可以滿足大部分的業務場景。

一個典型流程基本上就是,生產者先執行本地事務並將訊息落庫,狀態標記為待傳送,然後傳送訊息。如果傳送成功,則將訊息改為傳送成功;如果傳送失敗則不修改標記。然後會起一個定時任務,定時從資料庫撈取在一定時間內待傳送的訊息並將訊息傳送。為確保訊息一定能消費,消費者一般採用手動 ACK 機制,並且最好需要支援冪等。

在消費者端,我們可能面臨的問題和解決方案是:

  1. 1. 消費者消費到訊息後,消費者要保證對應的業務操作要執行成功之後再才能主動 ACK。如果業務執行失敗,訊息不能失效或者丟失。目前主流的 MQ 產品都具有持久化訊息的功能,如果消費者當機或者消費失敗,都可以執行重試機制的,因此這個比較好解決。

  2. 2. 消費者消費訊息要能夠在業務層面保持冪等,因為消費可能會失敗,因此只有具有冪等性,才能不影響業務。具體的方案,在業務層可以採用唯一主鍵來解決,也可以採用其他的日誌或庫表(去重表)來保證。

在生產者端,面臨的問題是執行本地事務和傳送訊息是兩個非同步的操作,他們並不能保證強一致性,因此有一定的機率會出現一些 bug。

MQ(事務訊息)【沒有開源方案】

在分散式事務實踐中事務性訊息也是比較常使用的,所謂的訊息事務就是基於訊息佇列的兩階段提交,本質上是對訊息佇列的一種特殊利用,它是將本地事務和發訊息放在了一個分散式事務裡,本地事務和傳送訊息這兩個步驟是保持了強一致性的,保證要麼本地操作成功成功並且對外發訊息成功,要麼兩者都失敗,這種訊息就是事務性訊息。和不支援事務的訊息中間相比,只是訊息傳送的時候,保證了和本地事務的一致。消費者實現還是不變。

透過 事務訊息來實現的話,整體的可靠性會比較高,阿里的 RocketMQ 就是屬於事務訊息。RocketMQ 的事務訊息的設計思路是:RocketMQ 第一階段傳送 Prepared 訊息時,會拿到訊息的地址,第二階段執行本地事物,第三階段透過第一階段拿到的地址去訪問訊息,並修改狀態。然後需要定期掃描訊息叢集中的事物訊息,這時候發現了 Prepared 訊息,它會向訊息傳送者確認,RocketMQ 會根據傳送端設定的策略來決定是回滾還是繼續傳送確認訊息。這樣就保證了訊息傳送與本地事務同時成功或同時失敗。RocketMQ 中的事務,它解決的問題是,確保執行本地事務和發訊息這兩個操作,要麼都成功,要麼都失敗。並且 RocketMQ 增加了一個事務反查的機制,來儘量提高事務執行的成功率和資料一致性。

目前一些主流的開源訊息佇列比如 ActiveMQ、RabbitMQ、Kafka 等都沒有實現對事務訊息的支援,但是可以有類似的實現方式。比如,Kafka 中的事務,它解決的問題是,確保在一個事務中傳送的多條訊息,要麼都成功,要麼都失敗。

SAGA 長流程分散式事務【較常用】

SAGA 用於處理有序的一長串的長流程的事務,相對來說,效能更好,無資源鎖定,無流程阻塞,但是不保證事務間的隔離性與原子性,需要業務側根據需要處理可能的問題。

SAGA 的每個子事務都有一個補償介面,如果執行到某個階段失敗後,則對已經成功的子事務按棧順序依次進行補償操作(補償不允許失敗,失敗必須重試直到成功),SAGA 的一階段為 Do,二階段是 Undo,每個 Do 都是一個完整的事務,但整個流程並不能保證隔離性與原子性。

業務補償方式:重試(or 回滾)+告警+人工修復【較常用】

另外一個實現弱一致性的比較簡單粗暴的方式,就是採用業務補償方式,透過重試 + 回滾 (自動或手動)的方式,當出現不一致的時候,進行重試,重試還是失敗後就進行回滾,或者告警後人工修復。

補償事務的缺點在於不同的業務要寫不同的補償事務,不具備通用性;並且如果業務流程很複雜,if/else會巢狀非常多層;同時也沒有考慮補償事務失敗的後續流程。總之是一個比較糙的解決方案。在對一致性要求不高的情況下,如果又想要比較簡單的實現,可以採用這種業務補償方式。業務補償設計的主要核心點在於:

  • • 將服務做成冪等性的,如果一個事務失敗了或是超時了,我們需要不斷地重試,努力地達到最終我們想要的狀態。

  • • 如果執行不下去,需要啟動補償機制,回滾業務流程。

一個業務補償的虛擬碼示例如下:

// 執行第一個事務
int flag = Do_AccountT();
if(flag=YES){
    //第一個事務成功,則執行第二個事務
    flag= Do_OrderT();
    if(flag=YES){
        // 第二個事務成功,則成功
        return YES;
    }
    else{
        // 第二個事務失敗,執行第一個事務的補償事務
        Compensate_AccountT();
    }
}

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2939557/,如需轉載,請註明出處,否則將追究法律責任。

相關文章