原創 | 分佈式事務科普(終結篇)

點擊上方“朱小廝的博客”,選擇“設爲星標”

後臺回覆"高效Java"領取《Effective Java第三版》

噹噹優惠碼:V54PW7,可減¥30

歡迎跳轉到本文的原文鏈接:https://honeypps.com/architect/introduction-of-distributed-transaction/

《分佈式事務科普》是我在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 DTP模型與XA

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的時候才能使用分佈式事務)。

  • 運維複雜。

Seata與AT模式

AT(Automatic Transaction)模式是基於XA事務演進而來,核心是對業務無侵入,是一種改進後的兩階段提交,需要數據庫支持。AT最早出現在阿里巴巴開源的分佈式事務框架Seata中,我們不妨先簡單瞭解下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):控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾。

參照上圖,簡要概括整個事務的處理流程爲:

  1. TM 向 TC 申請開啓一個全局事務,TC 創建全局事務後返回全局唯一的 XID,XID 會在全局事務的上下文中傳播;

  2. RM 向 TC 註冊分支事務,該分支事務歸屬於擁有相同 XID 的全局事務;

  3. TM要求TC提交或回滾XID的相應全局事務。

  4. TC在XID的相應全局事務下驅動所有分支事務以完成分支提交或回滾。

Seata 會有 4 種分佈式事務解決方案,分別是 AT 模式、TCC 模式、Saga 模式和 XA 模式。這個小節我們主要來講述一下AT模式的實現方式,TCC和Saga模式在後面會繼續介紹。

AT模式

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

關於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 分佈式事務模型需要業務系統提供三段業務邏輯:

  1. Try:完成所有業務檢查,預留必須的業務資源。

  2. Confirm:真正執行的業務邏輯,不作任何業務檢查,只使用 Try 階段預留的業務資源。因此,只要Try操作成功,Confirm必須能成功。另外,Confirm操作需滿足冪等性,保證分佈式事務有且只能成功一次。

  3. 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分佈式事務流程:

  1. 主業務服務首先開啓本地事務。

  2. 主業務服務向事務管理器申請啓動分佈式事務主業務活動。

  3. 然後針對要調用的從業務服務,主業務活動先向事務管理器註冊從業務活動,然後調用從業務服務的 Try 接口。

  4. 當所有從業務服務的 Try 接口調用成功,主業務服務提交本地事務;若調用失敗,主業務服務回滾本地事務。

  5. 若主業務服務提交本地事務,則TCC模型分別調用所有從業務服務的Confirm接口;若主業務服務回滾本地事務,則分別調用 Cancel 接口;

  6. 所有從業務服務的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 接口之後,是不是就真的高枕無憂了呢?答案是否定的。在微服務架構下,很有可能出現網絡超時、重發,機器宕機等一系列的異常情況。一旦遇到這些 情況,就會導致我們的分佈式事務執行過程出現異常,最常見的主要是空回滾、冪等、懸掛。因此,在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

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)來通知事務主動方進行回滾。

基於本地消息表的分佈式事務方案就介紹到這裏了,本地消息表的方案的優點是建設成本比較低,其雖然實現了可靠消息的傳遞確保了分佈式事務的最終一致性,其實它也有一些缺陷:

  1. 本地消息表與業務耦合在一起,難以做成通用性,不可獨立伸縮。

  2. 本地消息表是基於數據庫來做的,而數據庫是要讀寫磁盤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、本地消息表、事務消息、最大努力通知等方案,都沒有完美解決分佈式事務問題,它們不過是各自在性能、一致性、可用性等方面做取捨,尋求某些場景偏好下的權衡。

歡迎跳轉到本文的原文鏈接:https://honeypps.com/architect/introduction-of-distributed-transaction/

想知道更多?描下面的二維碼關注我

後臺回覆”加羣“獲取公衆號專屬羣聊入口

噹噹優惠碼福利來一波!噹噹全場自營圖書5折,用優惠碼:V54PW7(長按複製),滿200(原價400)再減30,相當於170=400,四折多一點。使用渠道:噹噹小程序或噹噹APP。使用時間:4/10-4/23。

【精彩推薦】

>>> 字節跳動社招內推入口 <<<

>>> 字節跳動校招內推入口 <<<

朕已閱 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章