1、簡介
1.1、背景
我們所做的項目中經常涉及到服務A調服務B、服務B調服務C這種情況,當整個調用鏈路中某一個服務發生異常時,需要將所涉及所有服務做的修改都回滾掉才能保證整體事務的一致性。Spring所提供的聲明式事務@Transcational只能保證服務在操作自身數據庫時的事務一致性,而無法保證其他服務的事務與自身保持一致。
1.2、我們真的需要分佈式事務嗎?
引入任何一個技術之前我覺得都應該想清楚真的需要嗎?其實並不是所有的服務間調用都有一致性事務的問題。例如以下幾種場景,個人認爲並不需要分佈式事務:
- 在一套大的單點登錄系統中,只會有一個服務管理用戶信息,我們可以設計爲其他的服務可以獲取用戶信息,但是不能修改,那麼這樣的設計就天然的避免了分佈式事務。
- zipkin這種調用鏈採集服務,通常我們在調用時並不需要過多的關注成功與否,也不會讓它的調用結果影響正常業務流程。
再看可能需要分佈式事務的場景,簡單的服務A調用服務B僞代碼:
@Transcational
public void method(){
//修改服務A數據庫
doSomeThingForA1();
//調用服務B接口(服務B接口中有修改服務B數據庫的操作)
accessServiceB();
//再次修改服務A數據庫
doSomeThingForA2();
}
若是方法doSomeThingForA1中發生異常,對數據庫A的操作將回滾,不會調用服務B接口;
若是方法accessServiceB發生異常,服務B中發生錯誤自身會回滾,服務A接收到錯誤也拋出異常回滾;
若是方法doSomeThingForA2發生異常,對數據庫A的操作可以回滾,但是服務B的數據庫將無法回滾。
這麼一分析,我們是不是在允許的情況下改造一下代碼順序就可以呢?
@Transcational
public void method(){
//修改服務A數據庫
doSomeThingForA();
//調用服務B接口(服務B接口中有修改服務B數據庫的操作)
accessServiceB();
}
將對數據庫A的修改操作全部寫在調用服務B接口之前,這樣一旦doSomeThingForA發生任何異常,就不會再調用B了,而B接口發生任何異常兩邊的操作也都可以回滾。
但是要強調的是”允許的情況下“,如果無法避免第一種情況的發生,我們還是要從分佈式事務的角度去解決一致性問題。
這麼一分析是不是感覺大部分項目都不需要這玩意了,畢竟不是每個系統都像天貓、淘寶那樣的,哈哈哈。。。
1.3、分佈式事務的特性
本地事務我們強調的是ACID,即原子性、一致性、隔離性、持久性。ACID對於事務的要求太過嚴格,對於高併發的分佈式系統來說,需要作出一些取捨來保證性能。那麼分佈式事務關注什麼呢?
- Consistency:一致性
- Availability:可用性
- Partition tolerance:分區容錯性
其實個人認爲CAP並不僅是分佈式事務的特性,幾乎所有的分佈式相關組件都在考慮CAP。我們在聊到euraka、zookeeper等等這些東西的時候也會提到CAP。分佈式組件很難同時滿足三者,所以纔出現了那麼多CA、CP、AP的組件。
既然CAP很難同時滿足,便出現了退而求其次的BASE理論:
- Basically Available:基本可用
- Soft State:軟狀態
- Eventual Consistency:最終一致性
2、常用解決方案
目前爲止應該沒有完美的解決方案,但是我們可以通過借鑑那些大型系統中用過的方案,瞭解其優缺點,根據實際項目分析,設計出自己的分佈式事務方案。
首先,瞭解幾個關鍵的概念:
- XA:由X/Open組織提出的分佈式事務的規範。 XA規範主要定義了(全局)事務管理器™和(局 部)資源管理器(RM)之間的接口。主流的關係型 數據庫產品都是實現了XA接口的。
- RM:資源管理器(Resource Manager),用來管理系統資源,是通向事務資源的途徑。數據庫就是一種資源管理器。資源管理還應該具有管理事務提交或回滾的能力。
- TM:事務管理器(Transaction Manager),事務管理器是分佈式事務的核心管理者。事務管理器與每個資源管理器RM進行通信,協調並完成事務的處理。
參考
2.1、兩階段提交協議
2.1.1 協議原理
兩階段提交協議是協調所有分佈式原子事務參與者,並決定提交或取消(回滾)的分佈式算法。
在兩階段提交協議中,系統一般包含兩類角色:協調者(coordinator),和事務參與者(participants,cohorts或workers)。協調者通常一個系統只有一個,而參與者一般包含多個。
2.1.2 兩個階段的執行
-
請求階段(commit-request phase,或稱表決階段,voting phase)
在請求階段,協調者將通知事務參與者準備提交或取消事務,然後進入表決過程。
在表決過程中,參與者將告知協調者自己的決策:同意(事務參與者本地作業執行成功)或取消(本地作業執行故障)。 -
提交階段(commit phase)
在該階段,協調者將基於第一個階段的投票結果進行決策:提交或取消。
當且僅當所有的參與者同意提交事務協調者才通知所有的參與者提交事務,否則協調者將通知所有的參與者取消事務。
參與者在接收到協調者發來的消息後將執行響應的操作。
2.1.3 方案缺點
-
同步阻塞問題。執行過程中,所有參與節點都是事務阻塞型的。
當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。 -
單點故障。由於協調者的重要性,一旦協調者發生故障。
參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那麼所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。(如果是協調者掛掉,可以重新選舉一個協調者,但是無法解決因爲協調者宕機導致的參與者處於阻塞狀態的問題) -
數據不一致。在二階段提交的階段二中,當協調者向參與者發送commit請求之後,發生了局部網絡異常或者在發送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。
而在這部分參與者接到commit請求之後就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分佈式系統便出現了數據不一致性的現象。 -
兩階段提交無法解決的問題
當協調者出錯,同時參與者也出錯時,兩階段無法保證事務執行的完整性。
考慮協調者再發出commit消息之後宕機,而唯一接收到這條消息的參與者同時也宕機了。那麼即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。
2.2、 三階段提交協議
2.2.1 協議簡介
三階段提交協議在協調者和參與者中都引入超時機制,並且把兩階段提交協議的第一個階段拆分成了兩步:詢問,然後再鎖資源,最後真正提交。
2.2.2 三個階段的執行
-
CanCommit階段
3PC的CanCommit階段其實和2PC的準備階段很像。
協調者向參與者發送commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。 -
PreCommit階段
Coordinator根據Cohort的反應情況來決定是否可以繼續事務的PreCommit操作。
根據響應情況,有以下兩種可能。
A.假如Coordinator從所有的Cohort獲得的反饋都是Yes響應,那麼就會進行事務的預執行:
發送預提交請求。Coordinator向Cohort發送PreCommit請求,並進入Prepared階段。
事務預提交。Cohort接收到PreCommit請求後,會執行事務操作,並將undo和redo信息記錄到事務日誌中。
響應反饋。如果Cohort成功的執行了事務操作,則返回ACK響應,同時開始等待最終指令。
B.假如有任何一個Cohort向Coordinator發送了No響應,或者等待超時之後,Coordinator都沒有接到Cohort的響應,那麼就中斷事務:
發送中斷請求。Coordinator向所有Cohort發送abort請求。
中斷事務。Cohort收到來自Coordinator的abort請求之後(或超時之後,仍未收到Cohort的請求),執行事務的中斷。 -
DoCommit階段
該階段進行真正的事務提交,也可以分爲以下兩種情況:
(1) 執行提交
A.發送提交請求。Coordinator接收到Cohort發送的ACK響應,那麼他將從預提交狀態進入到提交狀態。並向所有Cohort發送doCommit請求。
B.事務提交。Cohort接收到doCommit請求之後,執行正式的事務提交。並在完成事務提交之後釋放所有事務資源。
C.響應反饋。事務提交完之後,向Coordinator發送ACK響應。
D.完成事務。Coordinator接收到所有Cohort的ACK響應之後,完成事務。
(2) 中斷事務
Coordinator沒有接收到Cohort發送的ACK響應(可能是接受者發送的不是ACK響應,也可能響應超時),那麼就會執行中斷事務。
三階段提交協議和兩階段提交協議的不同
對於協調者(Coordinator)和參與者(Cohort)都設置了超時機制(在2PC中,只有協調者擁有超時機制,即如果在一定時間內沒有收到cohort的消息則默認失敗)。
在2PC的準備階段和提交階段之間,插入預提交階段,使3PC擁有CanCommit、PreCommit、DoCommit三個階段。
PreCommit是一個緩衝,保證了在最後提交階段之前各參與節點的狀態是一致的。
2.2.3 協議缺點
如果進入PreCommit後,Coordinator發出的是abort請求,假設只有一個Cohort收到並進行了abort操作,
而其他對於系統狀態未知的Cohort會根據3PC選擇繼續Commit,此時系統狀態發生不一致性。
2.3 TCC
2.3.1 協議簡介
TCC是服務化的兩階段編程模型,其Try、Confirm、Cancel 3個方法均由業務編碼實現。其中Try操作作爲一階段,負責資源的檢查和預留,Confirm操作作爲二階段提交操作,執行真正的業務,Cancel是預留資源的取消。
2.3.2 實現注意事項
(1) 業務操作分爲兩個階段
接入TCC前,業務操作只需要一步就能完成,但是在接入TCC之後,需要考慮如何將其分成2階段完成,把資源的檢查和預留放在一階段的Try操作中進行,把真正的業務操作的執行放在二階段的Confirm操作中進行。
(2) 允許空回滾
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因爲丟包而導致的網絡超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;
TCC服務在未收到Try請求的情況下收到Cancel請求,這種場景被稱爲空回滾;TCC服務在實現時應當允許空回滾的執行;
(3) 防懸掛控制
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因網絡擁堵而導致的超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;在此之後,擁堵在網絡上的一階段Try數據包被TCC服務收到,出現了二階段Cancel請求比一階段Try請求先執行的情況;
用戶在實現TCC服務時,應當允許空回滾,但是要拒絕執行空回滾之後到來的一階段Try請求
(4) 冪等控制
無論是網絡數據包重傳,還是異常事務的補償執行,都會導致TCC服務的Try、Confirm或者Cancel操作被重複執行;用戶在實現TCC服務時,需要考慮冪等控制,即Try、Confirm、Cancel 執行一次和執行多次的業務結果是一樣的
(5) 業務數據可見性控制
TCC服務的一階段Try操作會做資源的預留,在二階段操作執行之前,如果其他事務需要讀取被預留的資源數據,那麼處於中間狀態的業務數據該如何向用戶展示,需要業務在實現時考慮清楚;通常的設計原則是“寧可不展示、少展示,也不多展示、錯展示”
(6) 業務數據併發訪問控制
TCC服務的一階段Try操作預留資源之後,在二階段操作執行之前,預留的資源都不會被釋放;如果此時其他分佈式事務修改這些業務資源,會出現分佈式事務的併發問題;
用戶在實現TCC服務時,需要考慮業務數據的併發控制,儘量將邏輯鎖粒度降到最低,以最大限度的提高分佈式事務的併發性。
2.3.3 協議優缺點
- 優點:不與具體的服務框架耦合,位於業務服務層,而不是資源層,可以靈活的選擇業務資源的鎖定粒度。TCC裏對每個服務資源操作的是本地事務,數據被鎖住的時間短,可擴展性好,可以說是爲獨立部署的SOA服務而設計的。
- 缺點:實現TCC操作的成本較高,業務活動結束的時候Confirm和Cancel操作的執行成本。業務活動的日誌成本。
使用範圍:強隔離性,嚴格一致性要求的業務活動。適用於執行時間較短的業務,比如處理賬戶或者收費等等。
2.4、 基於可靠消息的最終一致性方案
2.4.1 方案簡介
(1) A 系統先發送一個 prepared 消息到 mq,如果這個 prepared 消息發送失敗那麼就直接取消操作別執行了;
(2) 如果這個消息發送成功過了,那麼接着執行本地事務,如果成功就告訴 mq 發送確認消息,如果失敗就告訴 mq 回滾消息;
(3) 如果發送了確認消息,那麼此時 B 系統會接收到確認消息,然後執行本地的事務;
(4) mq 會自動定時輪詢所有 prepared 消息回調你的接口,問你,這個消息是不是本地事務處理失敗了,所有沒發送確認的消息,是繼續重試還是回滾?一般來說這裏你就可以查下數據庫看之前本地事務是否執行,如果回滾了,那麼這裏也回滾吧。這個就是避免可能本地事務執行成功了,而確認消息卻發送失敗了。
(5) 這個方案裏,要是系統 B 的事務失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要麼就是針對重要的資金類業務進行回滾,比如 B 系統本地回滾後,想辦法通知系統 A 也回滾;或者是發送報警由人工來手工回滾和補償。
2.4.2 方案優缺點
- 優點: 實現了最終一致性,不需要依賴本地數據庫事務。
- 缺點: 實現難度大,主流MQ不支持,RocketMQ事務消息部分代碼也未開源。
3、開源框架
3.1 LCN
3.1.1 簡介
官網:https://www.txlcn.org/zh-cn/
LCN框架在2017年6月份發佈第一個版本,目前已經更新到5.0。LCN的名稱來源於其三個核心步驟的首字母
- 鎖定事務單元(lock)
- 確認事務模塊狀態(confirm)
- 通知事務(notify)
5.0以後由於框架兼容了LCN、TCC、TXC三種事務模式,爲了避免區分LCN模式,特此將LCN分佈式事務改名爲TX-LCN分佈式事務框架。
框架定位:
LCN並不生產事務,LCN只是本地事務的協調工
3.2 事務控制原理
TX-LCN由兩大模塊組成, TxClient、TxManager,TxClient作爲模塊的依賴框架,提供TX-LCN的標準支持,TxManager作爲分佈式事務的控制者。事務發起方或者參與方都由TxClient端來控制。
3.2 Seata
3.2.1 簡介
官網:https://github.com/seata/seata/wiki/%E6%A6%82%E8%A7%88
Seata(曾用名Fescar,開源版本GTS)是阿里的開源分佈式事務框架。設計上注重:
- 對業務無侵入: 這裏的 侵入 是指,因爲分佈式事務這個技術問題的制約,要求應用在業務層面進行設計和改造。這種設計和改造往往會給應用帶來很高的研發和維護成本。我們希望把分佈式事務問題在 中間件 這個層次解決掉,不要求應用在業務層面做額外的工作。
- 高性能: 引入分佈式事務的保障,必然會有額外的開銷,引起性能的下降。我們希望把分佈式事務引入的性能損耗降到非常低的水平,讓應用不因爲分佈式事務的引入導致業務的可用性受影響。
3.2.2 原理
一個典型的分佈式事務過程:
(1) TM 向 TC 申請開啓一個全局事務,全局事務創建成功並生成一個全局唯一的 XID。
(2) XID 在微服務調用鏈路的上下文中傳播。
(3) RM 向 TC 註冊分支事務,將其納入 XID 對應全局事務的管轄。
(4) TM 向 TC 發起針對 XID 的全局提交或回滾決議。
(5) TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求。
還有很多的細節見官網了,官網圖文並茂寫得很清楚。。。
另還有開源框架Saga也值得了解一下。