分布式事務的 N 種實現
需求緣起
在微服務架構中,隨著服務的逐步拆分,數據庫私有已經成為共識,這也導致所面臨的分布式事務問題成為微服務落地過程中一個非常難以逾越的障礙,但是目前尚沒有一個完整通用的解決方案。
其實不僅僅是在微服務架構中,隨著用戶訪問量的逐漸上漲,數據庫甚至是服務的分片、分區、水平拆分、垂直拆分已經逐漸成為較為常用的提升瓶頸的解決方案,因此越來越多的原子操作變成了跨庫甚至是跨服務的事務操作。最終結果是在對高性能、高擴展性,高可用性的追求的道路上,我們開始逐漸放松對一致性的追求,但是在很多場景下,尤其是賬務,電商等業務中,不可避免的存在著一致性問題,使得我們不得不去探尋一種機制,用以在分布式環境中保證事務的一致性
引用自 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
理論基石
ACID 和 BASE
見 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
見 https://www.txlcn.org/zh-cn/docs/preface.html
2PC
談到分布式事務,首先要說的就是 2PC(two phase commit)方案,如下圖所示:
2PC 把事務的執行分為兩個階段,第一個階段即 prepare 階段,這個階段實際上就是投票階段,協調者向參與者確認是否可以共同提交,再得到全部參與者的所有回答后,協調者向所有的參與者發布共同提交或者共同回滾的指令,用以保證事務達到一致性。
2PC 是幾乎所有分布式事務算法的基礎,后續的分布式事務算法幾乎都由此改進而來。
需求樣例
這里我們定義一個充值需求,后續我們在各個實現中看看如何為該需求實現分布式事務。
Order 和 Account 分別是獨立的一個服務,充值完成后,要分別將訂單Order 設置為成功以及增加用戶余額。
實現1 Seata
介紹 & 框架
Seata(Fescar) is a distributed transaction solution with high performance and ease of use for microservices architecture.
阿里開源,其特點是用一個事務管理器,來管理每個服務的事務,本質上是 2PC(后文會解釋) 的一種實現。
Seata 提供了全局的事務管理器
原理
代理 SQL 查詢,實現事務管理,類似中間件
實現充值需求
用該方案實現需求的話,就是這樣的:
Order 和 Account 都接入 Seata 來代理事務
代碼示例
比起自己去實現 2PC,Seata 提供了簡化方案,代碼實例見 :
實現2 TCC
介紹
TCC(Try-Confirm-Concel) 模型是一種補償性事務,主要分為 Try:檢查、保留資源,Confirm:執行事務,Concel:釋放資源三個階段,如下圖所示:
其中,活動管理器記錄了全局事務的推進狀態以及各子事務的執行狀態,負責推進各個子事務共同進行提交或者回滾。同時負責在子事務處理超時后不停重試,重試不成功后轉手工處理,用以保證事務的最終一致性。
原理
每個子節點,要實現 TCC 接口,才能被管理。
優點:不依賴 local transaction,可以管理非關系數據庫庫的服務
缺點:TCC 模式多增加了一個狀態,導致在業務開發過程中,復雜度上升,而且協調器與子事務的通信過程增加,狀態輪轉處理也更為復雜。而且,很多業務是無法補償的,例如銀行卡充值。
實現框架
tx-lcn LCN distributed transaction framework, compatible with dubbo, spring cloud and Motan framework, supports various relational databases https://www.txlcn.org
或者 Seata MT 模式
代碼示例
實現充值需求
需要把 Oder.done 和 Account 的余額+ 操作都實現 tcc 接口
可以看出,這樣真的很麻煩,能用本地事務的還是盡量用本地事務吧
實現3 事務消息
介紹
以購物場景為例,張三購買物品,賬戶扣款 100 元的同時,需要保證在下游的會員服務中給該賬戶增加 100 積分。由于數據庫私有,所以導致在實際的操作過程中會出現很多問題,比如先發送消息,可能會因為扣款失敗導致賬戶積分無故增加,如果先執行扣款,則有可能因服務宕機,導致積分不能增加,無論是先發消息還是先執行本地事務,都有可能導致出現數據不一致的結果。
事務消息的本質就是為了解決此類問題,解決本地事務執行與消息發送的原子性問題。
實現框架
Apache RocketMQ™ is an open source distributed messaging and streaming data platform.
原理
- 事務發起方首先發送 prepare 消息到 MQ。
- 在發送 prepare 消息成功后執行本地事務。
- 根據本地事務執行結果返回 commit 或者是 rollback。
- 如果消息是 rollback,MQ 將刪除該 prepare 消息不進行下發,如果是 commit 消息,MQ 將會把這個消息發送給 consumer 端。
- 如果執行本地事務過程中,執行端掛掉,或者超時,MQ 將會不停的詢問其同組的其它 producer 來獲取狀態。
- Consumer 端的消費成功機制有 MQ 保證。
優點:對異步操作支持友好
缺點:Producer 端要為 RMQ 實現事務查詢接口,導致在業務開發過程中,復雜度上升。
代碼示例
// TODO
實現充值需求
通過 MQ,來保障 Order 和 Acount 的兩個操作要么一起成功,要么一起失敗。
注意一個點,假設 Account 的余額+失敗了,這里是無法回滾 Order 的操作的,Account 要保證自己能正確處理消息。
實現4 本地消息表
介紹 & 原理
分布式事務=A系統本地事務 + B系統本地事務 + 消息通知;
準備:
A系統維護一張消息表log1,狀態為未執行,
B系統維護2張表,
未完成表log2,
已完成表log3,
消息中間件用兩個topic,
topic1是A系統通知B要執行任務了,
topic2是B系統通知A已經完成任務了,
- 用戶在A系統里領取優惠券,并往log1插入一條記錄
- 由定時任務輪詢log1,發消息給B系統
- B系統收到消息后,先檢查是否在log3中執行過這條消息,沒有的話插入log2表,并進行發短信,發送成功后刪除log2的記錄,插入log3
- B系統發消息給A系統
- A系統根據id刪除這個消息
假設出現網絡中斷和系統 Crash 等問題時,為了繼續執行事務,需要進行重試。重試方式有:
- 定時任務恢復事務的執行,
- 使用 MQ 來傳遞消息,MQ可以保證消息被正確消費。
優點:簡單
缺點:程序會出現執行到一半的狀態,重試則要求每個操作需要實現冪等性
注意:分布式系統實現冪等性的時候,記得使用分布式鎖,分布式鎖詳細介紹見文末參考文章
實現充值需求
通過消息表,把斷開的事務繼續執行下去。
實現5 考拉的方案
介紹 & 原理
考拉的方案,就是使用本地消息表,但是少了兩個重要組件(MQ 和 關系型數據庫),寫起來還是比較辛苦的。
考拉方案有如下特點:
- Order 表承擔了消息表功能
- 服務之間使用 http 通信,所以碰到問題要依賴定時任務發布補單重試
- 沒有使用關系型數據庫,冪等性的實現比較困難。
實現充值需求
難點:
-
實現冪等性的要求太高,基本要求所有操作都需要實現冪等性,例如更新余額操作,要高效更新,簡單的辦法是使用樂觀鎖,但是要同時兼顧冪等性的話,樂觀鎖就不夠用了。
-
程序在任一一步斷開,都需要重新運行起來,補單程序會很難寫(簡單的業務還好,復雜業務就會混亂了)
改進建議:
- 服務直接使用 mq 通信,服務異常需要重試消費。
- 使用關系型數據庫,通過本地事務,可以只程序開始處判斷重復,簡化冪等性的實現邏輯
實際上就是往上一個實現4上走。
總結
我們先對這些實現方案進行一個總結:
基礎原理 | 實現 | 優勢 | 必要前提 |
---|---|---|---|
2PC | Seata | 簡單 | 關系型數據庫 |
2PC | TCC | 不依賴關系系數據庫 | 實現 TCC 接口 |
2PC | 事務消息 | 高性能 | 實現事務檢查接口 |
最終一致性 | 本地消息表 | 去中心化 | 侵入業務,接口需要冪等性 |
各個方案有自己的優劣,實際使用過程中,我們還是需要根據情況來選擇不同事務方案來靈活組合。
例如存在服務模塊A 、B、 C。A模塊是mysql作為數據源的服務,B模塊是基于redis作為數據源的服務,C模塊是基于mongo作為數據源的服務。若需要解決他們的事務一致性就需要針對不同的節點采用不同的方案,并且統一協調完成分布式事務的處理。
方案:將A模塊采用 Seata 模式、B/C采用TCC模式就能完美解決。