概念
分布式事務
分布式事務是指事務的參與者、支持事務的服務器、資源服務器分別位於分布式系統的不同節點之上,通常一個分布式事物中會涉及到對多個數據源或業務系統的操作。
典型的分布式事務場景:跨銀行轉操作就涉及調用兩個異地銀行服務
CAP理論
一個分布式系統不可能同時滿足一致性,可用性和分區容錯性這個三個基本需求,最多只能同時滿足其中兩項
-
一致性(C):
寫操作之后的讀操作,必須返回該值。意味着,數據在多個副本之間是否能夠保持一致的特性。 -
可用性(A):
是指系統提供的服務必須一致處於可用狀態,對於每一個用戶的請求總是在有限的時間內返回結果,超過時間就認為系統是不可用的 -
分區容錯性(P):
分布式系統在遇到任何網絡分區故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務,除非整個網絡環境都發生故障。
CAP定理的應用
-
放棄P(CA):
如果希望能夠避免系統出現分區容錯性問題,一種較為簡單的做法就是將所有的數據(或者是與事物先相關的數據)都放在一個分布式節點上,這樣雖然無法保證100%系統不會出錯,但至少不會碰到由於網絡分區帶來的負面影響。 -
放棄A(CP):
其做法是一旦系統遇到網絡分區或其他故障時,那受到影響的服務需要等待一定的時間,應用等待期間系統無法對外提供正常的服務,即不可用 -
放棄C(AP):
這里說的放棄一致性,並不是完全不需要數據一致性,是指放棄數據的強一致性,保留數據的最終一致性。
BASE理論
全稱:Basically Available(基本可用),Soft state(軟狀態),和 Eventually consistent([ɪ'ventʃuəli]最終一致性)三個短語的縮寫。
BASE是對CAP中一致性和可用性權限的結果,是基於CAP定理演化而來的,核心思想是即使無法做到強一致性,但每個應用都可以根據自身的業務特定,采用適當的方式來使系統達到最終一致性
-
什么是基本可用呢?
假設系統,出現了不可預知的故障,但還是能用,相比較正常的系統而言:-
響應時間上的損失:
正常情況下的搜索引擎 0.5 秒即返回給用戶結果,而基本可用的搜索引擎可以在 1 秒作用返回結果。 -
功能上的損失:
在一個電商網站上,正常情況下,用戶可以順利完成每一筆訂單,但是到了大促期間,為了保護購物系統的穩定性,部分消費者可能會被引導到一個降級頁面。
-
-
什么是軟狀態呢?
相對於原子性而言,要求多個節點的數據副本都是一致的,這是一種 “硬狀態”。
軟狀態指的是:允許系統中的數據存在中間狀態,並認為該狀態不影響系統的整體可用性,即允許系統在多個不同節點的數據副本存在數據延時。 -
最終一致性
系統能夠保證在沒有其他新的更新操作的情況下,數據最終一定能夠達到一致的狀態(必須有個時間期限),因此所有客戶端對系統的數據訪問最終都能夠獲取到最新的值。
最終一致性分為 5 種
-
因果一致性(Causal consistency)
指的是:如果節點 A 在更新完某個數據后通知了節點 B,那么節點 B 之后對該數據的訪問和修改都是基於 A 更新后的值。於此同時,和節點 A 無因果關系的節點 C 的數據訪問則沒有這樣的限制。 -
讀己之所寫(Read your writes)
這種就很簡單了,節點 A 更新一個數據后,它自身總是能訪問到自身更新過的最新值,而不會看到舊值。其實也算一種因果一致性。 -
會話一致性(Session consistency)
會話一致性將對系統數據的訪問過程框定在了一個會話當中:系統能保證在同一個有效的會話中實現 “讀己之所寫” 的一致性,也就是說,執行更新操作之后,客戶端能夠在同一個會話中始終讀取到該數據項的最新值。 -
單調讀一致性(Monotonic read consistency)
單調讀一致性是指如果一個節點從系統中讀取出一個數據項的某個值后,那么系統對於該節點后續的任何數據訪問都不應該返回更舊的值。 -
單調寫一致性(Monotonic write consistency)
指一個系統要能夠保證來自同一個節點的寫操作被順序的執行。
然而,在實際的實踐中,這 5 種系統往往會結合使用,以構建一個具有最終一致性的分布式系統。
實際上,不只是分布式系統使用最終一致性,關系型數據庫在某個功能上,也是使用最終一致性的,比如備份,數據庫的復制過程是需要時間的,這個復制過程中,業務讀取到的值就是舊的。當然,最終還是達成了數據一致性。這也算是一個最終一致性的經典案例。
總的來說,BASE 理論面向的是大型高可用可擴展的分布式系統,和傳統事務的 ACID 是相反的,它完全不同於 ACID 的強一致性模型,而是通過犧牲強一致性來獲得可用性,並允許數據在一段時間是不一致的。
2PC提交
二階段提交協議是將事務的提交過程分成提交事務請求
和執行事務提交
兩個階段進行處理。
基本邏輯就是協調者給參與者發送請求,各個參與者將操作的結果反饋給協調者,協調者統一安排是提交還是終止事務。
階段一:提交事務請求(投票階段)
-
事務詢問:協調者向所有的參與者發送事務內容,詢問是否可以執行事務提交操作,並開始等待各參與者的響應
-
執行事務:各參與者節點執行事務操作,並將Undo和Redo信息記入事務日志中
-
如果參與者成功執行事務操作,就反饋給協調者Yes響應,表示事務可以執行;如果沒有成功執行事務,就反饋給協調者No響應,表示事務不可以執行
二階段提交協議的階段一也被稱為投票階段,即各參與者投票票表明是否可以繼續執行接下去的事務提交操作。
階段二:執行事務提交(提交階段)
- 正常提交
- 假如協調者從所有的參與者或得反饋都是Yes響應,那么就會執行事務提交。
- 發送提交請求:協調者向所有參與者節點發出Commit請求
- 事務提交:參與者接受到Commit請求后,會正式執行事務提交操作,並在完成提交之后放棄整個事務執行期間占用的事務資源
- 反饋事務提交結果:參與者在完成事物提交之后,向協調者發送ACK消息
- 完成事務:協調者接收到所有參與者反饋的ACK消息后,完成事務
- 中斷事務
- 假如任何一個參與者向協調者反饋了No響應,或者在等待超市之后,協調者尚無法接收到所有參與者的反饋響應,那么就中斷事務。
- 發送回滾請求:協調者向所有參與者節點發出Rollback請求
- 事務回滾:參與者接收到Rollback請求后,會利用其在階段一種記錄的Undo信息執行事物回滾操作,並在完成回滾之后釋放事務執行期間占用的資源。
- 反饋事務回滾結果:參與則在完成事務回滾之后,向協調者發送ACK消息
- 中斷事務:協調者接收到所有參與者反饋的ACk消息后,完成事務中斷;
- 優缺點
優點:盡量保證了數據的強一致性,但也無法完全保證
缺點:
-
同步阻塞問題
每一個參與者都是事務阻塞型的 -
單點故障
由於協調者的重要性,一旦協調者發生故障。參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那么所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。(如果是協調者掛掉,可以重新選舉一個協調者,但是無法解決因為協調者宕機導致的參與者處於阻塞狀態的問題) -
數據不一致
在二階段提交的階段二中,當協調者向參與者發送commit請求之后,發生了局部網絡異常或者在發送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。而在這部分參與者接到commit請求之后就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分布式系統便出現了數據部一致性的現象。 -
二階段無法解決的問題
協調者再發出commit消息之后宕機,而唯一接收到這條消息的參與者同時也宕機了。那么即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。 -
太過保守
任意一個節點失敗就會導致整個事務失敗,沒有完善的容錯機制。
Seata分布式事務方案
Seata 是一款開源的分布式事務解決方案,致力於提供高性能和簡單易用的分布式事務服務。Seata 將為用戶提供了AT、TCC、SAGA 和 XA 事務模式,為用戶打造一站式的分布式解決方案。
Seata術語
TC:事務協調者。維護全局和分支事務的狀態,驅動全局事務提交或回滾。
TM:事務管理器。定義全局事務的范圍:開始全局事務、提交或回滾全局事務
RM:管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
Seata的2PC方案
一階段:業務數據和回滾日志記錄在同一個本地事務中提交,釋放本地鎖和連接資源。
二階段:提交異步化,非常快速地完成。回滾通過一階段的回滾日志進行反向補償。
一階段本地事務提交前,需要確保先拿到全局鎖
。拿不到全局鎖,不能提交本地事務。
拿全局鎖的嘗試被限制在一定范圍內,超出范圍將放棄,並回滾本地事務,釋放本地鎖。
在數據庫本地事務隔離級別讀已提交或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是讀未提交
如果應用在特定場景下,必需要求全局的讀已提交
,目前 Seata 的方式是通過 SELECT FOR UPDATE 語句的代理。
Seata執行流程分析
-
每個RM使用DataSourceProxy鏈接數據路,目的是使用ConnectionProxy,使用數據源和數據代理的目的是在第一階段將undo_log和業務數據放在一個本地事務提交,這樣就保存了只要有業務操作就一定有undo_log
-
在第一階段undo_log中存放了數據修改前后修改后的值,為事務回滾做好准別,所以第一階段完成就已經將分支事務提交了,也就釋放了鎖資源
-
TM開啟全局事務開始,將XID全局事務ID放在事務上下文中,通過feign調用也將XID傳入下游分支事務,每個分支事務將自己的Branch ID 分支事務ID與XID關聯
-
第二階段全局事務提交,TC會通知各分支參與者提交分支事務,在第一階段就已經提交了分支事務,這里各參與者只需要刪除undo_log即可,並且可以異步執行,第二階段很快可以完成
-
如果某一個分支事務異常,第二階段就全局事務回滾操作,TC會通知各分支參與者回滾分支事務,通過XID和Branch-ID找到對應的回滾日志,通過回滾日志生成的反向SQL並執行,以完成分支事務回滾到之前
Seata的實戰案列
https://github.com/seata/seata-samples
3PC提交
三階段提,也叫三階段提交協議,是二階段提交(2PC)的改進版本。
與兩階段提交不同的是,三階段提交有兩個改動點。
-
引入超時機制
同時在協調者和參與者中都引入超時機制。 -
在第一階段和第二階段中插入一個准備階段。
保證了在最后提交階段之前各參與節點的狀態是一致的。
三階段提交就有CanCommit、PreCommit、DoCommit三個階段。
第一階段:can_commit
該階段協調者會去詢問各個參與者是否能夠正常執行事務,參與者根據自身情況回復一個預估值,相對於真正的執行事務,這個過程是輕量的,具體步驟如下:
- 協調者向各個參與者發送事務詢問通知,詢問是否可以執行事務操作,並等待回復
- 各個參與者依據自身狀況回復一個預估值,如果預估自己能夠正常執行事務就返回確定信息,並進入預備狀態,否則返回否定信息
第二階段:pre_commit
本階段協調者會根據第一階段的詢盤結果采取相應操作,詢盤結果主要有三種:
- 所有的參與者都返回確定信息
- 一個或多個參與者返回否定信息
- 協調者等待超時
針對第一種情況,協調者會向所有參與者發送事務執行請求,具體步驟如下:
- 協調者向所有的事務參與者發送事務執行通知
- 參與者收到通知后,執行事務,但不提交
- 參與者將事務執行情況返回給客戶端
在上面的步驟中,如果參與者等待超時,則會中斷事務。 針對第二、三種情況,協調者認為事務無法正常執行,於是向各個參與者發出abort通知,請求退出預備狀態,具體步驟如下:
- 協調者向所有事務參與者發送abort通知
- 參與者收到通知后,中斷事務
第三階段:do_commit
如果第二階段事務未中斷,那么本階段協調者將會依據事務執行返回的結果來決定提交或回滾事務,分為三種情況:
- 所有的參與者都能正常執行事務
- 一個或多個參與者執行事務失敗
- 協調者等待超時
針對第一種情況,協調者向各個參與者發起事務提交請求,具體步驟如下:
- 協調者向所有參與者發送事務commit通知
- 所有參與者在收到通知之后執行commit操作,並釋放占有的資源
- 參與者向協調者反饋事務提交結果
針對第二、三種情況,協調者認為事務無法正常執行,於是向各個參與者發送事務回滾請求,具體步驟如下:
- 協調者向所有參與者發送事務rollback通知
- 所有參與者在收到通知之后執行rollback操作,並釋放占有的資源
- 參與者向協調者反饋事務提交結果
在本階段如果因為協調者或網絡問題,導致參與者遲遲不能收到來自協調者的commit或rollback請求,那么參與者將不會如兩階段提交中那樣陷入阻塞,而是等待超時后繼續commit。相對於兩階段提交雖然降低了同步阻塞,但仍然無法避免數據的不一致性。
在分布式數據庫中,如果期望達到數據的強一致性,那么服務基本沒有可用性可言,這也是為什么許多分布式數據庫提供了跨庫事務,但也只是個擺設的原因,在實際應用中我們更多追求的是數據的弱一致性或最終一致性,為了強一致性而丟棄可用性是不可取的。
TCC分布式事務
TCC是服務化的兩階段編程模型,要求每個分支事務實現三個方法操作:預處理Try,確認Confirm,撤銷Cancel。3個方法操作均由業務編碼實現。
- Try操作做業務檢查及資源預留,
- Confirm做業務確認操作,
- Cancel實現一個與Try相反的操作即回滾操作。
TM首先發起所有的分支事務Try操作,任何一個分支事務的Try操作執行失敗,TM將會發起所有分支事務的Cancel操作,若Try操作全部成功,TM將會發起所有分支事務的Confirm操作,其中Confirm/Cancel操作若執行失敗,TM會進行重試。
TCC的三個階段
-
Try階段
是做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和后續的Confirm一起才能構成一個完整的業務邏輯 -
Confirm階段是做確認提交
Try階段所有分支事務執行成功后開始執行Confirm,通常情況下,采用TCC則認為Confirm階段是不會出錯的,即:只要Try成功,Confirm一定成功,若Confirm階段真的出錯,需要引入重試機制或人工處理 -
Cancel階段是在業務執行錯誤需要回滾到狀態下,執行分支事務的取消,預留資源的釋放,通常情況下,采用TCC則認為Cancel階段也一定是真功的,若Cance階段真的出錯,需要引入重試機制或人工處理
TM事務管理器
可以實現為獨立的服務,也可以讓全局事務發起方充當TM的角色,TM獨立出來是為了公用組件,是為了考慮系統結構和軟件的復用
TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條,用來記錄事務上下文,追蹤和記錄狀態,用於Confirm和cacel失敗需要進行重試,因此需要實現 冪等
TCC的三種異常處理情況
-
冪等處理
- 因為網絡抖動等原因,分布式事務框架可能會重復調用同一個分布式事務中的一個分支事務的二階段接口。所以分支事務的二階段接口Confirm/Cancel需要能夠保證冪等性。如果二階段接口不能保證冪等性,則會產生嚴重的問題,造成資源的重復使用或者重復釋放,進而導致業務故障。
- 對於冪等類型的問題,通常的手段是引入冪等字段進行防重放攻擊。對於分布式事務框架中的冪等問題,同樣可以祭出這一利器。
- 冪等記錄的插入時機是參與者的Try方法,此時的分支事務狀態會被初始化為INIT。然后當二階段的Confirm/Cancel執行時會將其狀態置為CONFIRMED/ROLLBACKED。
- 當TC重復調用二階段接口時,參與者會先獲取事務狀態控制表的對應記錄查看其事務狀態。如果狀態已經為CONFIRMED/ROLLBACKED,那么表示參與者已經處理完其分內之事,不需要再次執行,可以直接返回冪等成功的結果給TC,幫助其推進分布式事務。
-
空回滾
- 當沒有調用參與方Try方法的情況下,就調用了二階段的Cancel方法,Cancel方法需要有辦法識別出此時Try有沒有執行。如果Try還沒執行,表示這個Cancel操作是無效的,即本次Cancel屬於空回滾;如果Try已經執行,那么執行的是正常的回滾邏輯。
- 要應對空回滾的問題,就需要讓參與者在二階段的Cancel方法中有辦法識別到一階段的Try是否已經執行。很顯然,可以繼續利用事務狀態控制表來實現這個功能。
- 當Try方法被成功執行后,會插入一條記錄,標識該分支事務處於INIT狀態。所以后續當二階段的Cancel方法被調用時,可以通過查詢控制表的對應記錄進行判斷。如果記錄存在且狀態為INIT,就表示一階段已成功執行,可以正常執行回滾操作,釋放預留的資源;如果記錄不存在則表示一階段未執行,本次為空回滾,不釋放任何資源。
-
資源懸掛
- 問題:TC回滾事務調用二階段完成空回滾后,一階段執行成功
- 解決:事務狀態控制記錄作為控制手段,二階段發現無記錄時插入記錄,一階段執行時檢查記錄是否存在
TCC和2PC比較
- 2PC通常都是在跨庫的DB層面,而TCC則在應用層面處理,需要通過業務邏輯實現,這種分布式事務的實現方式優勢在於,可以讓應用自己定義數據操作的粒度,使得降低鎖沖突,提高吞吐量成為可能;
- 而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現Try,confirm,cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態,系統故障的不同失敗原因實現不同的回滾策略
Hmily框架實現TCC案例
# 賬戶A
try:
try的冪等效驗
try的懸掛處理
檢查余額是否夠30元
扣減30元
confirm:
空處理即可,通常TCC階段是認為confirm是不會出錯的
cancel:
cancel冪等效驗
cacel空回滾處理
增加可用余額30元,回滾操作
# 賬戶B
try:
空處理即可
confirm:
confirm的冪等效驗
正式增加30元
cancel:
空處理即可
可靠消息最終一致性
可靠消息最終一致性就是保證消息從生產方經過消息中間件傳遞到消費方的一致性。
有一些第三方的MQ是支持事務消息的,比如RocketMQ,他們支持事務消息的方式也是類似於采用的二階段提交,但是市面上一些主流的MQ都是不支持事務消息的,比如 RabbitMQ 和 Kafka 都不支持。
RocketMQ主要解決了兩個功能:
- 本地事務與消息發送的原子性問題。
- 事務參與方接收消息的可靠性。
可靠消息最終一致性事務適合執行周期長且實時性要求不高的場景,引入消息機制后,同步的事務操作變為基於消息執行的異步操作,避免分布式事務中的同步阻塞操作的影響,並實現了兩個服務的解耦。
RocketMQ 思路大致為:
第一階段Prepared消息,會拿到消息的地址。
第二階段執行本地事務。
第三階段通過第一階段拿到的地址去訪問消息,並修改狀態。
也就是說在業務方法內要想消息隊列提交兩次請求,一次發送消息和一次確認消息。
如果確認消息發送失敗了RocketMQ會定期掃描消息集群中的事務消息,這時候發現了Prepared消息,它會向消息發送者確認,所以生產方需要實現一個check接口,RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。
優點: 實現了最終一致性,不需要依賴本地數據庫事務。
缺點: 實現難度大,主流MQ不支持,RocketMQ事務消息部分代碼也未開源,出現坑可能無法處理。
最大努力通知
最大努力通知
與 可靠消息一致性
有什么不同?
-
可靠消息一致性
發起通知方需要保證將消息發出去,並且將消息發送到接收通知方,消息的可靠性由發起通知方保證 -
最大努力通知
發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是消息可能接收不到,此時需要接收通知方主動調用發起通知方的接口查詢業務,通知可靠性關鍵在於接收通知方。 -
兩者的應用場景
- 可靠消息一致性關注的是交易過程的事務一致,以異步的方式完成交易
- 最大努力通知關注的是交易后的通知事務,即將交易結果可靠的通知出去
-
基於MQ的ack機制實現最大努力通知
- 利用MQ的ack機制由MQ向接收通知方發送消息通知,發起方將普通消息發送到MQ
- 接收通知監聽MQ,接收消息,業務處理完成回應ACK
- 接收通知方如果沒有回應ACK則MQ會重復通知,按照時間間隔的方式,逐步拉大通知間隔
- 此方案適用於內部微服務之間的通知,不適應與通知外部平台
分布式事務方案對比分析
-
2PC
最大的一個詬病是一個阻塞協議。RM在執行分支事務后需要等待TM的決定,此時服務會阻塞鎖定資源。由於其阻塞機制和最差時間復雜度高,因此,這種設計不能適應隨着事務涉及的服務數量增加而擴展的需要,很難用於並發較高以及子事務生命周期較長的分布式服務中 -
TCC
如果拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面處理,需要通過業務邏輯來實現。這種分布式事務的優勢在於,可以讓應用自定義數據操作的粒度,使得降低鎖沖突,提高吞吐量成為可能。而不足之處在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現三個操作。此外,其實現難度也比較大,需要按照網絡狀態,系統故障等不同失敗原因實現不同的策略。 -
可靠消息最終一致性事務
適合執行周期長且實時性要求不高的場景。引入消息機制后,同步的事務操作變為基於消息執行的異步操作,避免了分布式事務中的同步阻塞操作的影響,並實現了兩個服務的解耦,典型的場景:注冊送積分,登陸送優惠券等 -
最大努力通知
是分布式事務中要求最低的一種,適用於一些最終一致性時間敏感度低的業務,允許發起通知方業務處理失敗,在接收通知方收到通知后積極進行失敗處理,無論發起通知方如何處理結果都不會影響到接收通知方的后續處理,發起通知方需提供查詢執行情況接口,用於接收通知方校對結果,典型的應用場景:銀行通知,支付結果通知等。
2PC | TCC | 可靠消息 | 最大努力通知 | |
---|---|---|---|---|
一致性 | 強一致性 | 最終一致 | 最終一致 | 最終一致 |
吞吐量 | 低 | 中 | 高 | 高 |
實現復雜度 | 容易 | 難 | 中 | 容易 |