分佈式事務的幾種解決方案

作者:叫我田露也行
鏈接:https://www.jianshu.com/p/3772ba87378a

在單體應用中,在同一數據源上更新數據的一致性由數據庫本身的事務特性來保證

如圖一個訂單業務,如果中間某一操作失敗,依賴數據庫的事務可以保證數據的一致性。然而隨着業務需求和架構的變化,單體應用被拆分爲微服務,原來在一個應用中完成的業務被拆分到多個應用

上圖中 3 個 DB 操作本應爲一個事務,每一個服務內部的數據一致性仍由本地事務來保證。而整個業務層面的全局數據一致性要如何保障呢,這就是分佈式事務要解決的問題。分佈式事務是涉及倆個以上網絡主機的數據庫事務,和單機數據庫事務一樣,需要具有 ACID(原子性、一致性、隔離性、持久性) 屬性,其中原子性保證一系列操作的結果是全生效或全失效。

幾種解決方案

CAP 原理是現代分佈式系統的理論基石

C:Consistency 一致性,每次讀取都是最新的數據

A:Availability 可用性,保證每次請求都會返回數據,即使該數據並不是最新的。

P:Partition tolerance 分區容錯性,節點之間網絡發生故障,保證系統可用。

CAP 原理:必須在可用性與一致性之間做出選擇,不能二者同時滿足。

當選擇了一致性,那麼當集羣之間的節點數據未同步完成,就不能給予請求響應,必須等到數據同步完成。當選擇了可用性,數據未同步完成時也能給予請求響應,只是該數據有可能是已經被修改了。CA 不能同時兼得

BASE 理論:全稱:Basically Available(基本可用),Soft state(軟狀態), 和 Eventually consistent(最終一致性),BASE 是對 CAP 中一致性和可用性權衡的結果,核心思想是無法做到強一致性(Strong consistency),但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。軟狀態指的是:允許系統中的數據存在中間狀態,並認爲該狀態不影響系統的整體可用性,即允許系統在多個不同節點的數據副本存在數據延時。

1. XA

最早的分佈式事務模型是 X/Open 國際聯盟提出的 X/Open Distributed Transaction Processing(DTP)模型,簡稱 XA 協議。

其中包含一個全局事務管理器(TM,Transaction Manager)和多個資源管理器(RM,Resource Manager)。全局事務管理器負責管理全局事務狀態與參與的資源,協同資源一起提交或回滾;資源管理器則負責具體的資源操作。XA 協議描述了 TM 與 RM 之間的接口,允許多個資源在同一分佈式事務中訪問。XA 使用兩階段提交來保證所有資源同時提交或回滾任何特定的事務。階段一爲準備(prepare)階段。即所有的 RM 鎖住需要的資源,在本地執行這個事務(執行 sql,寫 redo/undo log 等),但不提交,然後向 Transaction Manager 報告已準備就緒。階段二爲提交階段(commit)。當 Transaction Manager 確認所有參與者都 ready 後,向所有參與者發送 commit 命令

關於二階段提交

關於 2PC 與 3PC 這篇博文 2PC 與 3PC 總結的很好,詳細的介紹可以看下這個,這裏只做簡單介紹。

二階段提交即 2PC,當一個事務跨越多個節點時,爲了保持事務的 ACID 特性,需要引入一個作爲協調者的組件來統一控制所有節點(參與者)的操作結果並最終指示這些節點是否要把操作結果進行真正的提交。它將事務的提交過程分爲兩個階段來進行處理:準備階段和提交階段。
準備階段:協調者詢問參與者是否準備好提交,準備階段並不會釋放鎖資源
提交階段:協調者首先持久化本地事務,再發送 commit 請求,參與者若未回覆或回覆失敗則發送 rollback 指令
3 個缺點:協調者單點問題(參與者等待協調者指令時無法做超時處理),性能問題,一致性問題(例如協調者 commit 之後斷連)

三階段提交:將準備階段進行拆分,分爲 canCommit 和 preCommit,相當於只是增加了一輪詢問操作。在事務需要回滾的情況下通常性能比兩階段要好,但正常情況下性能比 2 階段要差。
解決單點問題:協調者在 preCommit 之後宕機,參與者默認是提交事務
一致性問題並沒有解決

回到 XA,現在很多的數據庫都實現了 XA 協議,這使得我們使用分佈式事務可以同本地事務一樣簡單,我們來看最熟悉的 MySQL XA 協議的相關實現。首先能夠支持 XA 的只有 InnoDB 引擎,MySQL 的 XA 又分爲外部 XA 與內部 XA。

內部 XA:在開啓 binlog(二進制日誌, 它記錄了數據庫上的所有改變)的情況下,MySQL 同時維護了 binlog 日誌與 InnoDB 的 redo log(新數據的份。在事務提交前,只要將 Redo Log 持久化即可,不需要將數據持久化。當系統崩潰時,雖然數據沒有持久化,但是 Redo Log 已經持久化,系統可以根據 Redo Log 的內容,將所有數據恢復到最新的狀態, Redo Log 保證事務的持久性。還有一種 log 叫 Undo Log, Undo Log 的原理很簡單,爲了滿足事務的原子性,在操作數據之前,首先將數據備份到 Undo Log。然後進行數據的修改。如果出現了錯誤或者用戶執行了  ROLLBACK 語句,系統可以利用 Undo Log 中的備份將數據恢復到事務開始之前的狀態, Undo Log 保證事務的持久性。)爲了保證這兩個日誌的一致性,MySQL 使用了 XA 事務,由於只在單機上工作,所以被稱爲內部 XA,此時 MySQL 服務器中的存儲引擎充當 RM,而服務器本身充當 TM。

