本篇文章我們重點討論關于分布式事務的一些相關知識點。這是學習分布式系統的一個必不可少的技術。我們最常見的案例就是銀行轉賬問題,A賬戶向B賬戶轉100元,那么A賬戶余額要減少100,B賬戶上要增加100。兩個步驟必須都要成功才算成功,只成功一個的話應當回滾掉。如果A和B不在同一個環境或者系統上,這個事務就是分布式事務了,那么在這種情況下,如何保證事務的正確執行,有哪些執行方案呢?
CAP理論和BASE理論
在將分布式事務的開始,我們先從幾個分布式系統方面的基礎知識點說起,首先是大名鼎鼎的CAP理論,CAP三個字母代表了三個單詞,Consistency(一致性),Availability(可用性),Partition Tolerance(分區容忍性)。CAP理論指的是任何一個分布式系統,最多只能保證CAP三項中的兩項。
首先,一致性,它表示所有的數據節點上的數據要保證是一致且正確的。可用性指的是每一個操作總是能夠在一定時間內返回結果即是說每個讀寫操作都是成功的。我們平時經常看到的一些對于系統穩定性的描述都是達到了幾個9,比如3個9就是99.9%,4個9就是99.99%。這就意味著系統只有在極少數情況下才會發生故障或錯誤。分區容忍性指的是當出現部分節點故障時,分布式系統仍然能夠正常運行。
常見的分布式系統應用架構都是AP或者CP,因為如果沒有P那本質上就是個單機應用談不上分布式了。工作中最常用到的幾個注冊中心,ZooKeeper就是基于CP架構的,Eureka是基于AP的。
在工程實踐中,基于CAP理論又演化出一個新的理論-BASE理論。Base代表了三個單詞:Basically Available(基本可用),Soft State(軟狀態)和Eventally Consistent(最終一致性)。它的核心思想是最終一致性,即無法做到強一致性,但我們可以根據實際業務情況,使系統達到最終一致性。
首先,Base理論中的基本可用,就是不追求CAP中的任何時候的讀寫操作都是成功的,但系統能夠保證基本運行。比如我們平時雙十一的時候可能會出現排隊或者失敗的情況,其實就是犧牲了部分可用性來保證系統的穩定。
軟狀態可以對標ACID中的強一致性,ACID中要么全做要么全不做,所有用戶看到的數據都是絕對一致的。但軟狀態是允許出現數據不一致的時刻,相當于是一個中間狀態,幾個數據節點的數據出現了延遲的情況。
最終一致性指的是數據不能一直處于軟狀態的情況,最終還是要達到所有節點數據一致的。這也是我們大部分開發過程中所追求的。
數據一致性模型
數據一致性模型一般分為弱一致性和強一致性,上述的BASE理論是實現的最終一致性其實就是弱一致性,而強一致性有時候也稱為線性一致性,就是每次更新操作后,其他每個進程獲取到的數據都是最新的,這種方式對用戶友好,即用戶之前做了什么操作,下一步就能保證得到什么。但這種方式會犧牲系統的可用性,可以理解為我們實現了CP犧牲了A。
除了強一致性之外的一致性模型都是弱一致性,也就是說系統不承諾更新操作后一定能保證下次能讀取到最新數據,要能正確讀取到這個數據需要等待一段時間,這個時間差稱作“不一致窗口”。
最終一致性是弱一致性的特例,它強調的是所有節點的數據副本,經過一段時間后,一定能達到全部一致。它的不一致窗口的時間主要受通信延遲,系統負載和復制副本的個數影響。其根據不同的保證也可以分為不同模型,包括因果一致性和會話一致性等。
因果一致性要求有因果關系的操作順序得到保證,非因果關系的操作順序則無所謂。
會話一致性將對系統數據的訪問過程框定在了一個會話當中,約定了系統能保證在同一個有效的會話中實現“讀己之所寫”的一致性,就是在你的一次訪問中,執行更新操作之后,客戶端能夠在同一個會話中始終讀取到該數據項的最新值。
那么為了實現數據一致性,我們就自然要回到我們一開始就要說的分布式事務的概念了,它不同于我們平時單機應用上的服務,由于在數據分布在多臺服務器上,我們就不能用傳統的方式來保證事務的正確提交,這就引出了集中針對分布式事務的解決方案。
2PC 兩階段提交
兩階段提交(2PC,Two-phase Commit Protocol)是非常經典的強一致性、中心化的原子提交協議。這個算法包含了兩類角色,協調者(Coordinator)和參與者(Participants)。所謂的兩階段指的是準備階段(Commit-request)和執行階段(Commit)。
在準備階段的時候,協調者通知各個參與者準備提交事務,詢問它們是否接受。參與者們會各自反饋自己的響應,同意或者取消(故障),在這個階段里面,所有參與者都沒有進行commit事務操作。
協調者收到參與者們的反饋結果后,進行決策,如果所有的參與者都表示同意,則要提交事務,否則就是取消。此時通知所有參與者們進行事務提交/取消,參與者們接受到協調者的通知后進行事務操作commit/rollback。
但是兩階段提交存在著一些問題,列舉如下:
- 資源被同步阻塞:在執行過程中,所有參與者都是事務獨占的,也就意味著,當參與者占用公共資源的時候,其他節點訪問公共資源會處于阻塞狀態。
- 協調者可能出現單點故障:協調者作為這個算法中的核心角色,一旦發生故障,參與者會一直阻塞下去。如果在第二階段發生的,所有的參與者還處于鎖定事務資源的狀態,但收不到協調者的決策通知,導致一直鎖定無法繼續完成事務。
- Commit階段出現數據不一致:在第二階段中,協調者發起了commit的通知,但由于網絡等原因導致部分節點收到部分節點每收到通知,會導致沒收到的還處于阻塞狀態,收到的會進行commit操作,從而出現了數據不一致問題。
XA規范
因為講了二階段提交,那么這里也順便提一下我們被問到MySQL時經常會說的XA規范,因為它實際上是實現了二階段提交的。它是由 X/Open 組織提出的分布式事務規范,XA 規范主要定義了事務協調者(Transaction Manager)和資源管理器(Resource Manager)之間的接口。
事務管理器擔任著協調者的角色,它來負責協調和管理事務,提供給AP應用程序編程接口并管理資源管理器。事務管理器向事務指定標識,監視它們的進程,并負責處理事務的完成和失敗。資源管理器,可以理解為一個DBMS系統,或者消息服務器管理系統。應用程序通過資源管理器對資源進行控制,資源必須實現XA定義的接口。資源管理器提供了存儲共享資源的支持。
目前,主流數據庫都提供了對 XA 的支持,在 JMS 規范中,即 Java 消息服務(Java Message Service)中,也基于 XA 定義了對事務的支持。
根據2PC的規范,XA也是將事務分為兩個步驟準備(Prepare)和提交(Commit)階段。準備階段就是TM向RM發送Prepare命令,準備提交,RM執行數據操作后,返回TM結果。TM根據收到的結果,進入提交階段,通知RM進行Commit或者Rollback操作。
在MySQL中,有兩種XA事務,一種是內部XA,一種是外部XA。
在 MySQL 的 InnoDB 存儲引擎中,開啟 binlog 的情況下,MySQL 會同時維護 binlog 日志與 InnoDB 的 redo log,為了保證這兩個日志的一致性,MySQL 使用了 XA 事務,由于是在 MySQL 單機上工作,所以被稱為內部 XA。內部 XA 事務由 binlog 作為協調者,在事務提交時,則需要將提交信息寫入二進制日志,也就是說,binlog 的參與者是 MySQL 本身。
外部 XA 就是典型的分布式事務,MySQL 支持 XA START/END/PREPARE/Commit 這些 SQL 語句,通過使用這些命令,可以完成分布式事務。
3PC 三階段提交
三階段提交(3PC,Three-phase commit)是基于2PC的改進版本,它引入了兩個改動點。
- 引入超時機制,同時在協調者和參與者都加入超時機制。
- 把2PC的第一個階段拆分成了兩步:詢問,然后再鎖資源,最后真正提交。
它的三個階段叫做Can Commit, PreCommit和Do Commit。
CanCommit 階段
和2PC的準備階段很像,就是協調者向參與者發起CanCommit 請求,參與者返回yes或no。
PreCommit階段
協調者根據參與者的反應情況來決定是否可以繼續事務的 PreCommit 操作。根據響應情況,有以下兩種可能。
- 全部返回ok
- 如果全部返回OK的話,就要進行事務預提交,協調者向參與者發送PreCommit請求,參與者接受到后進行事務操作但不提交,如果成功執行了則返回ACK響應。
- 部分返回ok
- 如果部分返回OK可能指的是有部分參與者返回了NO或者是超時后協調者依然未收到參與者的反饋。那么此時進行中斷事務,協調者發送中斷請求給參與者,參與者收到后進行abort中斷。
DoCommit階段
此階段進行真正的提交,跟2PC的最終階段有點相似。如果協調者上一步收到了所有的ACK則會通知參與者進行提交操作,參與者收到后進行提交并反饋協調者ACK。但如果協調者上一步未收到ACK響應,同樣也要執行中斷事務。如果超過超時時間參與者都沒有收到協調者的通知,則自動進行Commit。
很顯然3PC由于協調者和參與者都有了超時機制(2PC只有協調者有),可以保證資源不會因為協調者的故障而一直鎖定,并且由于多了一個PreCommit階段,使得參與者在提交之前的狀態是一致的。但這并不能解決最終可能出現的一致性問題,因為在上面DoCommit階段也有介紹,如果參與者未收到協調者的通知,達到了超時時間后,依然會進行提交,這就出現了數據的不一致性。
TCC(Try-Confirm-Cancel)
TCC(Try-Confirm-Cancel)的概念來源于 Pat Helland 發表的一篇名為“Life beyond Distributed Transactions:an Apostate’s Opinion”的論文。它的三個字母也對應了三個階段:
- Try階段:調用Try接口,嘗試執行業務,完成業務檢查,預留業務資源。
- Confirm階段:確認執行業務操作,不做業務檢查,只使用Try階段預留的業務資源。
- Cancel階段:跟Confirm互斥,兩者只能進入其中一個。在業務執行錯誤的時候進行回滾,執行業務取消,釋放資源。
Try階段失敗可以Cancel,但Confirm/Cancel階段沒有,為了解決此問題,TCC中添加了事務日志,如果Confirm或者Cancle階段出錯,是允許進行重試的,所以這兩個接口需要支持冪等,如果重試依然失敗那就要靠人工介入了。
TCC的特點
通過上面的描述,很顯然,TCC相對于之前的2PC等方式,它關注的是業務層而不是數據庫或者存儲資源層面的事務。它的核心思想是針對每個業務操作,都要添加相應的確認和補償操作,同時把相關的處理從數據庫拿到了業務層,以此實現了跨數據庫的事務。
但正因為它是在業務層進行事務的處理,因此對微服務的侵入性強,業務邏輯的每個分支都要實現Try,Confirm,Cancel三個操作,而且Confirm,Cancel還要實現冪等。另外TCC 的事務管理器要記錄事務日志,也會損耗一定的性能。
在業務中引入 TCC 一般是依賴單獨的 TCC 事務框架,最常見的就是Seata框架了,這個可以自己嘗試去使用體驗下。
基于消息補償的最終一致性
在實際生產工作中,我們還經常會用到一種方案,基于消息補償的方式,它是一種異步事務機制。常見的實現方案有本地消息表、消息隊列等。
本地消息表
首先說下本地消息表,它最初是由 ebay 的工程師提出,核心思想是將分布式事務拆分成本地事務進行處理,通過消息日志的方式來異步執行。所以其實就是利用了各系統本地的事務實現了分布式事務。在本地要創建一張消息表,業務執行的時候也要往這個消息表里面存放一條數據,這樣可以保證存放消息和業務數據都是同時成功或失敗的。成功存放后再進行后續的業務操作,如果成功了則將消息狀態更新為成功。如果失敗了,首先會有個定時任務經常去掃描未成功的任務去執行,如果失敗也是可以重試的,所以要保證業務處理接口的冪等。
所以能看出本地消息表的方式是能保證最終一致性的,但可能出現部分時間的數據不一致。
可靠消息隊列
我們常見的消息隊列中RocketMQ就支持消息事務。所以我這里講下RocketMQ實現分布式事務。這部分我從阿里云的文檔上找的,感興趣的可以自己去看,顯示幾個概念:
- 半事務消息:暫不能投遞的消息,發送方已經成功地將消息發送到了消息隊列RocketMQ版服務端,但是服務端未收到生產者對該消息的二次確認,此時該消息被標記成“暫不能投遞”狀態,處于該種狀態下的消息即半事務消息。
- 消息回查:由于網絡閃斷、生產者應用重啟等原因,導致某條事務消息的二次確認丟失,消息隊列RocketMQ版服務端通過掃描發現某條消息長期處于“半事務消息”時,需要主動向消息生產者詢問該消息的最終狀態(Commit或是Rollback),該詢問過程即消息回查。
交互流程如下
事務消息發送步驟如下:
- 發送方將半事務消息發送至消息隊列RocketMQ版服務端。
- 消息隊列RocketMQ版服務端將消息持久化成功之后,向發送方返回Ack確認消息已經發送成功,此時消息為半事務消息。
- 發送方開始執行本地事務邏輯。
- 發送方根據本地事務執行結果向服務端提交二次確認(Commit或是Rollback),服務端收到Commit狀態則將半事務消息標記為可投遞,訂閱方最終將收到該消息;服務端收到Rollback狀態則刪除半事務消息,訂閱方將不會接受該消息。
事務消息回查步驟如下:
- 在斷網或者是應用重啟的特殊情況下,上述步驟4提交的二次確認最終未到達服務端,經過固定時間后服務端將對該消息發起消息回查。
- 發送方收到消息回查后,需要檢查對應消息的本地事務執行的最終結果。
- 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍按照步驟4對半事務消息進行操作。
盡最大努力通知
和上面的可靠消息隊列方式類似的,還有一種方案叫做盡最大努力通知。它的核心思想是發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。一般也是通過MQ去實現的。
它和基于可靠消息一致的區別如下(摘自某博客):
1、解決方案思想不同
可靠消息一致性,發起通知方需要保證將消息發出去,并且將消息發到接收通知方,消息的可靠性關鍵由發起通知方來保證。 最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能消息接收不到,此時需要接收通知方主動調用發起通知方的接口查詢業務處理結果,通知的可靠性關鍵在接收通知方。
2、兩者的業務應用場景不同
可靠消息一致性關注的是交易過程的事務一致,以異步的方式完成交易。 最大努力通知關注的是交易后的通知事務,即將交易結果可靠的通知出去。
3、技術解決方向不同
可靠消息一致性要解決消息從發出到接收的一致性,即消息發出并且被接收到。 最大努力通知無法保證消息從發出到接收的一致性,只提供消息接收的可靠性機制。可靠機制是,最大努力的將消息通知給接收方,當消息無法被接收方接收時,由接收方主動查詢消費(業務處理結果)。
總結
2PC和3PC都是一種強一致性事務,基于數據庫層面,但也存在一些數據不一致的風險。TCC是一種補償性的事務思想,由于需要在業務層實現,所以對業務侵入大。基于消息補償的方式,TCC還有盡最大努力通知其實都是一種柔性事務,都是保證了最終一致性,允許出現部分時刻數據不一致的情況。
這篇文章有點水,其實就是講了點概念,很多還是摘錄自其他博客和拉勾教育-分布式技術原理與實戰45講這門課程的,就當個讀書筆記看吧,至少擴展了一些對分布式事務的基礎概念的了解。