看一遍就理解:分佈式事務詳解

此次分享的緣由

支付重構

考慮支付重構的時候,自然想到原本屬於一個本地事務中的處理,現在要跨應用了要怎麼處理。拿充值訂單舉個栗子吧,假設:原本訂單模塊和賬戶模塊是放在一起的,現在需要做服務拆分,拆分成訂單服務,賬戶服務。原本收到充值回調後,可以將修改訂單狀態和增加金幣放在一個mysql事務中完成的,但是呢,因爲服務拆分了,就面臨着需要協調2個服務才能完成這個事務

所以就帶出來,我們今天要分享和討論的話題是:怎麼解決分佈式場景下數據一致性問題,暫且用 分佈式事務 來定義吧。

同樣的問題還存在於其他的場景:

送禮:

1. 調用支付服務:先扣送禮用戶的金幣,然後給主播加相應的荔枝
2. 確認第一步成功後,播放特效,發聊天室送禮評論等

充值成功消息:

1. 完成充值訂單
2. 發送訂單完成的kafka消息

目前分佈式事務是怎麼解決的呢?

購買基礎商品成功後發送支付訂單完成消息爲例:

假設支付下單購買基礎商品,此刻已經收到支付回調,訂單已經處理成功了,這個時候 kafka 服務故障,消息發送失敗;而這個時候處理訂單的事務已經提交了,怎麼保證訂單完成的消息一定能發出去呢?

解讀一下這個流程:

綠色部分,表示流程正常運行的交互過程:

  1. 先往 JobController 中提交一個 job(用於故障恢復)

  2. 提交成功後,開始處理訂單邏輯

  3. 處理完訂單邏輯之後,開始發送 kafka 消息

  4. 消息也發送成功後,刪除第一步提交的 job

黃色部分,表示流程出現了異常,數據可能存在不一致現象。這個時候就需要進行流程恢復

  1. JobController 任務控制器定時去 redis 查詢延時任務列表(每個任務都有一個時間戳,按時間戳排序過濾)

  2. 將任務進行恢復(調用 job 註冊時定義的處理方法)

  3. 任務執行成功,表示流程完成;否則下一個定時週期重試

問題:

  1. 基於 redis 存儲恢復任務,可能存在數據丟失風險

  2. 架構體系中沒有統一的分佈式事務規範,可否將這層邏輯獨立爲分佈式事務中間件

  3. 缺少事務執行策略管理,如:控制最大重試次數等

  4. 事務執行狀態沒有記錄,追查需要去翻看日誌

行業中有什麼解決方案

說解決方案之前,我們先了解一下這些方案的理論依據,有助於幫助我們來理解和實踐這些方案

理論依據(討論的前提)

本地事務、分佈式事務

如果說本地事務是解決單個數據源上的數據操作的一致性問題的話,那麼分佈式事務則是爲了解決跨越多個數據源上數據操作的一致性問題。

強一致性、弱一致性、最終一致性

從客戶端角度,多進程併發訪問時,更新過的數據在不同進程如何獲取的不同策略,決定了不同的一致性。對於關係型數據庫,要求更新過的數據能被後續的訪問都能看到,這是強一致性。如果能容忍後續的部分或者全部訪問不到,則是弱一致性。如果經過一段時間後要求能訪問到更新後的數據,則是最終一致性.

從服務端角度,如何儘快將更新後的數據分佈到整個系統,降低達到最終一致性的時間窗口,是提高系統的可用度和用戶體驗非常重要的方面。對於分佈式數據系統:

N — 數據複製的份數 W — 更新數據時需要保證寫完成的節點數 R — 讀取數據的時候需要讀取的節點數 如果W+R>N,寫的節點和讀的節點重疊,則是強一致性。例如對於典型的一主一備同步複製的關係型數據庫,N=2,W=2,R=1,則不管讀的是主庫還是備庫的數據,都是一致的。

如果W+R<=N,則是弱一致性。例如對於一主一備異步複製的關係型數據庫,N=2,W=1,R=1,則如果讀的是備庫,就可能無法讀取主庫已經更新過的數據,所以是弱一致性。

CAP 理論

分佈式環境下(數據分佈)要任何時刻保證數據一致性是不可能的,只能採取妥協的方案來保證數據最終一致性。這個也就是著名的CAP定理。

