分布式事務核心概念小結
一、詳解分布式事務基礎概念
1. 什么是CAP理論,為什么不能同時滿足CP或者AP
回答這個問題我們優先需要了解CAP的3個概念:
- C(Consistency):一致性,每次讀取到的數據都是實時最新的數據。
- A(Availability):可用性,每次發起調用都可以得到非錯誤的正常響應。
- P(Partition tolerance):分區容錯性,即分布式系統遇到某節點或者網絡分區故障的時候,系統仍然能夠對外提供一致性或者可用性的服務。
回答這個問題,為什么CAP不能同時滿足,這里我們給出這樣一個場景,有一個訂單服務的集群,提供系統訂單的業務操作,因為業務體量原因采用分布式集群,各自一份庫源,服務之間會不斷進行數據的同步:
以這套集群架構為例可以看到,P是必須可以且必須滿足的條件,即環境因為節點下線或者網絡波動后網絡節點分區故障后系統依然可以對外提供服務:
我們可以通過反證法來印證C一致性或者A可用性是否可以同時存在,假設我們的系統是CP即一致性和分區容錯性都符合,這意味著即使集群環境因為某個節點因為網絡波動后依然可以對外提供正確的服務,但他們必須通過分布式協議將節點間數據同步后保證一致性才能對外提供服務,這意味著服務節點數據沒有完全同步期間,系統不可以對外提供服務(因為數據不一致):
同理假設分布式系統是AP那么,假設集群某節點網絡波動通信出現故障,服務依然可以實時的對外提供服務,那么因為節點間沒有及時的同步就無法保證一致性,也就收達不到C一致性的要求。
2. 什么是分布式BASE理論
BASE是CAP理論的一種延伸,BASE理論也可以按照以下幾個概念組合:
- B(Basically Available):基本可用,即不要求分布式場景在出現故障之后,允許損失部分可用性,只要保證分布式系統服務基本核心可用即可。
- S(Soft State):軟狀態,允許分布式系統執行過程中數據存在中間狀態,例如:初始化、處理中、已完成,有了軟狀態才能保證分布式系統失敗后不斷重試,保證數據最終處理完成。
- E(Eventual Consistency):最終一致性,即分布式系統所有的副本在一段時間后都會達到一致狀態,舉個簡單例子在傳統事務情況下,用戶下單的流程是創建訂單、用戶扣款、庫存扣減3個步驟,在傳統的強一致性的情況下,這三個步驟在單位時間內用戶要么看到沒創建訂單、沒扣款、沒扣減庫存,要么看到創建訂單、用戶扣款、庫存扣減:
而分布式環境因為事務發布在不同系統之間,為了保證執行性能而犧牲操作的原子性,這意味著操作的過程中可能出現臟讀,即軟狀態,例如創建了訂單、扣減了用戶余額、但是查看庫存還沒有完成扣減,但是在過一段時間后,這幾個操作都會完成,這就是最終一致性。
3. 什么是分布式事務
傳統事務即MySQL事務處于單個庫源中,通過數據庫自帶的事務即可保證ACID,當系統庫表處于分布式節點的情況下,需要利用XA協議、TCC事務、最大努力通知、可靠性消息隊列等方式協同多個節點完成分布式事務ACID。
二、分布式事務的幾種方案
1. XA協議下的2PC和3PC
解決分布式事務本質上就是要將分布在不同硬件資源上的事務參與者、支持事務的服務器、事務資源管理器以及資源服務器進行統一協調管理。 我們先來說說基于XA協議的分布式事務解決方案,整體上是有事務協調者和本地資源管理器構成,對應的實現方式分為2PC和3PC。
我們先來說說2PC解決方案,也就是所謂的2階段提交,它分為2個階段:
- 一階段詢問各個分支事務是否準備好,此時分支事務(假設是MySQL)就會準備好相應的redo和undo日志,然后告知事務協調者準備完成。
- 二階段事務協調者會基于各個分支事務準備結果進行進一步全局操作,假設所有分支事務都準備成功那么就通知所有分支事務提交,反之若有單個事務準備失敗則通知所有事務回滾。
2PC嚴格的遵守了事務的ACID的四大原則,同時也相應的導致事務執行期間會出現數據會被鎖定(這也就是我們常說的剛性事務),所以在并發場景之下性能表現會差一些。
同時,2PC在二階段若因為網絡抖動等某些原因,可能會導致某些分支事務無法被提交而導致數據長時間被鎖定,導致業務夯住,進而還會導致一些數據不一致的問題:
基于上述的二階段死鎖問題就有了3PC的解決方案,與2PC步驟不同的是它有三階段,一階段是預提交階段,它會先詢問各個分支事務是否具備事務提交操作的條件,明確都具備之后通知分支事務進行prepare準備這一步的2PC的一階段一致,都是準備undo和redo日志,成功后再進入三階段全局事務提交:
可以看出3PC本質上是想通過一階段的詢問操作視圖避免2PC的死鎖問題,雖然一定程度上可以避免死鎖問題,但在一階段明確正常后,三階段還是可以存在commit的失敗問題,并且為了保證事務最終一致性各個節點又增加了一次網絡IO和系統實現的復雜度,似乎是有點得不償失。
2. AT模式(自動提交模式)
關于AT模式基本概念和實踐可以參考筆者這篇文章:《一步一步教你:用 Docker Compose 完成 Seata 的整合部署》
3. TCC模式
TCC也就是try-confirm-cancel的縮寫,是一種業務入侵性較強的解決方案,可以保證資源的隔離,且性能表現較好,我們以一個下單業務來講解一下TCC的大體執行流程。 整體來說用戶針對單個商品的下單流程為:
- 創建訂單。
- 檢查用戶額度是否足夠,若足夠則扣款。
- 扣減庫存。
在分布式事務的場景下,3個操作是分布式在不同節點上的,所以為保證數據的最終一致性,執行步驟為:
- 事務管理器告知每個事務執行try操作,訂單服務創建預訂單,將狀態設置為預下單狀態,用戶服務鎖定部分額度存入凍結資源表,庫存服務同理,鎖定數量庫存到凍結表。
- 若上述的try服務都執行成功,則將訂單設置為已下單,另外兩個服務都基凍結數據進行扣減這也就是confirm操作,若單個事務失敗則全局回滾,則將凍結表數據清除(也就是cancel操作),一切回到初始的樣子。
需要注意的是因為網絡波動的原因,活動管理器對應的confirm或者cancel通知沒有收到分支事務的確認操作時可能會重復發送,這也就可能導致confirm或者cancel會重復執行,所以我們的業務上要盡可能保證冪等:
TCC相較于XA來說它只需要保證最終一致性,所以是存在中間狀態,不會存在數據鎖,所以性能表現會相對出色一些,但是它卻存在如下幾個問題:
- 業務侵入性較強,相較于XA這種業務層無感知的實現,TCC很明顯會讓開發者感覺到二階段過程,整個分布式事務的數據管理都需要交由開發實現,整體考慮的維度較多,實現相對復雜。
- 空回滾問題:假設我們try操作因為網絡波動導致分支事務延遲執行,而事務管理器卻通知我們的事務進行回滾,此時我們的分支事務就回滾不到任何東西,也就收事務空回滾:
- 空懸掛問題:基于上述場景,此時懸掛的try的操作開始執行,可是業務上我們明明已經回滾,而這份try的數據卻落到庫中,導致部分數據被鎖定,業務上出現不一致問題。
4. saga模式
saga:也比較易于理解,就目前seata的官方文檔的說法,它的特點是:
- 是一種長事務,適用于長、多的業務流程。
- 一種基于補償實現的事務。
- 一階段本地提交、無鎖性能表現好。
就目前seata的實現來說,它是一種基于狀態機引擎實現,不過整體思路也是大差不差:
- 順序執行每個事務,注意這里的每個事務都有相應的補償方法,一個事務執行完成走到下一個事務。
- 順序執行過程中,如果某個事務執行失敗,則基于這個事務往回走反向執行補償方法將數據回滾:
5. 本地消息表
本地消息表也是解決分布式事務的一種有效手段,整體思路是通過本地消息表維護其他事務需要消費的信息,通過定時掃描將消息投遞到消息隊列,讓其他事務完成消息消費從而保證業務最終一致性。
這里我們以下單業務為例講解一下大體流程:
- 訂單服務基于本地數據庫原子性將訂單信息和消息(狀態為待消費)寫入到本地數據庫中。
- 業務定時掃表將數據寫入消息隊列。
- 庫存服務從消息隊列中獲取消息完成庫存扣減。
- 庫存服務通知訂單服務扣減完成,消息可以更新為已完成了。
整體來說本地消息表實現比較簡單但需要考慮一下問題:
- 定時掃表的延遲。
- 本地消息表建議和MQ進行重試次數等工作的協調維護,便于追蹤問題。
- 因為延遲導致消息消費滿。
- 避免消息重復消費,消費者可能需要考慮冪等操作。
- 消費完成后的通知操作可能會失敗,同樣可以借助消息隊列完成這一點。
6. 最大努力通知
最大努力通知模型整體邏輯與前者大體一致,只不過在細節上有所區別:
- 分支事務-1執行本地事務操作,完成后通過RPC、HTTP、MQ同步或者異步發送消息告知其它事務執行自己的邏輯以保證業務的最終一致性。
- 對于消息發送這個過程,發送方會盡自己最大的努力去交付消息,但不可避免某些情況下消息可能無法送達。
- 如果接收方長時間沒有收到消息,則接收方可以主動詢問發送消息的詳情。
- 基于消息內容,接收方完成本地事務操作,實現業務最終一致性。
與本地消息事務不同的是,本地消息消息需要發送方去保證消息發送的可靠性以保證消費方能夠正確消費,以實現業務的最終一致性。
而最大努力通知模型則是要求發送方盡自己的最大的努力去交付(甚至消息是可以重復發送的),并不能保證消息一定能夠送達,所以它允許接收方去主動找發送方查詢消息,也就是說消息的可靠性可能依賴于接收方:
由此,最大努力交付模型的場景,需要考慮的問題可能會更多:
- 消息丟失:最大努力通知模型不保證消息送達,即使發送方發送多次也無法消除該風險。
- 消息冪等
- 數據不一致:因為消息通知不可靠或者消息消費失敗等情況導致數據不一致。
- 主動詢問等系列操作帶來的性能開銷
- 消息表高性能保證:即索引和表結構設計,是否需要結合業務維度進行分庫分表等
- 補償復雜性:針對接受、發送、消費等多個維度的失敗都需要結合問題的場景針對性的措施阻斷并完成補償,這會增加系統的復雜性。
- 不適合關鍵業務。
7. 基于事務消息的分布式事務
《深度解析:基于 RocketMQ 實現分布式事務的技術實踐與原理探究》
三、分布式事務常見問題
1. 有了2階段提交為什么還需要3階段提交
2PC和3PC在上文中都已經做了詳盡的介紹了,我們在這里也針對該問題進行相應的補充,2PC是XA分布式事務中的一個重要解決方案,通過TC一階段通知各個分支事務做好準備(生成undo.log和redo.log)然后根據各個分支事務的提交情況決定二階段是全局提交還是回滾,只有明確所有事務預成功后再進行事務提交。
2PC在特定場景下可能無法保證最終一致性,明確一階段準備成功后某個分支事務沒有收到提交通知或者提交失敗,所以就有了3PC,3PC本質上就是基于2PC基礎上先執行的預提交明確一下各個分支事務是否可以正常執行再決定是否進行后續的分支事務準備和提交工作,由此在一定程度上避免同步阻塞、單點故障、數據不一致等問題。
2. 什么是TCC,和2PC有什么區別
關于TCC上文也已經提到過,它是一種相對而言業務侵入性較強的分布式事務解決方案,通過手動編寫try-confirm-cancel實現每個事務資源預留、confirm提交、rollback回滾邏輯,通過在try階段進行資源預留,其他節點同理,只有所有分布式節點的try邏輯都成功之后統一進行confirm操作,如果有一個失敗的統一cancel。
相較于XA協議的2PC而言,它有著如下幾個不同點:
- XA的2PC是強一致性協議,操作期間會持有資源的鎖,它可以一定程度上避免臟讀問題,TCC是最終一致性協議。
- XA的2PC執行期間存在數據鎖的情況,而TCC則是通過邏輯層面實現資源預留,也就是所謂的補償型事務,它不會鎖數據,性能表現更加出色。
- XA的2PC對于用戶而言整個二階段是無感知的,而TCC可以讓開發明確的感受到各個階段的執行,業務侵入性較強。
- 在實現層面上,TCC需要用戶自行實現資源預留、提交和補償邏輯相較于2PC實現更復雜,而且在功能層面還需要考慮到空懸掛和空回滾的問題,實現成本相較于2PC更高一些。
3. 什么是柔性事務
柔性事務是業內解決分布式事務的常用解決方案,與之相反的就是以MySQL的innodb存儲引擎為例的剛性事務,它要求數據實時的ACID,而柔性事務則是基于BASE理論即保證性能只要保證事務最終一致性,對于ACID原則,它也是的支撐程度為:
- 原子性:遵循。
- 一致性:為保證性能有些解決方案會做出妥協,例如AT模式,它只能保證最終一致性。
- 隔離性:分布式事務中間處理過程在安全的情況下允許看到。
- 持久性:遵循。
4. 如何基于本地消息表實現分布式事務
大體步驟為:
- 執行本地事務。
- 事務執行成功后創建消息到消息表
- 其他節點同步輪詢消息表消費消息
- 失敗后不斷處理
- 完成后更新消息狀態。
5. 什么是最大努力通知?
與本地消息表不同,最大努力通知不保證消息交付到消費者手里,完成本地事務后會創建消息到消息表中,然后通過接口、消息隊列等形式通知消費者消費,但是不保證消費者可以準確收到消息,需要消費者主動去確認,這就可能考慮到以下幾個問題:
- 冪等
- 一致性
- 可靠性
6. 最大努力通知、事務消息、本地消息表三者區別
最大努力通知它所對應的消息發送方放不保證消息可靠性,允許接收方進行主動會查等操作,在發送方層面實現比較簡單,也就是消息沒有成功發送的地方做個重試機制就好了,總的來說它是一種最終一致性的解決方案,仍然是存在數據不一致和消息重復消費的問題。
再來說說事務消息,它要求發送方保證消息發送的可靠性,但是這套方案實現上業務的侵入性比較強,它要求我們的發送方需要做到如下幾點:
- 發送的消息必須是half消息,即必須MQ回復ack之后才能才能消息發送成功。
- 在接收方明確ACK后,發送方必須執行本地事務并提交執行成功發送消息的通知,如果接收方長時間沒有收到該確認信號需要發送方提供一個回查接口詢問該情況。
- 接收方需要保證消息消費的冪等判斷以避免消息重復消費。
總的來說,MQ這套事務消息方案業務入侵性較強,實現相對復雜一些,但是整體可靠性相較于前者會高一些。
本地消息表相較于上述來說做了較好的折中,業務入侵性不算很高,做好消息表設計然后定時掃表準確投遞到MQ中,然后提供一個消費者完成消費后的通知接口即可。 它也是同樣要求發送方保證消息可靠性,但針對消息表的維度我們還是需要做好表結構設計和索引調優,并要的情況下需要考慮一下分庫分表,所以在明確MQ不支持事務消息的情況下,我們可以考慮用這套方案。
7. TCC的空回滾和懸掛問題和解決方案
空懸掛和空回滾問題上文中都已經做了詳細的介紹,大體來說就是全局事務協調者執行confirm/cancel操作時沒有感知到try操作的狀態,導致時許錯誤出現的不一致問題,對應的我們可以在這幾個階段執行期間通過一張表(如下tcc_transaction_status)維護各個分布式事務的狀態。
CREATE TABLE tcc_transaction_status (
tx_id varchar(128) NOT NULL COMMENT '事務id',
state int(1) DEFAULT NULL COMMENT'事務狀態,0:try,1:confirm,2:cancel' ,
PRIMARY KEY(tx_id) u
)
有了這張表,遇到空回滾時我們也能夠記錄當前回滾狀態標識回滾,后續即使出現懸掛的try邏輯也能基于此表感知到回滾則不執行:
8. TCC中,Confirm或者Cancel失敗了如何解決
- 可以將需要執行的邏輯封裝成消息投遞到MQ等工具進行重試,并設置重試上限避免無線重試。
- 監控模塊告警。
- 人工結合業務數據進行人工干預補償。
9. TCC是強一致性還是最終一致性
在理想情況下,分布式節點能夠正確執行try、confirm/cancel邏輯,系統看到的數據只有以下3種情況:
- 分布式事務執行前。
- 資源預留后的事務數據(和1看到的一樣,只不過其他業務進行資源預留時可能會失敗)
- 所有事務一并confirm/cancel后的結果。
所以按照理想情況的維度考慮,它是可以認為是強一致性。但是由于網絡波動或者延遲或者執行失敗等原因,TCC并不能做到各個節點操作上的一致性,所以一般情況下我們認為TCC是最終一致性。
10. seata的四種事務模式的適用場景
- XA:不考慮性能的強一致性
- AT:不支持ACID或對性能有要求的分布式事務
- TCC:對性能有較高要求,且業務場景比較單一的的場景。
- SAGA:適用于業務流程長、多的分布式事務場景。
11. Seata的AT模式和XA有什么區別
AT是軟狀態的,存在臟讀情況,但是可以保證最終一致性不會出現行鎖,可以保證最終一致性。 XA是強一致性的所以在事務執行期間,查詢操作會被鎖住,性能表現差,可以保證強一致性,依賴于數據的ACID。
12. 什么是事務消息,為什么需要事務消息
我們假設不采用MQ的half消息,用消息隊列落地分布式事務時,對應過程為:
- 分支事務執行自己的SQL操作。
- 執行成功后將消息投遞到消息隊列讓分支事務2執行。
- 分支事務2消費消息成功,實現數據最終一致性。
基于該方案,讀者可以看看是否可以解決該問題:
- 分支事務-1投遞消息到MQ,長時間沒有收到MQ回復(但是MQ收到了),分支事務-1回滾,分支事務-2執行成功,數據不一致問題。
- 分支事務-1提交消息,但是消息隊列因為某些原因出現消息丟失,導致分支事務-2未能執行本地事務,出現數據不一致怎么辦?
于是就有了事務消息,我們不妨基于事務消息的維度看看它是如何解決上述問題的:
- 針對問題1,事務消息在發送half階段明確要求消息隊列回復ACK后才執行本地事務,而不是非事務消息模式的先執行事務再提交消息。
- 針對問題2,即使commit消息丟失,分支事務也提供回查接口告知本地事務狀態,讓MQ將half消息發送出去。
13. seata AT模式存在什么問題
盡管seata的AT模式用lock_table解決了臟寫問題,但它還是存在非seata管控范圍的臟讀問題,我們都知道seata的AT模式是最終一致性的,這這意味著的一階段提交期間,當前分支事務的數據并不是最終結果,以下單結果為例,我們的下單流程為:
- 訂單服務創建訂單。
- 賬戶服務扣款。
- 庫存服務發貨。
假如一階段庫存服務完成一階段事務準備,此時我們其他非seata管控的事務查到的用戶余額由1000變為900,假設此時我們的業務需要基于這個數值進行全額轉賬執行的操作是:
- 將當前用戶額度變為0
- 其他用戶變為900
結果完成該操作后,seata因為其他分支事務失敗導致數據回滾,結果當前用戶余額變為1000,這就是嚴重的臟讀導致的問題: