分佈式事務的前世今生

分佈式事務產生的原因

要搞清分佈式事務,我們先想一個問題,分佈式事務產生的原因是什麼?

如圖所示:

那麼,我們如何實現分佈式事務呢?根據分佈式系統的 CAP 理論,要麼是 CP 模式,要麼是 AP 模式。三個無法同時實現。

在 ACID 裏,I 是最難實現的。Isolation 包含四個層級,如下表所示。在 XA 中,一般可以做到 RC 就可以了。

LFBqwB

對於 MySQL 而言,它是通過 RC+MVCC 實現 RR 的效果。但 ACID 整體上是不靠譜的,爲啥?

因爲 ACID 只是滿足數據隔離性,但沒有做法併發控制。

分佈式事務真實使用場景

如果面向於實際使用場景,我們把上圖分佈式實現區分一下,結果如下:

也就是說,真實分佈式場景中,80% 是柔性事物,20% 是剛性事務,即使在金融行業,也是這樣。剛性事務通過 2PC 實現;柔性事物同步模式通過 Sagas 實現,異步模式通過事務消息實現。

分佈式事務 - 剛性事務 - 2PC 的實現原理

我們先看一下剛性事務的最終實現:2PC。如下圖所示。在下圖中,RM 管理共享資源,如 DB。TM 負責管理全局事務,如分配事務唯一標識、監控事務的執行進度、並負責事務的提交、回滾、失敗、恢復等。


2PC 的缺點在於:它是同步阻塞模型、數據庫鎖定時間過長、全局鎖(隔離級別串行化)併發低、不適合長事務場景(RM 特別多的情況)。

3PC 沒有安全解決 2PC 的問題,但又引入了新的問題。由於實際場景幾乎沒人使用,因此我不做介紹。

分佈式事務 - 柔性事務

在介紹了剛性事務 2PC 後,接下來我們介紹柔性事務。柔性事物的理念是 BASE。

BASE 理論指的是:

我們舉一個數據中間態的例子。一個轉賬業務,我們給轉賬業務數據分成兩部分:可用金額要和凍結金額(並不是說,轉賬業務一定要按照下面模式設計,只是當對一致性要求比較高的時候,用下面的模式)。

分佈式事務 - 柔性事務 - BASE-Saga

接下來,我們看柔性事物的實現,先看 Saga。

Saga 本質是將一個分佈式事務分爲多個本地事務。每個本地事務只有執行和補償。我們拿銀行業餘來說,有轉賬業務,轉賬業務的補償事務就是:轉賬衝正,如下圖所示。如果轉賬失敗,就調用轉賬衝正進行事務補償。

Saga 的恢復模式分爲:向後恢復(逆向補償)和向前恢復(重試失敗的事務)。

Sagas

業務邏輯層:基於 AOP 實現 Proxy。@around

邏輯事務層增加註解,開啓全局事務。

數據訪問層:基於原則接口方法,在方法名加註釋補償方法名。@compensable(cancelMethod...)

Saga 的開源實現:

ServiceComb(需要使用整套微服務,才能使用其中的 Saga)

Seata,阿里開源的。SeataAT 是 Saga 的優雅實現(使用狀態機實現。上面的案例使用 AOP 實現)

在隔離性方面,Sagas 隔離是通過業務中間態實現的。例如金融系統的中間賬戶。

例如郭德綱向大魏轉賬,真實模式是:

郭德綱向中間賬戶轉賬失敗,那麼中間賬戶會刪除記錄。如果一段時間沒刪除,例如 5s,那麼中間賬戶就會想大魏轉賬。

前面提到的轉賬和衝正是互爲逆方法。同樣,CRUD 中,insert 和 delete 也是互爲逆方法。因此我們需要爲 update 操作提供逆方法。

那麼,如何爲 update 提供優雅的事務補償呢?使用 Seata AT。Seata AT 就是在 Sagas 的基礎上實現了自動補償。目前自動補償的方案都是往這個方向努力,看誰支持的 sql 語法更多,支持的 db 類型更多。

Seata AT 實現的方法如下圖:

剛性事務與柔性事務的對比

經過上面內容的介紹,我們將分佈式事務分類圖進行進一步細化。

分佈式事務的分類如下:

我們首先對剛性事務和柔性事務進行對比如下。

在柔性事物中,在柔性事物中,當業務失敗還沒來得及補償時,是容易出現髒讀的。隔離性支持到 RU 就可以。

分佈式事務 - BASE - 事務消息的實現 - 半消息

大家不要認爲金融行業的分佈式事務一定是強一致的,實際情況中,強一致的比率不是特別高。例如螞蟻金服 2PC 也沒有大於 20%。例如轉賬,用 Saga、2PC 都成。

那麼,我們我們看一下事務消息的實現原理。

事務消息解決的是什麼問題?我們先看一個常見的場景。