需要明確的一點是,對於一個分佈式系統而言,分區容錯性是一個最基本的要求。因爲 既然是一個分佈式系統,那麼分佈式系統中的組件必然需要被部署到不同的節點,否則也就無所謂分佈式系統了,因此必然出現子網絡。而對於分佈式系統而言,網 絡問題又是一個必定會出現的異常情況,因此分區容錯性也就成爲了一個分佈式系統必然需要面對和解決的問題。因此係統架構師往往需要把精力花在如何根據業務 特點在C(一致性)和A(可用性)之間尋求平衡。

BASE 理論

BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的縮寫。BASE理論是對CAP中一致性和可用性權衡的結果,其來源於對大規模互聯網系統分佈式實踐的總結, 是基於CAP定理逐步演化而來的。BASE理論的核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性。

BASE理論面向的是大型高可用可擴展的分佈式系統,和傳統的事物ACID特性是相反的,它完全不同於ACID的強一致性模型,而是 通過犧牲強一致性來獲得可用性,並允許數據在一段時間內是不一致的,但最終達到一致狀態。 但同時,在實際的分佈式場景中,不同業務單元和組件對數據一致性的要求是不同的,因此在具體的分佈式系統架構設計過程中,ACID特性和BASE理論往往又會結合在一起。

柔性事務

不同於ACID的剛性事務,在分佈式場景下基於BASE理論,就出現了柔性事務的概念。要想通過柔性事務來達到最終的一致性,就需要依賴於一些特性,這些特性在具體的方案中不一定都要滿足,因爲不同的方案要求不一樣;但是都不滿足的話,是不可能做柔性事務的。

可見性 (對外可查詢)

在分佈式事務執行過程中,如果某一個步驟執行出錯,就需要明確的知道其他幾個操作的處理情況,這就需要其他的服務都能夠提供查詢接口,保證可以通過查詢來判斷操作的處理情況。

爲了保證操作的可查詢,需要對於每一個服務的每一次調用都有一個全局唯一的標識,可以是業務單據號(如訂單號)、也可以是系統分配的操作流水號(如支付記錄流水號)。除此之外,操作的時間信息也要有完整的記錄。

冪等操作

冪等性,其實是一個數學概念。冪等函數,或冪等方法,是指可以使用相同參數重複執行,並能獲得相同結果的函數。

f(f(x)) = f(x)

在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。也就是說,同一個方法,使用同樣的參數,調用多次產生的業務結果與調用一次產生的業務結果相同。這一個要求其實也比較好理解,因爲要保證數據的最終一致性,很多解決防範都會有很多重試的操作,如果一個方法不保證冪等,那麼將無法被重試。冪等操作的實現方式有多種,如在系統中緩存所有的請求與處理結果、檢測到重複操作後,直接返回上一次的處理結果等

業界方案

兩階段提交(2PC)

XA是X/Open CAE Specification (Distributed Transaction Processing)模型中定義的TM(Transaction Manager)與RM(Resource Manager)之間進行通信的接口。

在XA規範中,數據庫充當RM角色,應用需要充當TM的角色,即生成全局的txId,調用XAResource接口,把多個本地事務協調爲全局統一的分佈式事務。

2PC 模型中,在 prepare 階段需要等待所有參與子事務的反饋,因此可能造成數據庫資源鎖定時間過長,不適合併發高以及子事務生命周長較長的業務場景。兩階段提交這種解決方案屬於犧牲了一部分可用性來換取的一致性。

saga

saga 的提出,最早是爲了解決可能會長時間運行的分佈式事務(long-running process)的問題。所謂 long-running 的分佈式事務,是指那些企業業務流程,需要跨應用、跨企業來完成某個事務,甚至在事務流程中還需要有手工操作的參與,這類事務的完成時間可能以分計,以小時計,甚至可能以天計。這類事務如果按照事務的 ACID 的要求去設計,勢必造成系統的可用性大大的降低。試想一個由兩臺服務器一起參與的事務,服務器 A 發起事務,服務器 B 參與事務,B 的事務需要人工參與,所以處理時間可能很長。如果按照 ACID 的原則,要保持事務的隔離性、一致性,服務器 A 中發起的事務中使用到的事務資源將會被鎖定,不允許其他應用訪問到事務過程中的中間結果,直到整個事務被提交或者回滾。這就造成事務 A 中的資源被長時間鎖定,系統的可用性將不可接受。