外部 XA:指多個 MySQL 實例的分佈式事務,即廣泛意義上的分佈式事務,此時 MySQL 服務器作爲 RM,外部應用程序作爲 TM

JTA 可以用於分佈式系統中分佈式事務的管理. 原理是通過兩階段的提交. 可以同時管理多個數據源的事務.

XA 協議是一套分佈式事務管理的規範, JTA 是 XA 協議在 Java 中的實現, 多個數據庫或是消息廠商實現 JTA 接口, 開發人員只需要調用 SpringJTA 接口即可實現 JTA 事務管理.

但是 JTA 也有比較嚴重的性能問題, 由於同時操作多個數據源, 如果其中一個數據源獲取數據的時間過長, 會導致整個請求都非常的長, 因此現實中對性能要求比較高的系統較少使用 JTA 事務管理.

常用分佈式系統事務管理實現高性能和高吞吐的方式是 Spring 事務同步機制以及犧牲掉事務的暫時一致性, 而保證事務的最終一致性.

SpringBoot 通過 Atomikos 或 Bitronix 支持 JTA 分佈式事務,當配置了 JTA 時,Spring JtaTransactionManager 將作爲事務管理器,@Transactional 註解自動支持 XA 事務

補償事務 TCC

TCC 爲以下三個英文的縮寫:
try:完成所有業務檢查, 預留必須業務資源
confirm:確認執行業務操作
cancel:取消執行業務操作。
其中 confirm 和 cancel 是一對反向操作,所以 TCC 的原理其實比較簡單粗暴,如果出錯,則對原來的所有操作進行回滾。其本質上爲一個 2 階段提交。它的缺點在於 Try、Confirm 和 Cancel 操作功能需業務提供,開發成本高。

對於 TCC,阿里的分佈式事務框架 Seata 就採用了該方案。

本地消息表

最初由 ebay 架構師提出來的分佈式事務解決方案,大致流程如下
1、系統 A 首先更新自己的業務表,並同時會在本地消息表中插入一條數據,這兩個操作是在同一個事務當中完成
2、定時任務掃描本地消息表,向 MQ 中間件進行消息發送
3、系統 B 消費消息,消費成功通知 A 進行本地消息表狀態更改,消費失敗通知 A 進行回滾

缺點:依賴 MQ 的可靠性,由於 MQ 可能失敗重試等原因,需要生產者和消費者同時支持冪等性

可靠消息最終一致性

同本地消息表一樣還是需要依賴 MQ 來實現分佈式事務,基本步驟如下
1、系統 A 發送 prepare 消息到 MQ
2、收到 ack 執行本地事務,並根據執行結果發送消息,MQ 根據執行結果判斷是否將 prepare 消息轉爲 confirm 消息
3、對於遲遲不發送本地事務執行結果的 prepare 消息,MQ 會進行回查
4、系統 B 消費 MQ 中的消息

阿里 RocketMQ 基於該方案爲我們提供了完整實現:

第一階段:生產者向 MQ 服務器發送事務消息 (prepare 消息),服務端確認後回調通知生產者執行本地事務 (此時消息爲 Prepare 消息,存儲於 RMQ_SYS_TRANS_HALF_TOPIC 隊列中,不會被消費者消費)
第二階段:生產者執行完本地事務後 (業務執行完成,同時將消息唯一標記,如 transactionId 與該業務執行記錄同時入庫,方便事務回查),根據本地事務執行結果,返回 Commit/Rollback/Unknow 狀態碼
1、服務端若收到 Commit 狀態碼,則將 prepare 消息變爲提交 (正常消息,可被消費者消費)
2、收到 Rollback 則對消息進行回滾 (丟棄消息)
3、若狀態爲 Unknow,則等待 MQ 服務端定時發起消息狀態回查,超過一定重試次數或者超時,消息會被丟棄

被動方服務訂閱主題後只需要等待 MQ 投遞消息即可。
當消息投遞,被動方服務消費該消息並執行本地業務操作,當本地業務執行成功,被動方服務調用消息服務,返回本地業務執行成功。
可靠消息服務根據業務唯一參數(訂單號結合消息 id)設置消息狀態爲 “已完成”
整個過程中,作爲被動方服務需要盡最大努力將業務向最終狀態推進,最終成功或者失敗並通知消息服務置消息狀態爲完成的終態。

最大努力通知

這個方案比較簡單,適用於一些時間敏感度比較低或者分佈式事務要求不太嚴格的業務。
1、系統 A 執行本地事務之後,發送消息到 MQ
2、系統 B 消費 MQ,系統 B 執行失敗會進行重試。

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