背景回複"高效Java"領取《Effective Java第三版》

《分布式事務科普》是我在YiQing期間整理的一篇科普型文章,内容共計兩萬五千字左右,應該算是涵蓋了這個領域的大多數知識點。篇幅較長,遂分為上下兩篇發出。上篇為《分布式事務科普——初識篇》:ACID、事務隔離級别、MySQL事務實作原理、CAP、BASE、2PC、3PC等(昨天已經發出,有需要的同學可以跳轉)。下篇為《分布式事務科普——終結篇》,詳細講解分布式事務的解決方案:XA、AT、TCC、Saga、本地消息表、消息事務、最大努力通知等。
分布式事務科普
随着業務的快速發展、業務複雜度越來越高,傳統單體應用逐漸暴露出了一些問題,例如開發效率低、可維護性差、架構擴充性差、部署不靈活、健壯性差等等。而微服務架構是将單個服務拆分成一系列小服務,且這些小服務都擁有獨立的程序,彼此獨立,很好地解決了傳統單體應用的上述問題,但是在微服務架構下如何保證事務的一緻性呢?本文首先從事務的概念出來,帶大家先回顧一下ACID、事務隔離級别、CAP、BASE、2PC、3PC等基本理論(參考上篇《分布式事務科普——初識篇》),然後再詳細講解分布式事務的解決方案:XA、AT、TCC、Saga、本地消息表、消息事務、最大努力通知等。
在引入分布式事務前,我們最好先明确一下我們是否真的需要分布式事務。有可能因為過度設計緻使微服務過多,進而不得不引入分布式事務,這個時候就不建議你采用下面的任何一種方案,而是把需要事務的微服務聚合成一個單機服務,使用資料庫的本地事務。因為不論任何一種方案都會增加你系統的複雜度,這樣的成本實在是太高了,千萬不要因為追求某些設計,而引入不必要的成本和複雜度。
常見的分布式事務方案有:XA、AT、TCC、Saga、本地消息表、MQ消息事務、最大努力通知等。
X/Open,即現在的open group,是一個獨立的組織,主要負責制定各種行業技術标準。官網位址:http://www.opengroup.org/。X/Open組織主要由各大知名公司或者廠商進行支援,這些組織不光遵循X/Open組織定義的行業技術标準,也參與到标準的制定。
DTP全稱是Distributed Transaction Process,即分布式事務模型。在DTP本地模型執行個體中包含3個部分:AP、TM和RM,如下圖所示。其中,AP 可以和TM 以及 RM 通信,TM 和 RM 互相之間可以通信。
AP(Application Program,應用程式):AP定義事務邊界(定義事務開始和結束)并通路事務邊界内的資源。
RM(Resource Manager,資料總管):RM管理着某些共享資源的自治域,比如說一個MySQL資料庫執行個體。在DTP裡面還有兩個要求,一是RM自身必須是支援事務的,二是RM能夠根據全局(分布式)事務辨別(GTID之類的)定位到自己内部的對應事務。
TM(Transaction Manager,事務管理器):TM能與AP和RM直接通信,協調AP和RM來實作分布式事務的完整性。負責管理全局事務,配置設定全局事務辨別,監控事務的執行進度,并負責事務的送出、復原、失敗恢複等。
AP和RM之間則通過RM提供的Native API 進行資源控制,這個沒有進行約API和規範,各個廠商自己實作自己的資源控制,比如Oracle自己的資料庫驅動程式。
DTP模型裡面定義了XA接口,TM 和 RM 通過XA接口進行雙向通信(這也是XA的主要作用, 除此之外,XA還對兩階段送出協定進行了部分優化),例如:TM通知RM送出事務或者復原事務,RM把送出結果通知給TM。XA 的全稱是eXtended Architecture,它是一個分布式事務協定,它通過二階段送出協定保證強一緻性。
其過程大緻如下:
第一階段:TM請求所有RM進行準備,并告知它們各自需要做的局部事務(Transaction Branch)。RM收到請求後,如果判斷可以完成自己的局部事務,那就持久化局部事務的工作内容,再給TM肯定答複;要是發生了其他情況,那給TM的都是否定答複。在發送了否定答複并復原了局部事務之後,RM才能丢棄持久化了的局部事務資訊。
第二階段:TM根據情況(比如說所有RM Prepare成功,或者,AP通知它要Rollback等),先持久化它對這個全局事務的處理決定和所涉及的RM清單,然後通知所有涉及的RM去送出或者復原它們的局部事務。RM們處理完自己的局部事務後,将傳回值告訴TM之後,TM才可以清除掉包括剛才持久化的處理決定和RM清單在内的這個全局事務的資訊。
基于XA協定實作的分布式事務是強一緻性的分布式事務,典型應用場景如JAVA中有關分布式事務的規範如JTA(Java Transaction API)和JTS(Java Transaction Service)中就涉及到了XA。
XA 協定通常實作在資料庫資源層,直接作用于資料總管上。是以,基于 XA 協定實作的分布式事務産品,無論是分布式資料庫還是分布式事務架構,對業務幾乎都沒有侵入,就像使用普通資料庫一樣。
不過XA的使用并不廣泛,究其原因主要有以下幾類:
性能,如:阻塞性協定,增加響應時間、鎖時間、死鎖等因素的存在,在高并發場景下并不适用。
支援程度,并不是所有的資源都支援XA協定;在資料庫中支援完善度也有待考驗,比如MySQL 5.7之前都有缺陷(MySQL 5.0版本開始支援XA,隻有當隔離級别為SERIALIZABLE的時候才能使用分布式事務)。
運維複雜。
AT(Automatic Transaction)模式是基于XA事務演進而來,核心是對業務無侵入,是一種改進後的兩階段送出,需要資料庫支援。AT最早出現在阿裡巴巴開源的分布式事務架構Seata中,我們不妨先簡單了解下Seata。
Seata(Simple Extensible Autonomous Transaction Architecture,一站式分布式事務解決方案)是 2019 年 1 月份螞蟻金服和阿裡巴巴共同開源的分布式事務解決方案。Seata 的設計思路是将一個分布式事務可以了解成一個全局事務,下面挂了若幹個分支事務,而一個分支事務是一個滿足 ACID 的本地事務,是以我們可以操作分布式事務像操作本地事務一樣。
Seata 内部定義了 3個子產品來處理全局事務和分支事務的關系和處理過程,如上圖所示,分别是 TM、RM 和 TC。其中 TM 和 RM 是作為 Seata 的用戶端與業務系統內建在一起,TC 作為 Seata 的服務端獨立部署。 Transaction Coordinator(TC):事務協調器,維護全局事務的運作狀态,負責協調并驅動全局事務的送出或復原。 Transaction Manager(TM):控制全局事務的邊界,負責開啟一個全局事務,并最終發起全局送出或全局復原的決議。 Resource Manager(RM):控制分支事務,負責分支注冊、狀态彙報,并接收事務協調器的指令,驅動分支(本地)事務的送出和復原
Transaction Coordinator(TC):事務協調器,維護全局事務的運作狀态,負責協調并驅動全局事務的送出或復原。
Transaction Manager(TM):控制全局事務的邊界,負責開啟一個全局事務,并最終發起全局送出或全局復原的決議。
Resource Manager(RM):控制分支事務,負責分支注冊、狀态彙報,并接收事務協調器的指令,驅動分支(本地)事務的送出和復原。
參照上圖,簡要概括整個事務的處理流程為:
TM 向 TC 申請開啟一個全局事務,TC 建立全局事務後傳回全局唯一的 XID,XID 會在全局事務的上下文中傳播;
RM 向 TC 注冊分支事務,該分支事務歸屬于擁有相同 XID 的全局事務;
TM要求TC送出或復原XID的相應全局事務。
TC在XID的相應全局事務下驅動所有分支事務以完成分支送出或復原。
Seata 會有 4 種分布式事務解決方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。這個小節我們主要來講述一下AT模式的實作方式,TCC和Saga模式在後面會繼續介紹。
Seata 的事務送出方式跟 XA 協定的兩段式送出在總體上來說基本是一緻的,那它們之間有什麼不同呢?
我們都知道 XA 協定它依賴的是資料庫層面來保障事務的一緻性,也即是說 XA 的各個分支事務是在資料庫層面上驅動的,由于 XA 的各個分支事務需要有 XA 的驅動程式,一方面會導緻資料庫與 XA 驅動耦合,另一方面它會導緻各個分支的事務資源鎖定周期長,這也是它沒有在網際網路公司流行的重要因素。
基于 XA 協定以上的問題,Seata 另辟蹊徑,既然在依賴資料庫層會導緻這麼多問題,那我們就從應用層做手腳,這還得從 Seata 的 RM 子產品說起,前面也說過 RM 的主要作用了,其實 RM 在内部做了對資料庫操作的代理層。如上圖所示,在使用 Seata 時,我們使用的資料源實際上用的是 Seata 自帶的資料源代理 DataSourceProxy,Seata 在這層代理中加入了很多邏輯,主要是解析 SQL,把業務資料在更新前後的資料鏡像組織成復原日志,并将 undo log 日志插入 undo_log 表中,保證每條更新資料的業務 SQL都有對應的復原日志存在。
這樣做的好處就是,本地事務執行完可以立即釋放本地事務鎖定的資源,然後向 TC 上報分支狀态。當 TM 決議全局送出時,就不需要同步協調處理了,TC 會異步排程各個 RM 分支事務删除對應的 undo log 日志即可,這個步驟非常快速地可以完成;當 TM 決議全局復原時,RM 收到 TC 發送的復原請求,RM 通過 XID 找到對應的 undo log 復原日志,然後執行復原日志完成復原操作。
如上圖(左),XA 方案的 RM 是放在資料庫層的,它依賴了資料庫的 XA 驅動程式。而上圖(右),Seata 的 RM 實際上是已中間件的形式放在應用層,不用依賴資料庫對協定的支援,完全剝離了分布式事務方案對資料庫在協定支援上的要求。
AT模式下是如何做到對業務無侵入,又是如何執行送出和復原的呢?
第一階段
參照下圖,Seata 的 JDBC 資料源代理通過對業務 SQL 的解析,把業務資料在更新前後的資料鏡像組織成復原日志(undo log),利用本地事務的 ACID 特性,将業務資料的更新和復原日志的寫入在同一個本地事務中送出。這樣可以保證任何送出的業務資料的更新一定有相應的復原日志存在,最後對分支事務狀态向 TC 進行上報。基于這樣的機制,分支的本地事務便可以在全局事務的第一階段送出,馬上釋放本地事務鎖定的資源。
第二階段
如果決議是全局送出,此時分支事務此時已經完成送出,不需要同步協調處理(隻需要異步清理復原日志),第二階段可以非常快速地結束,參考下圖。
如果決議是全局復原,RM收到協調器發來的復原請求,通過XID和Branch ID找到相應的復原日志記錄,通過復原記錄生成反向的更新SQL并執行,以完成分支的復原,參考下圖。
講到這裡,關于AT模式大部分問題我們應該都清楚了,但總結起來,核心也隻解決了一件事情,就是ACID中最基本、最重要的 A(原子性)。但是,光解決A顯然是不夠的:既然本地事務已經送出,那麼如果資料在全局事務結束前被修改了,復原時怎麼處理?ACID 的 I(隔離性)在Seata的AT模式是如何處理的呢?
Seata AT 模式引入全局鎖機制來實作隔離。全局鎖是由 Seata 的 TC 維護的,事務中涉及的資料的鎖。
參考官網(https://seata.io/en-us/docs/overview/what-is-seata.html)的資料,寫隔離的要領如下:
第一階段本地事務送出前,需要確定先拿到全局鎖 。
拿不到全局鎖,不能送出本地事務。
拿全局鎖的嘗試被限制在一定範圍内,超出範圍将放棄,并復原本地事務,釋放本地鎖。
以一個示例來說明。兩個全局事務tx1和tx2,分别對a表的m字段進行更新操作,m的初始值1000。tx1先開始,開啟本地事務拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務送出前,先拿到該記錄的全局鎖,本地送出釋放本地鎖。tx2後開始,開啟本地事務拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務送出前,嘗試拿該記錄的全局鎖,tx1全局送出前,該記錄的全局鎖被 tx1持有,tx2需要重試等待全局鎖 。
tx1 第二階段全局送出,釋放全局鎖 。tx2拿到全局鎖送出本地事務。
如果tx1的第二階段全局復原,則tx1需要重新擷取該資料的本地鎖,進行反向補償的更新操作,實作分支的復原。
參考下圖,此時如果tx2仍在等待該資料的全局鎖,同時持有本地鎖,則tx1的分支復原會失敗。分支的復原會一直重試,直到tx2的全局鎖等鎖逾時,放棄全局鎖并復原本地事務釋放本地鎖,tx1 的分支復原最終成功。
因為整個過程全局鎖在tx1結束前一直是被tx1持有的,是以不會發生髒寫的問題。
在資料庫本地事務隔離級别為讀已送出(READ COMMITTED)或以上的基礎上,Seata(AT模式)的預設全局隔離級别是讀未送出(READ UNCOMMITTED)。如果應用在特定場景下,必需要求全局的讀已送出,目前Seata的方式是通過SELECT FOR UPDATE語句的代理。
SELECT FOR UPDATE語句的執行會申請全局鎖 ,如果全局鎖被其他事務持有,則釋放本地鎖(復原SELECT FOR UPDATE語句的本地執行)并重試。這個過程中,查詢是被阻塞 住的,直到全局鎖拿到,即讀取的相關資料是已送出的,才傳回。
全局鎖是由 TC 也就是服務端來集中維護,而不是在資料庫維護的。這樣做有兩點好處:一方面,鎖的釋放非常快,尤其是在全局送出的情況下收到全局送出的請求,鎖馬上就釋放掉了,不需要與 RM 或資料庫進行一輪互動;另外一方面,因為鎖不是資料庫維護的,從資料庫層面看資料沒有鎖定。這也就是給極端情況下,業務降級提供了友善,事務協調器異常導緻的一部分異常事務,不會阻塞後面業務的繼續進行。
AT模式基于本地事務的特性,通過攔截并解析 SQL 的方式,記錄自定義的復原日志,進而打破 XA 協定阻塞性的制約,在一緻性、性能、易用性三個方面取得一定的平衡:在達到确定一緻性(非最終一緻)的前提下,即保障一定的性能,又能完全不侵入業務。在很多應用場景下,Seata的AT模式都能很好地發揮作用,把應用的分布式事務支援成本降到極低的水準。
不過AT模式也并非銀彈,在使用之前最好權衡好以下幾個方面:
隔離性。隔離性不高,目前隻能支援到接近讀已送出的程度,更高的隔離級别,實作成本将非常高。
性能損耗。一條Update的SQL,則需要全局事務XID擷取(與TC通訊)、before image(解析SQL,查詢一次資料庫)、after image(查詢一次資料庫)、insert undo log(寫一次資料庫)、before commit(與TC通訊,判斷鎖沖突),這些操作都需要一次遠端通訊RPC,而且是同步的。另外undo log寫入時blob字段的插入性能也是不高的。每條寫SQL都會增加這麼多開銷,粗略估計會增加5倍響應時間(二階段雖然是異步的,但其實也會占用系統資源,網絡、線程、資料庫)。
全局鎖。Seata在每個分支事務中會攜帶對應的鎖資訊,在before commit階段會依次擷取鎖(因為需要将所有SQL執行完才能拿到所有鎖資訊,是以放在commit前判斷)。相比XA,Seata 雖然在一階段成功後會釋放資料庫鎖,但一階段在commit前全局鎖的判定也拉長了對資料鎖的占有時間,這個開銷比XA的prepare低多少需要根據實際業務場景進行測試。全局鎖的引入實作了隔離性,但帶來的問題就是阻塞,降低并發性,尤其是熱點資料,這個問題會更加嚴重。Seata在復原時,需要先删除各節點的undo log,然後才能釋放TC記憶體中的鎖,是以如果第二階段是復原,釋放鎖的時間會更長。Seata的引入全局鎖會額外增加死鎖的風險,但如果實作死鎖,會不斷進行重試,最後靠等待全局鎖逾時,這種方式并不優雅,也延長了對資料庫鎖的占有時間。
關于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。在該論文中,TCC還是以Tentative-Confirmation-Cancellation命名。正式以Try-Confirm-Cancel作為名稱的是Atomikos公司,其注冊了TCC商标。
TCC分布式事務模型相對于 XA 等傳統模型,其特征在于它不依賴資料總管(RM)對分布式事務的支援,而是通過對業務邏輯的分解來實作分布式事務。
TCC 模型認為對于業務系統中一個特定的業務邏輯,其對外提供服務時必須接受一些不确定性,即對業務邏輯初步操作的調用僅是一個臨時性操作,調用它的主業務服務保留了後續的取消權。如果主業務服務認為全局事務應該復原,它會要求取消之前的臨時性操作,這就對應從業務服務的取消操作。而當主業務服務認為全局事務應該送出時,它會放棄之前臨時性操作的取消權,這對應從業務服務的确認操作。每一個初步操作,最終都會被确認或取消。
是以,針對一個具體的業務服務,TCC 分布式事務模型需要業務系統提供三段業務邏輯:
Try:完成所有業務檢查,預留必須的業務資源。
Confirm:真正執行的業務邏輯,不作任何業務檢查,隻使用 Try 階段預留的業務資源。是以,隻要Try操作成功,Confirm必須能成功。另外,Confirm操作需滿足幂等性,保證分布式事務有且隻能成功一次。
Cancel:釋放 Try 階段預留的業務資源。同樣的,Cancel 操作也需要滿足幂等性。
TCC分布式事務模型包括三部分:
主業務服務(Main Server):主業務服務為整個業務活動的發起方、服務的編排者,負責發起并完成整個業務活動。
從業務服務(Service):從業務服務是整個業務活動的參與方,負責提供TCC業務操作,實作Try、Confirm、Cancel三個接口,供主業務服務調用。
事務管理器(Transaction Manager):事務管理器管理控制整個業務活動,包括記錄維護TCC全局事務的事務狀态和每個從業務服務的子事務狀态,并在業務活動送出時調用所有從業務服務的Confirm操作,在業務活動取消時調用所有從業務服務的Cancel操作。
上圖所展示的是TCC事務模型與DTP事務模型的對比圖,看上去這兩者差别很大。聰明的讀者應該可以從圖中的着色上猜出些端倪,其實這兩者基本一緻:TCC模型中的主業務服務相當于DTP模型中AP,從業務服務相當于DTP模型中的RM,兩者也都有一個事務管理器;TCC模型中從業務伺服器所提供的Try/Commit/Cancel接口相當于DTP模型中RM提供的Prepare/Commit/Rollback接口。
所不同的是DTP模型中Prepare/Commit/Rollback都是由事務管理器調用,TCC模型中的Try接口是由主業務服務調用的,二階段的Commit/Cancel才是由事務管理器調用。這就是TCC事務模型的二階段異步化功能,從業務服務的第一階段執行成功,主業務服務就可以送出完成,然後再由事務管理器架構異步的執行各從業務服務的第二階段。這裡犧牲了一定的隔離性和一緻性的,但是提高了長事務的可用性。
下面我們再來了解一下一個完整的TCC分布式事務流程:
主業務服務首先開啟本地事務。
主業務服務向事務管理器申請啟動分布式事務主業務活動。
然後針對要調用的從業務服務,主業務活動先向事務管理器注冊從業務活動,然後調用從業務服務的 Try 接口。
當所有從業務服務的 Try 接口調用成功,主業務服務送出本地事務;若調用失敗,主業務服務復原本地事務。
若主業務服務送出本地事務,則TCC模型分别調用所有從業務服務的Confirm接口;若主業務服務復原本地事務,則分别調用 Cancel 接口;
所有從業務服務的Confirm或Cancel操作完成後,全局事務結束。
使用者接入TCC,最重要的是考慮如何将自己的業務模型拆成兩階段來實作。下面,我們從一個簡答的例子來熟悉一下TCC的具體用法。
以“扣錢”場景為例,在接入TCC前,對A賬戶的扣錢,隻需一條更新賬戶餘額的 SQL 便能完成;但是在接入TCC之後,使用者就需要考慮如何将原來一步就能完成的扣錢操作拆成兩階段,實作成三個方法,并且保證Try成功Confirm一定能成功。
如下圖所示,一階段Try方法需要做資源的檢查和預留。在扣錢場景下,Try要做的事情是就是檢查賬戶餘額是否充足,預留轉賬資金,預留的方式就是當機A賬戶的轉賬資金。Try方法執行之後,賬号A餘額雖然還是100,但是其中30元已經被當機了,不能被其他事務使用。
二階段Confirm執行真正的扣錢操作。Confirm會使用Try階段當機的資金,執行賬号扣款。Confirm執行之後,賬号A在一階段中當機的30元已經被扣除,賬号A餘額變成 70 元 。
如果二階段是復原的話,就需要在Cancel方法内釋放一階段Try當機的30元,使賬号A的回到初始狀态,100元全部可用。
在TCC模型中,事務的隔離交給業務邏輯來實作。其隔離性思想就是通過業務的改造,在第一階段結束之後,從底層資料庫資源層面的加鎖過渡為上層業務層面的加鎖,進而釋放底層資料庫鎖資源,放寬分布式事務鎖協定,将鎖的粒度降到最低,以最大限度提高業務并發性能。
以上面的例子舉例,賬戶A上有100元,事務tx1要扣除其中的30元,事務tx2也要扣除30元,出現并發。在第一階段的Try操作中,需要先利用資料庫資源層面的加鎖,檢查賬戶可用餘額,如果餘額充足,則預留業務資源,扣除本次交易金額。一階段結束後,雖然資料庫層面資源鎖被釋放了,但這筆資金被業務隔離,不允許除本事務之外的其它并發事務動用。
TCC第一階段的Try或者第二階段的Confirm/Cancel在執行過程中,一般都會開啟各自的本地事務,來保證方法内部業務邏輯的ACID特性。這裡Confirm/Cancel執行的本地事務是補償性事務。
補償性事務是一個獨立的支援ACID特性的本地事務,用于在邏輯上取消服務提供者上一個ACID事務造成的影響,對于一個長事務(long-running transaction),與其實作一個巨大的分布式ACID事務,不如使用基于補償性的方案,把每一次服務調用當做一個較短的本地ACID事務來處理,執行完就立即送出。
TCC第二階段Confirm/Cancel執行的補償性事務用于取消Try階段本地事務造成的影響。因為第一階段Try隻是預留資源,之後必須要明确的告訴服務提供者,這個資源到底要還需不需要。下一節中所要講述的Saga也是一種補償性的事務。
在有了一套完備的 TCC 接口之後,是不是就真的高枕無憂了呢?答案是否定的。在微服務架構下,很有可能出現網絡逾時、重發,機器當機等一系列的異常情況。一旦遇到這些 情況,就會導緻我們的分布式事務執行過程出現異常,最常見的主要是空復原、幂等、懸挂。是以,在TCC接口設計中還需要處理好這三個問題。
Cancel接口設計時需要允許空復原。在Try接口因為丢包時沒有收到,事務管理器會觸發復原,這時會觸發Cancel接口,這時Cancel執行時發現沒有對應的事務 XID或主鍵時,需要傳回復原成功。讓事務服務管理器認為已復原,否則會不斷重試,而Cancel又沒有對應的業務資料可以進行復原。
幂等性的意思是對同一個系統使用同樣的條件,一次請求和重複的多次請求對系統資源的影響是一緻的。因為網絡抖動或擁堵可能會逾時,事務管理器會對資源進行重試操作,是以很可能一個業務操作會被重複調用,為了不因為重複調用而多次占用資源,需要對服務設計時進行幂等控制,通常我們可以用事務XID或業務主鍵判重來控制。
懸挂的意思是Cancel比Try接口先執行,出現的原因是Try由于網絡擁堵而逾時,事務管理器生成復原,觸發Cancel接口,而最終又收到了Try接口調用,但是Cancel比Try先到。按照前面允許空復原的邏輯,復原會傳回成功,事務管理器認為事務已復原成功,則此時的Try接口不應該執行,否則會産生資料不一緻,是以我們在Cancel空復原傳回成功之前先記錄該條事務 XID或業務主鍵,辨別這條記錄已經復原過,Try接口先檢查這條事務XID或業務主鍵如果已經标記為復原成功過,則不執行Try的業務操作。
XA兩階段送出是資源層面的,而TCC實際上把資源層面二階段送出上提到了業務層面來實作,有效了的避免了XA兩階段送出占用資源鎖時間過長導緻的性能低下問題。TCC也沒有AT模式中的全局行鎖,是以性能也會比AT模式高很多。不過,TCC模式對業務代碼有很大的侵入性,主業務服務和從業務服務都需要進行改造,從業務方改造成本更高。
Saga 算法(https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf)于 1987 年提出,是一種異步的分布式事務解決方案。其理論基礎在于,其假設所有事件按照順序推進,總能達到系統的最終一緻性,是以 Saga需要服務分别定義送出接口以及補償接口,當某個事務分支失敗時,調用其它的分支的補償接口來進行復原。
在Saga模式下,分布式事務内有多個參與者,每一個參與者都是一個沖正補償服務,需要使用者根據業務場景實作其正向操作和逆向復原操作。
分布式事務執行過程中,依次執行各參與者的正向操作,如果所有正向操作均執行成功,那麼分布式事務送出。如果任何一個正向操作執行失敗,那麼分布式事務會去退回去執行前面各參與者的逆向復原操作,復原已送出的參與者,使分布式事務回到初始狀态。
Saga模式下分布式事務通常是由事件驅動的,各個參與者之間是異步執行的,Saga 模式是一種長事務解決方案。
Saga模式不保證事務的隔離性,在極端情況下可能出現髒寫。比如在分布式事務未送出的情況下,前一個服務的資料被修改了,而後面的服務發生了異常需要進行復原,可能由于前面服務的資料被修改後無法進行補償操作。一種處理辦法可以是“重試”繼續往前完成這個分布式事務。由于整個業務流程是由狀态機編排的,即使是事後恢複也可以繼續往前重試。是以使用者可以根據業務特點配置該流程的事務處理政策是優先“復原”還是“重試”,當事務逾時的時候,服務端會根據這個政策不斷進行重試。
由于Saga不保證隔離性,是以我們在業務設計的時候需要做到“甯可長款,不可短款”的原則,長款是指在出現差錯的時候站在我方的角度錢多了的情況,錢少了則是短款,因為如果長款可以給客戶退款,而短款則可能錢追不回來了,也就是說在業務設計的時候,一定是先扣客戶帳再入帳,如果因為隔離性問題造成覆寫更新,也不會出現錢少了的情況。
Saga模式适用于業務流程長且需要保證事務最終一緻性的業務系統,Saga模式一階段就會送出本地事務,無鎖、長流程情況下可以保證性能。事務參與者可能是其它公司的服務或者是遺留系統的服務,無法進行改造和提供TCC要求的接口,也可以使用Saga模式。
Saga模式所具備的優勢有:一階段送出本地資料庫事務,無鎖,高性能;參與者可以采用事務驅動異步執行,高吞吐;補償服務即正向服務的“反向”,易于了解、易于實作;不過,Saga 模式由于一階段已經送出本地資料庫事務,且沒有進行“預留”動作,是以不能保證隔離性。
一個好的分布式事務應用應該盡可能滿足:
提高易用性、即降低業務改造成本。
性能損耗低。
隔離性保證完整。但如同CAP,這三個特性是互相制衡的,往往隻能滿足其中兩個,我們可以搭配AT、TCC和Saga來畫一個三角限制:
本地消息表最初是由eBay架構師Dan Pritchett在一篇解釋 BASE 原理的論文《Base:An Acid Alternative》(https://queue.acm.org/detail.cfm?id=1394128)中提及的,業界目前使用這種方案是比較多的,其核心思想是将分布式事務拆分成本地事務進行處理。
方案通過在事務主動發起方額外建立事務消息表,事務發起方處理業務和記錄事務消息在本地事務中完成,輪詢事務消息表的資料發送事務消息,事務被動方基于消息中間件消費事務消息表中的事務。
下面把分布式事務最先開始處理的事務方稱為事務主動方,在事務主動方之後處理的業務内的其他事務稱為事務被動方。事務的主動方需要額外建立事務消息表,用于記錄分布式事務的消息的發生、處理狀态。
參考上圖,我們不妨來聊一聊本地消息表的事務處理流程。
事務主動方處理好相關的業務邏輯之後,先将業務資料寫入資料庫中的業務表(圖中步驟1),然後将所要發送的消息寫入到資料庫中的消息表(步驟2)。注意:寫入業務表的邏輯和寫入消息表的邏輯在同一個事務中,這樣通過本地事務保證了一緻性。
之後,事務主動方将所要發送的消息發送到消息中間件中(步驟3)。消息在發送過程中丢失了怎麼辦?這裡就展現出消息表的用處了。在上一步中,在消息表中記錄的消息狀态是“發送中”,事務主動方可以定時掃描消息表,然後将其中狀态為“發送中”的消息重新投遞到消息中間件即可。隻有當最後事務被動方消費完之後,消息的狀态才會被設定為“已完成”。
重新投遞的過程中也可能會再次失敗,此時我們一般會指定最大重試次數,重試間隔時間根據重試次數而指數或者線性增長。若達到最大重試次數後記錄日志,我們可以根據記錄的日志來通過郵件或短信來發送告警通知,接收到告警通知後及時介入人工處理即可。
前面3個步驟可以避免“業務處理成功,消息發送失敗”或者“消息發送成功,業務處理失敗”這種棘手情況的出現,并且也可以保證消息不會丢失。
事務被動方監聽并消費消息中間件中的消息(步驟4),然後處理相應的業務邏輯,并把業務資料寫入到自己的業務表中(步驟5),随後将處理結果傳回給消息中間件(步驟6)。
步驟4-6中可能會出現各種異常情況,事務被動方可以在處理完步驟6之後再向消息中間件ACK在步驟4中讀取的消息。這樣,如果步驟4-6中間出現任何異常了都可以重試消費消息中間件中的那條消息。這裡不可避免的會出現重複消費的現象,并且在前面的步驟3中也會出現重複投遞的現象,是以事務被動方的業務邏輯需要能夠保證幂等性。
最後事務主動方也會監聽并讀取消息中間件中的消息(步驟7)來更新消息表中消息的狀态(步驟8)。
步驟6和步驟7是為了将事務被動方的處理結果回報給事務主動方,這裡也可以使用RPC的方式代替。如果在事務被動方處理業務邏輯的過程中發現整個業務流程失敗,那麼事務被動方也可以發送消息(或者RPC)來通知事務主動方進行復原。
基于本地消息表的分布式事務方案就介紹到這裡了,本地消息表的方案的優點是建設成本比較低,其雖然實作了可靠消息的傳遞確定了分布式事務的最終一緻性,其實它也有一些缺陷:
本地消息表與業務耦合在一起,難以做成通用性,不可獨立伸縮。
本地消息表是基于資料庫來做的,而資料庫是要讀寫磁盤IO的,是以在高并發下是有性能瓶頸的。
(歡迎關注公衆号:朱小厮的部落格)
消息事務作為一種異步確定型事務,其核心原理是将兩個事務通過消息中間件進行異步解耦。
消息事務的一種實作思路是通過保證多條消息的同時可見性來保證事務一緻性。但是此類消息事務實作機制更多的是用在 consume-transform-produce(Kafka支援)場景中,其本質上還是用來保證消息自身事務,并沒有把外部事務包含進來。
還有一種思路是依賴于 AMQP 協定(RabbitMQ支援)來確定消息發送成功。AMQP需要在發送事務消息時進行兩階段送出,首先進行 tx_select 開啟事務,然後再進行消息發送,最後執行 tx_commit 或tx_rollback。這個過程可以保證在消息發送成功的同時,本地事務也一定成功執行。但事務粒度不好控制,而且會導緻性能急劇下降,同時也無法解決本地事務執行與消息發送的原子性問題。
不過,RocketMQ事務消息設計解決了上述的本地事務執行與消息發送的原子性問題。在RocketMQ的設計中,broker和producer的雙向通信能力使得broker天生可以作為一個事務協調者存在。而RocketMQ本身提供的存儲機制,則為事務消息提供了持久化能力。RocketMQ 的高可用機制以及可靠消息設計,則為事務消息在系統在發生異常時,依然能夠保證事務的最終一緻性達成。
RocketMQ 事務消息的設計流程同樣借鑒了兩階段送出理論,整體互動流程如下圖所示:
下面我們來了解一下這個設計的整體流程。
首先,事務發起方發送一個Prepare消息到MQ Server中(對應于上圖中Step 1和Step 2),如果這個Prepare消息發送失敗,那麼就直接取消操作,後續的操作也都不再執行。如果這個Prepare消息發送成功了,那麼接着執行自身的本地事務(Step 3)。
如果本地事務執行失敗,那麼通知MQ Server復原(Step 4 - Rollback),後續操作都不再執行。如果本地事務執行成功,就通知MQ Server發送确認消息(Step 4 - Commit)。
倘若 Step 4中的Commit/Rollback消息遲遲未送達到MQ Server中呢?MQ Server會自動定時輪詢所有的 Prepare 消息,然後調用事務發起方事先提供的接口(Step 5),通過這個接口反查事務發起方的上次本地事務是否執行成功(Step 6)。
如果成功,就發送确認消息給 MQ Server;失敗則告訴 MQ Server復原消息(Step 7)。
事務被動方會接收到确認消息,然後執行本地的事務,如果本地事務執行成功則事務正常完成。如果事務被動方本地事務執行失敗了咋辦?基于 MQ 來進行不斷重試,如果實在是不行,可以發送報警由人工來手工復原和補償。
上圖是采用本地消息表方案和采用RocketMQ事務消息方案的對比圖,其實,我們不難發現RocketMQ的這種事務方案就是對本地消息表的封裝,其MQ内部實作了本地消息表的功能,其他方面的協定基本與本地消息表一緻。
RocketMQ 事務消息較好的解決了事務的最終一緻性問題,事務發起方僅需要關注本地事務執行以及實作回查接口給出事務狀态判定等實作,而且在上遊事務峰值高時,可以通過消息隊列,避免對下遊服務産生過大壓力。
事務消息不僅适用于上遊事務對下遊事務無依賴的場景,還可以與一些傳統分布式事務架構相結合,而 MQ 的服務端作為天生的具有高可用能力的協調者,使得我們未來可以基于MQ提供一站式輕量級分布式事務解決方案,用以滿足各種場景下的分布式事務需求。
最大努力通知型(Best-effort Delivery)是最簡單的一種柔性事務,适用于一些最終一緻性時間敏感度低的業務,且被動方處理結果不影響主動方的處理結果。典型的使用場景:如支付通知、短信通知等。
以支付通知為例,業務系統調用支付平台進行支付,支付平台進行支付,進行操作支付之後支付平台會盡量去通知業務系統支付操作是否成功,但是會有一個最大通知次數。如果超過這個次數後還是通知失敗,就不再通知,業務系統自行調用支付平台提供一個查詢接口,供業務系統進行查詢支付操作是否成功。
最大努力通知方案可以借助MQ(消息中間件)來實作,參考下圖。
發起通知方将通知發給MQ,接收通知方監聽 MQ 消息。接收通知方收到消息後,處理完業務回應ACK。接收通知方若沒有回應ACK,則 MQ 會間隔 1min、5min、10min 等重複通知。接受通知方可調用消息校對接口,保證消息的一緻性。
嚴格的ACID事務對隔離性的要求很高,在事務執行中必須将所有的資源鎖定,對于長事務來說,整個事務期間對資料的獨占,将嚴重影響系統并發性能。是以,在高并發場景中,對ACID的部分特性進行放松進而提高性能,這便産生了BASE柔性事務。柔性事務的理念則是通過業務邏輯将互斥鎖操作從資源層面上移至業務層面。通過放寬對強一緻性要求,來換取系統吞吐量的提升。另外提供自動的異常恢複機制,可以在發生異常後也能確定事務的最終一緻。
柔性事務需要應用層進行參與,是以這類分布式事務架構一個首要的功能就是怎麼最大程度降低業務改造成本,然後就是盡可能提高性能(響應時間、吞吐),最好是保證隔離性。
當然如果我們要自己設計一個分布式事務架構,還需要考慮很多其它特性,在明确目标場景偏好後進行權衡取舍,這些特性包括但不限于以下:
業務侵入性(基于注解、XML,補償邏輯);
隔離性(寫隔離/讀隔離/讀未送出,業務隔離/技術隔離);
TM/TC部署形态(單獨部署、與應用部署一起);
錯誤恢複(自動恢複、手動恢複);
性能(復原的機率、付出的代價,響應時間、吞吐);
高可用(注冊中心、資料庫);
持久化(資料庫、檔案、多副本一緻算法);
同步/異步(2PC執行方式);
日志清理(自動、手動);
......
分布式事務一直是業界難題,難在于CAP定理,在于分布式系統8大錯誤假設 ,在于FLP不可能原理 ,在于我們習慣于單機事務ACID做對比。無論是資料庫領域XA,還是微服務下AT、TCC、Saga、本地消息表、事務消息、最大努力通知等方案,都沒有完美解決分布式事務問題,它們不過是各自在性能、一緻性、可用性等方面做取舍,尋求某些場景偏好下的權衡。
想知道更多?掃描下面的二維碼關注我
背景回複”加群“擷取公衆号專屬群聊入口