一個分佈式事務由兩個本地事務組成。兩個事務之間有個 MQ。A 事務執行成功後(1),向 MQ 發送消息(2)。然後 B 事務消費消息(3)並執行本地事務(4)。

那麼,事務消息解決的是什麼問題呢?它解決的步驟 1 和步驟 2 的原子性問題。也即是說,把事務 A 寫數據庫和往 MQ 發消息這兩件事,捏成一個原子事務。兩件事要麼一起成功、要麼一起失敗(下圖籃圈)。

而分佈式事務保證的是什麼?是步驟 1、2、3、4 這四件事的原子性。也就是說,這四件事要麼一起成功、要麼一起失敗(下圖黃圈)。

所以說:事務消息不是分佈式事務。但它大大簡化了事務分佈式模型。它將兩次 RPC 調用(一次分佈式事務至少兩次 RPC 調用)簡化成 RPC + 發消息。因此事務消息對業務非常友好。

具體實現如下圖所示:

接下來,我們就上圖步驟進行分析:

  1. MQ Producer(是業務邏輯)發送半消息給 MQ Server。半消息的特點是:不會被消費,先存在 MQ 裏。

  2. MQ Server 告訴 MQ Producer,半消息發送成功(這個通知是同步交付)。

  3. 訪問數據庫、執行本地事務。然後 MQ Server 把消息投遞給 MQ Subscriber。

  4. MQ Producer 告訴 MQ Server,投遞還是不投遞消息。例如告訴 MQ Server 投遞,那麼 MQ Server 把半消息變成確認消息,進行消息投遞。

  5. MQ Server 沒有收到步驟 4 的確認,就回查 MQ Producer,看消息是否需要投遞。

  6. MQ Producer 查數據庫,看此前本地事務是否提交、是否成功。

  7. MQ Producer 根據在數據庫的查詢結果,告訴 MQ Server 提交還是 Rollback。然後 MQ Server 決定是丟棄還是投遞消息。如果步驟 1 中,MQ  Producer 發送半消息後,MQ  Producer 掛了或者因爲一些原因本地事務執行失敗,那麼步驟 5-6 回查,發現事務未成功,就會把半消息從 MQ Server 中刪除。

京東購物 24 小時未支付訂單自動取消的截圖:

分佈式事務 - BASE - 事務消息的實現 - 半消息 - RocketMQ 的實現

關於 RocketMQ 通過半消息實現事務消息原理,我們可以看得更細緻些。

我們知道,在普通的 MQ 中,消息生產者將消息寫入到 CommitLog,然後 Dispatcher 將消息的索引信息放到 Topic 中以便消費者消費。那麼,RocketMQ 如何實現半消息?也就是消息放到 Topic 中不被消費?

除了半消息外,還有個 OP 消息主題 RMQ_SYS_TRANS_OP_HALF_TOPIC。這個主題記錄二階段操作。OP 消息包含如下兩種:Rollback(只做記錄)和 Commit(根據備份信息重新構造消息並投遞)。如下圖所示其位置:

回查:對比 HALF 消息和 OP 消息主題。他們之間的差,就是需要回查的消息。如下圖所示邏輯。

我們根據上圖將半消息進行分析:

  1. 生產者發送事務消息,寫到 CommitLog。Dispatcher 將其索引放入到 RMQ_SYS_TRANS_HALF_TOPIC

  2. 消息生產者發送 OP 消息,這個消息先寫入到 CommitLog,然後被 Dispatch 到 RMQ_SYS_TRANS_OP_HALF_TOPIC。

  3. 如果 OP 消息是 Commit,MQ 會將對這個半消息在 CommitLog 進行重構,然後再有 Dispatcher 重新投遞到消費隊列中。

  4. MQ 對比 RMQ_SYS_TRANS_OP_HALF_TOPIC 和 RMQ_SYS_TRANS_HALF_TOPIC 兩個隊列,在後者超時的消息,到前者進行回查。到消息生產者回查事務狀態(DB)。

  5. 然後消息生產者再度發送 OP 消息(針對回查結果 Commit 還是 Rollback)

  6. 重新投遞到消費隊列中的消息被消費者消費。

事務消息需要業務方提供回查接口,對業務侵入較大。

在上面的方案中,所有事務一致性的保證,都由 RockerMQ 完成,就必然會有回查,業務就需要提供回查接口。

使用 RocketMQ 優點是方案通用,缺點是:需要業務代碼實現消息回查,增加開發的工作量;發送消息冪等;消費端需要處理冪等。

因此,方案 2 是使用本地消息事務表。

分佈式事務 - BASE - 事務消息的實現 - 事務消息表

我們可以通過客戶端來保證事務的一致性。也就是說,在本地 DB 中放一個消息表,通過本地事務管理器維護事務消息表(掃描發送、清理)。