saga,則是一種基於補償的消息驅動的用於解決 long-running process 的一種解決方案。目標是爲了在確保系統高可用的前提下儘量確保數據的一致性。 還是上面的例子,如果用 saga 來實現,那就是這樣的流程:服務器 A 的事務先執行,如果執行順利,那麼事務 A 就先行提交;如果提交成功,那麼就開始執行事務 B,如果事務 B 也執行順利,則事務 B 也提交,整個事務就算完成。但是如果事務 B 執行失敗,那事務 B 本身需要回滾,這時因爲事務 A 已經提交,所以需要執行一個補償操作,將已經提交的事務 A 執行的操作作反操作,恢復到未執行前事務 A 的狀態。這樣的基於消息驅動的實現思路,就是 saga。我們可以看出,saga 是犧牲了數據的強一致性,僅僅實現了最終一致性,但是提高了系統整體的可用性。

補償事務(TCC)

TCC 其實就是採用的補償機制,其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。TCC 模型是把鎖的粒度完全交給業務處理。它分爲三個階段:

  1. Try 階段主要是對業務系統做檢測及資源預留

  2. Confirm 階段主要是對業務系統做確認提交,Try 階段執行成功並開始執行 Confirm 階段時,默認 Confirm 階段是不會出錯的。即:只要 Try 成功,Confirm 一定成功。

  3. Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放

下面對 TCC 模式下,A 賬戶往 B 賬戶匯款 100 元爲例子,對業務的改造進行詳細的分析:

匯款服務和收款服務分別需要實現,Try-Confirm-Cancel 接口,並在業務初始化階段將其注入到 TCC 事務管理器中。

[匯款服務]
Try:
    檢查A賬戶有效性,即查看A賬戶的狀態是否爲“轉帳中”或者“凍結”;
    檢查A賬戶餘額是否充足;
    從A賬戶中扣減100元,並將狀態置爲“轉賬中”;
    預留扣減資源,將從A往B賬戶轉賬100元這個事件存入消息或者日誌中;
Confirm:
 不做任何操作;
Cancel:
    A賬戶增加100元;
 從日誌或者消息中,釋放扣減資源。

[收款服務]
Try:
 檢查B賬戶賬戶是否有效;
Confirm:
    讀取日誌或者消息,B賬戶增加100元;
    從日誌或者消息中,釋放扣減資源;
Cancel:
 不做任何操作。

由此可以看出,TCC 模型對業務的侵入強,改造的難度大。

本地消息表(異步確保)

本地消息表這種實現方式應該是業界使用最多的,其核心思想是將分佈式事務拆分成本地事務進行處理,這種思路是來源於 ebay。我們可以從下面的流程圖中看出其中的一些細節:

基本思路就是:

消息生產方,需要額外建一個消息表,並記錄消息發送狀態。消息表和業務數據要在一個事務裏提交,也就是說他們要在一個數據庫裏面。然後消息會經過 MQ 發送到消息的消費方。如果消息發送失敗,會進行重試發送。

消息消費方,需要處理這個消息,並完成自己的業務邏輯。此時如果本地事務處理成功,表明已經處理成功了,如果處理失敗,那麼就會重試執行。如果是業務上面的失敗,可以給生產方發送一個業務補償消息,通知生產方進行回滾等操作。

生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。如果有靠譜的自動對賬補賬邏輯,這種方案還是非常實用的。

事務消息

事務消息作爲一種異步確保型事務, 將兩個事務分支通過 MQ 進行異步解耦,事務消息的設計流程同樣借鑑了兩階段提交理論,整體交互流程如下圖所示:

  1. 事務發起方首先發送 prepare 消息到 MQ。

  2. 在發送 prepare 消息成功後執行本地事務。

  3. 根據本地事務執行結果返回 commit 或者是 rollback。

  4. 如果消息是 rollback,MQ 將刪除該 prepare 消息不進行下發,如果是 commit 消息,MQ 將會把這個消息發送給 consumer 端。

  5. 如果執行本地事務過程中,執行端掛掉,或者超時,MQ 將會不停的詢問其同組的其它 producer 來獲取狀態。

  6. Consumer 端的消費成功機制有 MQ 保證。

有一些第三方的 MQ 是支持事務消息的,比如 RocketMQ,但是市面上一些主流的 MQ 都是不支持事務消息的,比如 RabbitMQ 和 Kafka 都不支持。

盡最大努力通知

