分佈式事務的前世今生
分佈式事務產生的原因
要搞清分佈式事務,我們先想一個問題,分佈式事務產生的原因是什麼?
如圖所示:
那麼,我們如何實現分佈式事務呢?根據分佈式系統的 CAP 理論,要麼是 CP 模式,要麼是 AP 模式。三個無法同時實現。
-
CP 是在保證數據強一致性前提下,儘量實現高可用(如過半寫入)。
-
AP 是在保證高可用的前提下,儘量實現數據一致性(如異步一致)。
在 ACID 裏,I 是最難實現的。Isolation 包含四個層級,如下表所示。在 XA 中,一般可以做到 RC 就可以了。
對於 MySQL 而言,它是通過 RC+MVCC 實現 RR 的效果。但 ACID 整體上是不靠譜的,爲啥?
因爲 ACID 只是滿足數據隔離性,但沒有做法併發控制。
分佈式事務真實使用場景
如果面向於實際使用場景,我們把上圖分佈式實現區分一下,結果如下:
也就是說,真實分佈式場景中,80% 是柔性事物,20% 是剛性事務,即使在金融行業,也是這樣。剛性事務通過 2PC 實現;柔性事物同步模式通過 Sagas 實現,異步模式通過事務消息實現。
分佈式事務 - 剛性事務 - 2PC 的實現原理
我們先看一下剛性事務的最終實現:2PC。如下圖所示。在下圖中,RM 管理共享資源,如 DB。TM 負責管理全局事務,如分配事務唯一標識、監控事務的執行進度、並負責事務的提交、回滾、失敗、恢復等。
2PC 的缺點在於:它是同步阻塞模型、數據庫鎖定時間過長、全局鎖(隔離級別串行化)併發低、不適合長事務場景(RM 特別多的情況)。
3PC 沒有安全解決 2PC 的問題,但又引入了新的問題。由於實際場景幾乎沒人使用,因此我不做介紹。
分佈式事務 - 柔性事務
在介紹了剛性事務 2PC 後,接下來我們介紹柔性事務。柔性事物的理念是 BASE。
BASE 理論指的是:
-
Basically Available(基本可用)
-
Soft state(柔性狀態 ---> 中間狀態:業務中間態、數據中間態)
-
Eventually consistent(最終一致性)
我們舉一個數據中間態的例子。一個轉賬業務,我們給轉賬業務數據分成兩部分:可用金額要和凍結金額(並不是說,轉賬業務一定要按照下面模式設計,只是當對一致性要求比較高的時候,用下面的模式)。
分佈式事務 - 柔性事務 - 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 + 發消息。因此事務消息對業務非常友好。
具體實現如下圖所示:
接下來,我們就上圖步驟進行分析:
-
MQ Producer(是業務邏輯)發送半消息給 MQ Server。半消息的特點是:不會被消費,先存在 MQ 裏。
-
MQ Server 告訴 MQ Producer,半消息發送成功(這個通知是同步交付)。
-
訪問數據庫、執行本地事務。然後 MQ Server 把消息投遞給 MQ Subscriber。
-
MQ Producer 告訴 MQ Server,投遞還是不投遞消息。例如告訴 MQ Server 投遞,那麼 MQ Server 把半消息變成確認消息,進行消息投遞。
-
MQ Server 沒有收到步驟 4 的確認,就回查 MQ Producer,看消息是否需要投遞。
-
MQ Producer 查數據庫,看此前本地事務是否提交、是否成功。
-
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 消息主題。他們之間的差,就是需要回查的消息。如下圖所示邏輯。
我們根據上圖將半消息進行分析:
-
生產者發送事務消息,寫到 CommitLog。Dispatcher 將其索引放入到 RMQ_SYS_TRANS_HALF_TOPIC
-
消息生產者發送 OP 消息,這個消息先寫入到 CommitLog,然後被 Dispatch 到 RMQ_SYS_TRANS_OP_HALF_TOPIC。
-
如果 OP 消息是 Commit,MQ 會將對這個半消息在 CommitLog 進行重構,然後再有 Dispatcher 重新投遞到消費隊列中。
-
MQ 對比 RMQ_SYS_TRANS_OP_HALF_TOPIC 和 RMQ_SYS_TRANS_HALF_TOPIC 兩個隊列,在後者超時的消息,到前者進行回查。到消息生產者回查事務狀態(DB)。
-
然後消息生產者再度發送 OP 消息(針對回查結果 Commit 還是 Rollback)
-
重新投遞到消費隊列中的消息被消費者消費。
事務消息需要業務方提供回查接口,對業務侵入較大。
在上面的方案中,所有事務一致性的保證,都由 RockerMQ 完成,就必然會有回查,業務就需要提供回查接口。
使用 RocketMQ 優點是方案通用,缺點是:需要業務代碼實現消息回查,增加開發的工作量;發送消息冪等;消費端需要處理冪等。
因此,方案 2 是使用本地消息事務表。
分佈式事務 - BASE - 事務消息的實現 - 事務消息表
我們可以通過客戶端來保證事務的一致性。也就是說,在本地 DB 中放一個消息表,通過本地事務管理器維護事務消息表(掃描發送、清理)。
但是,上圖只是實現了分佈式事務的異步方式(有 MQ),沒有同步方式。接下來,我們看同時實現分佈式事務同步和異步的模型。
柔性分佈式事務的終極實現:Sagas + 事務消息表
關於這種方式的實現,我們舉例子說明。這是分佈式事務的終極模型,也就是說,既能實現同步分佈式事務(Saga),也能實現異步分佈式事務(事務消息表)。
先看一張圖:
我們結合京東購物場景看這張圖:
-
在京東買大魏的書,放在購物車裏,然後到支付界面。有哪位書太貴,猶豫了,沒再操作。這時候,業務系統做的事情是:訂單數據服務在兩個 DB 表裏寫入信息,一個是 orders 表,同一個是 mqMsgs,也就是消息表。表中存放的,就是支付消息。這兩個表的寫操作,在同一個數據庫鏈接裏完成。因此這兩個表的寫操作,就是一個本地事務。
-
訂單業務邏輯服務從 DB 消息表中讀取消息,寫入到 MQ 中,這是通過起定時任務實現的(它是個子線程),業務邏輯層有 MQClient。這個定時任務就是從 DB 表讀信息,投遞到 MQ 上。
-
MQ Server 給訂單邏輯服務返回 ACK。
-
MQ 給訂單邏輯服務發 ACK
-
訂單業務邏輯調用訂單數據訪問服務,發起刪除消息記錄。
-
訂單數據服務操作 DB 中的消息表,刪除表中的數據。===> 消息投遞成功,就刪除 DB 消息表中的記錄。
-
訂單業務邏輯從 MQ 中讀取訂單到期未支付消息(京東是 24 小時)。
-
訂單業務邏輯服務調用訂單數據服務,刪除訂單記錄。
-
訂單數據服務操作數據庫表,刪除數據庫中 orders 表中的訂單記錄。
總結來說,上面的路子是:將向 MQ 發送消息的操作,通過 AOP 篡改爲寫 DB,然後再通過業務邏輯層的定時任務定期從 DB 消息表中讀消息並且放到 MQ。
柔性分佈式事務的終極實現:Sagas + 事務消息表:架構分析
我們再舉一個京東網購退貨的例子:
在上圖中,TDB 會有兩個表。也就是說,事務補償能夠成功實現,主要靠 TDB 中的這兩張表!:
-
事務狀態表(記錄事務組狀態;txid、state、timestap)、
-
事務調用組表(記錄事務組中每一次調用和相關參數;txid、actionid、callmethod、pramatype、params)。
接下來,我們介紹上面兩個表每個字段的含義: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