但是,上圖只是實現了分佈式事務的異步方式(有 MQ),沒有同步方式。接下來,我們看同時實現分佈式事務同步和異步的模型。

柔性分佈式事務的終極實現:Sagas + 事務消息表

關於這種方式的實現,我們舉例子說明。這是分佈式事務的終極模型,也就是說,既能實現同步分佈式事務(Saga),也能實現異步分佈式事務(事務消息表)。

先看一張圖:

我們結合京東購物場景看這張圖:

  1. 在京東買大魏的書,放在購物車裏,然後到支付界面。有哪位書太貴,猶豫了,沒再操作。這時候,業務系統做的事情是:訂單數據服務在兩個 DB 表裏寫入信息,一個是 orders 表,同一個是 mqMsgs,也就是消息表。表中存放的,就是支付消息。這兩個表的寫操作,在同一個數據庫鏈接裏完成。因此這兩個表的寫操作,就是一個本地事務。

  2. 訂單業務邏輯服務從 DB 消息表中讀取消息,寫入到 MQ 中,這是通過起定時任務實現的(它是個子線程),業務邏輯層有 MQClient。這個定時任務就是從 DB 表讀信息,投遞到 MQ 上。

  3. MQ Server 給訂單邏輯服務返回 ACK。

  4. MQ 給訂單邏輯服務發 ACK

  5. 訂單業務邏輯調用訂單數據訪問服務,發起刪除消息記錄。

  6. 訂單數據服務操作 DB 中的消息表,刪除表中的數據。===> 消息投遞成功,就刪除 DB 消息表中的記錄。

  7. 訂單業務邏輯從 MQ 中讀取訂單到期未支付消息(京東是 24 小時)。

  8. 訂單業務邏輯服務調用訂單數據服務,刪除訂單記錄。

  9. 訂單數據服務操作數據庫表,刪除數據庫中 orders 表中的訂單記錄。

總結來說,上面的路子是:將向 MQ 發送消息的操作,通過 AOP 篡改爲寫 DB,然後再通過業務邏輯層的定時任務定期從 DB 消息表中讀消息並且放到 MQ。

柔性分佈式事務的終極實現:Sagas + 事務消息表:架構分析

我們再舉一個京東網購退貨的例子:

在上圖中,TDB 會有兩個表。也就是說,事務補償能夠成功實現,主要靠 TDB 中的這兩張表!:

接下來,我們介紹上面兩個表每個字段的含義:txid 是主鍵、state 表示事務狀態(例如:1 開始 2 成功  3 失敗  4 補償成功 )、actionid 是操作次數的 id、callmethod 是補償接口、調用服務的類型(如 web/RPC)、params 是具體數據包的調度參數。

而事務補償,就是當事務調用失敗,事務攔截器修改事務組狀態(state)。然後由 TM 發起,分佈式事務補償服務異步執行補償。

上面的內容比較抽象,我們結合場景進行說明,還是以下圖爲例。

大魏在京東購物,這是一個分佈式事務。這個時候,proxy 生成事務組表的一行數據,並且記錄(t1 代表分佈式事務 1):

接下來,要正式幹活兒了。Proxy 記錄事務 A 的調用信息,寫入事務組表:

記錄完畢後,A 做本地事務:

A 執行成功後,Proxy 用類似的方式管理 B,以此類推 C。

Proxy 在事務調用表中記錄 B 的調用內容:

B 本地事務執行:

Proxy 在事務調用表中記錄 C 的調用內容:

然後 C 執行。

當 C 本地事務執行成功後,Proxy 將會修改 TDB 中的信息:

修改前:

修改後:

也就是說,標記分佈式事務執行成功。

但是,如果在上面的步驟中,C 執行失敗呢?

首先,Proxy 會將 TDB 中的事務組表進行如下修改,將狀態從 1 開始修改爲 3 失敗:

修改前:

修改後:

接下來,Schedule(即 TM)會掃描到(定期掃描)t1 失敗(狀態爲 3)。然後 Schedule 發現 T1 在事務調用表有 2 個關聯的本地事務(C 做失敗已經自動 rollback)

接下來,根據事務調用表中的 pramatype 字段,RPC Client 調用補償接口,對事務進行補償。

先補償 B:

再補償 A:

最後,補償完畢後,proxy 修改 TBD 中的事務組表,將 state 從 3 改成 4,代表補償成功。

柔性分佈式事務的終極實現:Sagas + 事務消息表:案例分析

我們上述邏輯,結合業務邏輯組件進行結合。這次我們把業務邏輯模塊換一下。京東購物,選中貨物後,庫存會被鎖住(A)、然後支付的時候,會選擇紅包或者京東卡,會有減紅包或減京東卡餘額的操作(C)、最後成功創建訂單(C)。

具體參考下圖:

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