最大努力通知方案主要也是藉助 MQ 消息系統來進行事務控制,這一點與可靠消息最終一致方案一樣。看來 MQ 中間件確實在一個分佈式系統架構中,扮演者重要的角色。最大努力通知方案是比較簡單的分佈式事務方案,它本質上就是通過定期校對,實現數據一致性。

最大努力通知方案的實現:

  1. 業務活動的主動方,在完成業務處理之後,向業務活動的被動方發送消息,允許消息丟失。

  2. 主動方可以設置時間階梯型通知規則,在通知失敗後按規則重複通知,直到通知 N 次後不再通知。

  3. 主動方提供校對查詢接口給被動方按需校對查詢,用於恢復丟失的業務消息。

  4. 業務活動的被動方如果正常接收了數據,就正常返回響應,並結束事務。

  5. 如果被動方沒有正常接收,根據定時策略,向業務活動主動方查詢,恢復丟失的業務消息

最大努力通知方案的特點:

  1. 用到的服務模式:可查詢操作、冪等操作。

  2. 被動方的處理結果不影響主動方的處理結果;

  3. 適用於對業務最終一致性的時間敏感度低的系統;

  4. 適合跨企業的系統間的操作,或者企業內部比較獨立的系統間的操作,比如銀行通知、商戶通知等;

方案對比

kiqSnt

別人是怎麼做的

alipay 的分佈式事務服務 DTS

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

核心概念

在 DTS 內部,我們將一個分佈式事務的關聯方,分爲發起方和參與者兩類:

發起方: 分佈式事務的發起方負責啓動分佈式事務,觸發創建相應的主事務記錄。發起方是分佈式事務的協調者,負責調用參與者的服務,並記錄相應的事務日誌,感知整個分佈式事務狀態來決定整個事務是 COMMIT 還是 ROLLBACK。

參與者: 參與者是分佈式事務中的一個原子單位,所有參與者都必須在一階段接口(Prepare)中標註(Annotation)參與者的標識,它定義了 prepare、commit、rollback 3 個基本接口,業務系統需要實現這 3 個接口,並保證其業務數據的冪等性,也必須保證 prepare 中的數據操作能夠被提交(COMMIT)或者回滾(ROLLBACK)。從存儲結構上,DTS 的事務狀態數據可以分爲主事務記錄(Activity)和分支事務記錄(Action)兩類:

這應該屬於我們上面所說的 TCC 模式。

eBay 本地消息表

本地消息表這種實現方式的思路,其實是源於 ebay,後來通過支付寶等公司的佈道,在業內廣泛使用。其基本的設計思想是將遠程分佈式事務拆分成一系列的本地事務。如果不考慮性能及設計優雅,藉助關係型數據庫中的表即可實現。

舉個經典的跨行轉賬的例子來描述。第一步,扣款 1W,通過本地事務保證了憑證消息插入到消息表中。第二步,通知對方銀行賬戶上加 1W 了。那問題來了,如何通知到對方呢?

通常採用兩種方式:

  1. 採用時效性高的 MQ,由對方訂閱消息並監聽,有消息時自動觸發事件

  2. 採用定時輪詢掃描的方式,去檢查消息表的數據。

類似使用本地消息表 + 消息通知的還有去哪兒,蘑菇街

各種第三方支付回調

最大努力通知型。如支付寶、微信的支付回調接口方式,不斷回調直至成功,或直至調用次數衰減至失敗狀態。

我們可以怎麼來做

2PC/3PC 需要資源管理器 (mysql, redis) 支持 XA 協議,且整個事務的執行期間需要鎖住事務資源,會降低性能。故先排除。

TCC 的模式,需要事務接口提供 try,confirm,cancel 三個接口,提高了編程的複雜性。需要依賴於業務方來配合提供這樣的接口。推行難度大,暫時排除。

最大努力通知型,應用於異構或者服務平臺當中

可以看到 ebay 的經典模式中,分佈式的事務,是通過本地事務 + 可靠消息,來達到事務的最終一致性的。但是出現了事務消息,就把本地事務的工作給涵蓋在事務消息當中了。所以,接下來要基於事務消息來套我們的應用場景,看起是否滿足我們對分佈式事務產品的要求。

作者:鄭鄭好 victorzheng

來源:juejin.im/post/5baa54e1f265da0ac2566fb2

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bMFXg2M1RapiIt_8AdjuRA