聊聊微服務中的事務管理
幾乎所有的信息管理系統(tǒng)都會涉及到事務,事務的目的是為了保證數(shù)據(jù)的一致性,這里說的一致性是數(shù)據(jù)庫狀態(tài)的一致性。
說到數(shù)據(jù)庫狀態(tài)的一致性,相信大家都會想到 ACID :
- 原子性(Atomic):在一個事件的多個數(shù)據(jù)庫操作中,要么同時成功,要么同時失敗,例如:轉賬業(yè)務。
- 隔離性(Isolation):不同的業(yè)務之間處理數(shù)據(jù)相互獨立,互不影響。
- 持久性(Durability):正常提交的數(shù)據(jù)能夠被持久化,不丟失數(shù)據(jù),比如 mysql 天然就能持久化,redis 、 rabbitmq 也能通過設置進行持久化。
- 一致性(Consistency):最終的數(shù)據(jù)正確,所以說是通過 AID 這些手段來保證了 C 。
在單體架構中,通常是一套程序對應一個數(shù)據(jù)庫,事務基于數(shù)據(jù)庫本身的能力,如果你在 .NET Core 中使用 dapper 或 sqlsugar ,可以很容易進行事務的處理,可以參考下面文檔:
https://dapper-tutorial.net/transaction 。
https://www.donet5.com/Home/Doc?typeId=1183。
但是,在微服務架構,分布式的場景中,事務的處理就會變得復雜,會存在多個節(jié)點,多個節(jié)點的同步、可用性等都是需要考慮的問題,在分布式中有一個著名的 CAP 理論:
- C:數(shù)據(jù)一致性(Consisitency):分布式中存在多個節(jié)點,對某個指定的客戶端來說,從任一節(jié)點讀取的數(shù)據(jù)保證獲取到的是最新寫入的數(shù)據(jù)。
- A:可用性(Acailability),非故障節(jié)點在合理的時間內返回合理的響應(不是錯誤和超時的響應)。
- P:分區(qū)容錯性(Partition Tolerance),節(jié)點之間的數(shù)據(jù)傳遞是基于網(wǎng)絡的,由于網(wǎng)絡本身不是 100% 可靠,極端情況下會出現(xiàn)網(wǎng)絡不可用的情況,進而將網(wǎng)絡兩端的節(jié)點分隔開來,這就是所謂的「網(wǎng)絡分區(qū)」現(xiàn)象。在出現(xiàn)網(wǎng)絡分區(qū)時,兩部分的數(shù)據(jù)是不一致的,如果要保證數(shù)據(jù)的一致性,就必須要讓沒有及時同步數(shù)據(jù)的節(jié)點變?yōu)椴豢捎?,這就犧牲了可用性,否則就會犧牲一致性,所以在 P 一定存在的情況下,需要在 C 和 A 中間做取舍。
我們在 CAP、ACID 中討論的一致性稱為「強一致性」(Strong Consistency),而把犧牲了 C 的 AP 系統(tǒng),但又要保證最終的結果是一致的,稱為「弱一致性」,也叫最終一致性。最終一致性的概念由 eBay 的系統(tǒng)架構師丹 · 普利切特(Dan Pritchett)在 2008 年發(fā)表于 ACM 的論文「Base: An Acid Alternative」中提出的。
本文主要說下保證一致性的幾種方式:TCC、SAGA 和消息隊列。
TCC
TCC 是 Try-Confirm-Cancel 的縮寫,表示將整個過程分為了三個階段:
- Try:一個請求涉及到多個服務,多個服務會同時進行 Try,這個階段為嘗試執(zhí)行階段,在這個階段中會進行數(shù)據(jù)的校驗、檢查,保障一致性,并準備資源,都成功會進入到 Confirm 階段。
- Confirm:確認執(zhí)行階段,不進行任何業(yè)務檢查,多個服務的 Try 都執(zhí)行成功了,多個服務都進入到 Confirm 階段,在這個階段直接使用 Try 階段準備的資源來完成業(yè)務處理。注意,Confirm 階段可能會重復執(zhí)行,因此需要滿足冪等性。
- Cancel:如果在 Try 階段有一個服務沒有成功,那么所有的服務都進入到 Cancel 階段,在該階段,釋放 Try 階段預留的業(yè)務資源。注意,Cancel 階段也可能會重復執(zhí)行,因此也需要滿足冪等性。
在 .NET Core 中可以參考:
https://github.com/simpleway2016/JMS。
在 Java 中可以使用 seata:
https://github.com/seata/seata https://seata.io/zh-cn/。
因為在 TCC 中的第一步 Try 需要預留資源,進行檢查和校驗,但在某些場景下,資源不是我們所能控制的,比如支付中,余額是銀行管理的,我們通常沒有權限。所以這時就不太適合 TCC ,可以考慮用 SAGA 來代替 TCC。
SAGA
SAGA 起源于 1987 年普林斯頓大學的赫克托 · 加西亞 · 莫利納(Hector Garcia Molina)和肯尼斯 · 麥克米倫(Kenneth Salem)在 ACM 發(fā)表的一篇論文《SAGAS》。
SAGA 和 TCC 最大的區(qū)別是基于數(shù)據(jù)補償機制來代替回滾。一個 SAGA 表示處理多個服務中數(shù)據(jù)的一系列操作,由一連串的本地事務組成,每個獨立的本地事務中還是能夠使用 ACID 。
SATA 由兩部分組成:
- 將一個大的事務拆分成的若干個小的事務,比如一個大的事務 T ,拆分成 T1、T2、T3。
- 每一個子事務有對應的補償動作,例如對應上面的 T1、T2、T3 有 C1、C2、C3 的補償動作。
在 ACID 中如果出現(xiàn)異常,可以很容易進行回滾,但 SAGA 沒辦法自己回滾,必須依賴補償動作來進行回滾。
如果 T1、T2、T3 都提交成功了,整個事務 T 就提交成功,如果執(zhí)行 T2 時出現(xiàn)異常,這時有兩種方式進行處理:
正向(不斷重試):不斷對 T2 進行重試操作,直到成功(不排除人工干預),等 T2 重試成功后,繼續(xù)執(zhí)行后面的 T3。
反向(補償):T2 出現(xiàn)異常時,執(zhí)行對應的補償 C2,C2 必須執(zhí)行成功(不排除人工),然后執(zhí)行 T1 對應的補償動作 C1 。
在上面提到的 seata 中也同樣可以支持 SAGA 模式。
除了 seata ,還有一個用 go 語言寫的 DTM 分布式事務框架也不錯:
https://dtm.pub/ https://github.com/dtm-labs/dtm。
重要的是,DTM 支持 C# 客戶端:
https://github.com/dtm-labs/dtmcli-csharp。
消息隊列
消息隊列相信大家都不陌生,我們零代碼產品中調用外部接口的組件,會被用在一些復雜的業(yè)務邏輯編排中,對外部接口的調用就是使用消息隊列,RabbitMQ 的延時隊列加上死信隊列可以來進行重試的操作,來保證數(shù)據(jù)的最終一致。
還有另一種方式就是使用事務消息表,比如有這樣一個場景,在系統(tǒng)列表中刪除一條流程數(shù)據(jù),這時需要做:
1、列表服務中對數(shù)據(jù)進行刪除;。
2、文件服務對這條數(shù)據(jù)相關的附件進行刪除。
3、流程服務對該業(yè)務數(shù)據(jù)的所有流程信息進行刪除。
具體的步驟如下:
1、列表服務刪除數(shù)據(jù)成功后,在數(shù)據(jù)庫中創(chuàng)建一張事務消息表,該表中記錄事務 ID、數(shù)據(jù)刪除成功的狀態(tài)、業(yè)務數(shù)據(jù) ID、附件待刪除的狀態(tài)、流程信息待刪除的狀態(tài)等。
2、列表服務刪除數(shù)據(jù)成功后,發(fā)送消息分別進行附件刪除處理和流程信息刪除處理。
3、消息被正確處理后,修改事務消息表的狀態(tài)。
4、創(chuàng)建一個單獨的消息服務程序,輪詢掃描事務消息表,如果發(fā)現(xiàn)狀態(tài)沒有變成已完成,就重新發(fā)送一個新的消息,這樣附件刪除和流程信息刪除就會進行多次執(zhí)行,這也要求這些操作必須是冪等的。
RabbitMQ 本身不支持分布式事務,不過有一些消息中間件是支持的,例如:RocketMQ,原生就支持分布式事務操作,可以更方便進行事務處理。
本文是一些理論的梳理,要想更徹底地掌握,可以選擇一個框架,找?guī)讉€場景,寫寫代碼演練一下。