看了我的這編分佈式事務還不會的帶你白嫖

前言

文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/bin392328206/six-finger
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:549684836 鼓勵大家在技術的路上寫博客

分佈式事務

1 基礎概念

1.1 什麼是事務

什麼是事務?舉個生活中的例子:你去小賣鋪買東西,“一手交錢,一手交貨”就是一個事務的例子,交錢和交貨必 須全部成功,事務纔算成功,任一個活動失敗,事務將撤銷所有已成功的活動。 明白上述例子,再來看事務的定義:

事務可以看做是一次大的活動,它由不同的小活動組成,這些活動要麼全部成功,要麼全部失敗。

1.2 本地事務

在計算機系統中,更多的是通過關係型數據庫來控制事務,這是利用數據庫本身的事務特性來實現的,因此叫數據 庫事務,由於應用主要靠關係數據庫來控制事務,而數據庫通常和應用在同一個服務器,所以基於關係型數據庫的 事務又被稱爲本地事務。

回顧一下數據庫事務的四大特性 ACID:

  • A(Atomic):原子性,構成事務的所有操作,要麼都執行完成,要麼全部不執行,不可能出現部分成功部分失 敗的情況。
  • C(Consistency):一致性,在事務執行前後,數據庫的一致性約束沒有被破壞。比如:張三向李四轉100元, 轉賬前和轉賬後的數據是正確狀態這叫一致性,如果出現張三轉出100元,李四賬戶沒有增加100元這就出現了數 據錯誤,就沒有達到一致性。
  • I(Isolation):隔離性,數據庫中的事務一般都是併發的,隔離性是指併發的兩個事務的執行互不干擾,一個事 務不能看到其他事務運行過程的中間狀態。通過配置事務隔離級別可以避髒讀、重複讀等問題。
  • D(Durability):持久性,事務完成之後,該事務對數據的更改會被持久化到數據庫,且不會被回滾。
    數據庫事務在實現時會將一次事務涉及的所有操作全部納入到一個不可分割的執行單元,該執行單元中的所有操作 要麼都成功,要麼都失敗,只要其中任一操作執行失敗,都將導致整個事務的回滾

1.3 分佈式事務

隨着互聯網的快速發展,軟件系統由原來的單體應用轉變爲分佈式應用,下圖描述了單體應
+用向微服務的演變:分佈式系統會把一個應用系統拆分爲可獨立部署的多個服務,因此需要服務與服務之間遠程協作才能完成事務操 作,這種分佈式系統環境下由不同的服務之間通過網絡遠程協作完成事務稱之爲分佈式事務,例如用戶註冊送積分 事務、創建訂單減庫存事務,銀行轉賬事務等都是分佈式事務。

我們知道本地事務依賴數據庫本身提供的事務特性來實現,因此以下邏輯可以控制本地事務:

begin transaction; 
//1.本地數據庫操作:張三減少金額 
//2.本地數據庫操作:李四增加金額 
commit transation;

但是在分佈式環境下,會變成下邊這樣:

begin transaction;
 //1.本地數據庫操作:張三減少金額
  //2.遠程調用:讓李四增加金額 
  commit transation;

可以設想,當遠程調用讓李四增加金額成功了,由於網絡問題遠程調用並沒有返回,此時本地事務提交失敗就回滾 了張三減少金額的操作,此時張三和李四的數據就不一致了。 因此在分佈式架構的基礎上,傳統數據庫事務就無法使用了,張三和李四的賬戶不在一個數據庫中甚至不在一個應 用系統裏,實現轉賬事務需要通過遠程調用,由於網絡問題就會導致分佈式事務問題。

1.4 分佈式事務的產生場景

  1. 典型的場景就是微服務架構 微服務之間通過遠程調用完成事務操作。 比如:訂單微服務和庫存微服務,下單的 同時訂單微服務請求庫存微服務減庫存。 簡言之:跨JVM進程產生分佈式事務。

2.單體系統訪問多個數據庫實例 當單體系統需要訪問多個數據庫(實例)時就會產生分佈式事務。 比如:用戶信 息和訂單信息分別在兩個MySQL實例存儲,用戶管理系統刪除用戶信息,需要分別刪除用戶信息及用戶的訂單信 息,由於數據分佈在不同的數據實例,需要通過不同的數據庫鏈接去操作數據,此時產生分佈式事務。 簡言之:跨 數據庫實例產生分佈式事務。

3.多服務訪問同一個數據庫實例 比如:訂單微服務和庫存微服務即使訪問同一個數據庫也會產生分佈式事務,原 因就是跨JVM進程,兩個微服務持有了不同的數據庫鏈接進行數據庫操作,此時產生分佈式事務。

2.1 分佈式事務的基本理論

我們瞭解到了分佈式事務的基礎概念。與本地事務不同的是,分佈式系統之所以叫分佈式,是因 爲提供服務的各個節點分佈在不同機器上,相互之間通過網絡交互。不能因爲有一點網絡問題就導致整個系統無法 提供服務,網絡因素成爲了分佈式事務的考量標準之一。因此,分佈式事務需要更進一步的理論支持,接下來,我 們先來學習一下分佈式事務的CAP理論。

2.1.1 CAP理論

CAP是 ConsistencyAvailabilityPartition tolerance三個詞語的縮寫,分別表示一致性、可用性、分區容忍 性。
下邊我們分別來解釋:

爲了方便對CAP理論的理解,我們結合電商系統中的一些業務場景來理解CAP。

如下圖,是商品信息管理的執行流程:

整體執行流程如下:

1、商品服務請求主數據庫寫入商品信息(添加商品、修改商品、刪除商品)
2、主數據庫向商品服務響應寫入成功。
3、商品服務請求從數據庫讀取商品信息。

C - Consistency:

一致性是指寫操作後的讀操作可以讀取到最新的數據狀態,當數據分佈在多個節點上,從任意結點讀取到的數據都 是最新的狀態

上圖中,商品信息的讀寫要滿足一致性就是要實現如下目標:

1、商品服務寫入主數據庫成功,則向從數據庫查詢新數據也成功。 
2、商品服務寫入主數據庫失敗,則向從數據庫查詢新數據也失敗。 

如何實現一致性?

1、寫入主數據庫後要將數據同步到從數據庫。 
2、寫入主數據庫後,在向從數據庫同步期間要將從數據庫鎖定,待同步完成後再釋放鎖,以免在新數據寫入成功 後,向從數據庫查詢到舊的數據。

分佈式系統一致性的特點:

1、由於存在數據同步的過程,寫操作的響應會有一定的延遲。 
2、爲了保證數據一致性會對資源暫時鎖定,待數據同步完成釋放鎖定資源。 3、如果請求數據同步失敗的結點則會返回錯誤信息,一定不會返回舊數據。

A - Availability :

可用性是指任何事務操作都可以得到響應結果,且不會出現響應超時或響應錯誤。 上圖中,商品信息讀取滿足可用性就是要實現如下目標

1、從數據庫接收到數據查詢的請求則立即能夠響應數據查詢結果。 
2、從數據庫不允許出現響應超時或響應錯誤。

如何實現可用性?

1、寫入主數據庫後要將數據同步到從數據庫。
2、由於要保證從數據庫的可用性,不可將從數據庫中的資源進行鎖定。
3、即時數據還沒有同步過來,從數據庫也要返回要查詢的數據,哪怕是舊數據,如果連舊數據也沒有則可以按照 約定返回一個默認信息,但不能返回錯誤或響應超時。

分佈式系統可用性的特點:

1、 所有請求都有響應,且不會出現響應超時或響應錯誤。

P - Partition tolerance :

通常分佈式系統的各各結點部署在不同的子網,這就是網絡分區,不可避免的會出現由於網絡問題而導致結點之間 通信失敗,此時仍可對外提供服務,這叫分區容忍性。

上圖中,商品信息讀寫滿足分區容忍性就是要實現如下目標:

1、主數據庫向從數據庫同步數據失敗不影響讀寫操作。 
2、其一個結點掛掉不影響另一個結點對外提供服務。

如何實現分區容忍性?

1、儘量使用異步取代同步操作,例如使用異步方式將數據從主數據庫同步到從數據,這樣結點之間能有效的實現 松耦合。 
2、添加從數據庫結點,其中一個從結點掛掉其它從結點提供服務。

分佈式分區容忍性的特點:

1、分區容忍性分是布式系統具備的基本能力。

2.1.2 CAP組合方式

1、上邊商品管理的例子是否同時具備 CAP呢?

在所有分佈式事務場景中不會同時具備CAP三個特性,因爲在具備了P的前提下C和A是不能共存的

比如: 下圖滿足了P即表示實現分區容忍:

本圖分區容忍的含義是:

1)主數據庫通過網絡向從數據同步數據,可以認爲主從數據庫部署在不同的分區,通過網絡進行交互。 
2)當主數據庫和從數據庫之間的網絡出現問題不影響主數據庫和從數據庫對外提供服務。 
3)其一個結點掛掉不影響另一個結點對外提供服務。

如果要實現C則必須保證數據一致性,在數據同步的時候爲防止向從數據庫查詢不一致的數據則需要將從數據庫數 據鎖定,待同步完成後解鎖,如果同步失敗從數據庫要返回錯誤信息或超時信息。

如果要實現A則必須保證數據可用性,不管任何時候都可以向從數據查詢數據,則不會響應超時或返回錯誤信息。

通過分析發現在滿足P的前提下C和A存在矛盾性。

2、CAP有哪些組合方式呢?

所以在生產中對分佈式事務處理時要根據需求來確定滿足CAP的哪兩個方面。

1)AP:放棄一致性,追求分區容忍性和可用性。這是很多分佈式系統設計時的選擇

例如: 上邊的商品管理,完全可以實現AP,前提是隻要用戶可以接受所查詢的到數據在一定時間內不是最新的即可。 通常實現AP都會保證最終一致性,後面講的BASE理論就是根據AP來擴展的,一些業務場景 比如:訂單退款,今 日退款成功,明日賬戶到賬,只要用戶可以接受在一定時間內到賬即可。

2)CP:

放棄可用性,追求一致性和分區容錯性,我們的zookeeper其實就是追求的強一致,又比如跨行轉賬,一次轉賬請 求要等待雙方銀行系統都完成整個事務纔算完成。

3)CA:

放棄分區容忍性,即不進行分區,不考慮由於網絡不通或結點掛掉的問題,則可以實現一致性和可用性。那麼系統 將不是一個標準的分佈式系統,我們最常用的關係型數據就滿足了CA。

上邊的商品管理,如果要實現CA則架構如下:
![](https://user-gold-cdn.xitu.io/2019/11/26/16ea73868e4e7392?w=599&h=393&f=png&s=16509

主數據庫和從數據庫中間不再進行數據同步,數據庫可以響應每次的查詢請求,通過事務隔離級別實現每個查詢請 求都可以返回最新的數據。

2.1.3 總結

通過上面我們已經學習了CAP理論的相關知識,CAP是一個已經被證實的理論:一個分佈式系統最多隻能同時滿足 一致性(Consistency)、可用性(Availability)和分區容忍性(Partition tolerance)這三項中的兩項。它可以作 爲我們進行架構設計、技術選型的考量標準。對於多數大型互聯網應用的場景,結點衆多、部署分散,而且現在的 集羣規模越來越大,所以節點故障、網絡故障是常態,而且要保證服務可用性達到N個9(99.99…%),並要達到良 好的響應性能來提高用戶體驗,因此一般都會做出如下選擇:保證P和A,捨棄C強一致,保證最終一致性。

2.2 BASE理論

1、理解強一致性和最終一致性

CAP理論告訴我們一個分佈式系統最多隻能同時滿足一致性(Consistency)、可用性(Availability)和分區容忍 性(Partition tolerance)這三項中的兩項,其中AP在實際應用中較多,AP即捨棄一致性,保證可用性和分區容忍 性,但是在實際生產中很多場景都要實現一致性,比如前邊我們舉的例子主數據庫向從數據庫同步數據,即使不要 一致性,但是最終也要將數據同步成功來保證數據一致,這種一致性和CAP中的一致性不同,CAP中的一致性要求 在任何時間查詢每個結點數據都必須一致,它強調的是強一致性,但是最終一致性是允許可以在一段時間內每個結 點的數據不一致,但是經過一段時間每個結點的數據必須一致,它強調的是最終數據的一致性。

2、Base理論介紹

BASE 是 Basically Available(基本可用)、Soft state(軟狀態)和 Eventually consistent (最終一致性)三個短語的縮 寫。BASE理論是對CAP中AP的一個擴展,通過犧牲強一致性來獲得可用性,當出現故障允許部分不可用但要保證 核心功能可用,允許數據在一段時間內是不一致的,但最終達到一致狀態。滿足BASE理論的事務,我們稱之爲 “柔性事務”

  • 基本可用:分佈式系統在出現故障時,允許損失部分可用功能,保證核心功能可用。如,電商網站交易付款出 現問題了,商品依然可以正常瀏覽。
  • 軟狀態:由於不要求強一致性,所以BASE允許系統中存在中間狀態(也叫軟狀態),這個狀態不影響系統可用 性,如訂單的"支付中"、“數據同步中”等狀態,待數據最終一致後狀態改爲“成功”狀態。
  • 最終一致:最終一致是指經過一段時間後,所有節點數據都將會達到一致。如訂單的"支付中"狀態,最終會變 爲“支付成功”或者"支付失敗",使訂單狀態與實際交易結果達成一致,但需要一定時間的延遲、等待。

3 分佈式事務解決方案之2PC(兩階段提交)

3.1 什麼是2PC

2PC即兩階段提交協議,是將整個事務流程分爲兩個階段,準備階段(Prepare phase)、提交階段(commit phase),2是指兩個階段,P是指準備階段,C是指提交階段。

舉例:張三和李四好久不見,老友約起聚餐,飯店老闆要求先買單,才能出票。這時張三和李四分別抱怨近況不如 意,囊中羞澀,都不願意請客,這時只能AA。只有張三和李四都付款,老闆才能出票安排就餐。但由於張三和李四 都是鐵公雞,形成了尷尬的一幕:

準備階段:老闆要求張三付款,張三付款。老闆要求李四付款,李四付款。

提交階段:老闆出票,兩人拿票紛紛落座就餐。

例子中形成了一個事務,若張三或李四其中一人拒絕付款,或錢不夠,店老闆都不會給出票,並且會把已收款退 回。

整個事務過程由事務管理器和參與者組成,店老闆就是事務管理器,張三、李四就是事務參與者,事務管理器負責 決策整個分佈式事務的提交和回滾,事務參與者負責自己本地事務的提交和回滾

3.2 解決方案

3.2.1 XA方案

2PC的傳統方案是在數據庫層面實現的,如Oracle、MySQL都支持2PC協議,爲了統一標準減少行業內不必要的對 接成本,需要制定標準化的處理模型及接口標準,國際開放標準組織Open Group定義了分佈式事務處理模型 DTP(Distributed Transaction Processing Reference Model)。

爲了讓大家更明確XA方案的內容程,下面新用戶註冊送積分爲例來說明:

執行流程如下:

  1、應用程序(AP)持有用戶庫和積分庫兩個數據源。 
  2、應用程序(AP)通過TM通知用戶庫RM新增用戶,同時通知積分庫RM爲該用戶新增積分,RM此時並未提交事 務,此時用戶和積分資源鎖定。 
  3、TM收到執行回覆,只要有一方失敗則分別向其他RM發起回滾事務,回滾完畢,資源鎖釋放。 
  4、TM收到執行回覆,全部成功,此時向所有RM發起提交事務,提交完畢,資源鎖釋放。

DTP模型定義如下角色:

  • AP(Application Program):即應用程序,可以理解爲使用DTP分佈式事務的程序。
  • RM(Resource Manager):即資源管理器,可以理解爲事務的參與者,一般情況下是指一個數據庫實例,通過 資源管理器對該數據庫進行控制,資源管理器控制着分支事務
  • TM(Transaction Manager):事務管理器,負責協調和管理事務,事務管理器控制着全局事務,管理事務生命 週期,並協調各個RM。全局事務是指分佈式事務處理環境中,需要操作多個數據庫共同完成一個工作,這個 工作即是一個全局事務。
  • DTP模型定義TM和RM之間通訊的接口規範叫XA,簡單理解爲數據庫提供的2PC接口協議,基於數據庫的XA 協議來實現2PC又稱爲XA方案。
  • 以上三個角色之間的交互方式如下:
    • TM向AP提供 應用程序編程接口,AP通過TM提交及回滾事務。
    • TM交易中間件通過XA接口來通知RM數據庫事務的開始、結束以及提交、回滾等
    • 總結:
    • 整個2PC的事務流程涉及到三個角色AP、RM、TM。AP指的是使用2PC分佈式事務的應用程序;RM指的是資 源管理器,它控制着分支事務;TM指的是事務管理器,它控制着整個全局事務。

1)在準備階段RM執行實際的業務操作,但不提交事務,資源鎖定;

2)在提交階段TM會接受RM在準備階段的執行回覆,只要有任一個RM執行失敗,TM會通知所有RM執行回滾操 作,否則,TM將會通知所有RM提交該事務。提交階段結束資源鎖釋放。

XA方案的問題:

1、需要本地數據庫支持XA協議。 
2、資源鎖需要等到兩個階段結束才釋放,性能較差。

3.2.2 Seata方案

Seata是由阿里中間件團隊發起的開源項目 Fescar,後更名爲Seata,它是一個是開源的分佈式事務框架

傳統2PC的問題在Seata中得到了解決,它通過對本地關係數據庫的分支事務的協調來驅動完成全局事務,是工作 在應用層的中間件。主要優點是性能較好,且不長時間佔用連接資源,它以高效並且對業務0侵入的方式解決微服 務場景下面臨的分佈式事務問題,它目前提供AT模式(即2PC)及TCC模式的分佈式事務解決方案。

Seata的設計思想如下

Seata的設計目標其一是對業務無侵入,因此從業務無侵入的2PC方案着手,在傳統2PC的基礎上演進,並解決 2PC方案面臨的問題。

Seata把一個分佈式事務理解成一個包含了若干分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務 達成一致,要麼一起成功提交,要麼一起失敗回滾。此外,通常分支事務本身就是一個關係數據庫的本地事務,下 圖是全局事務與分支事務的關係圖:

與 傳統2PC 的模型類似,Seata定義了3個組件來協議分佈式事務的處理過程:

  1. Transaction Coordinator (TC): 事務協調器,它是獨立的中間件,需要獨立部署運行,它維護全局事務的運 行狀態,接收TM指令發起全局事務的提交與回滾,負責與RM通信協調各各分支事務的提交或回滾。
  2. Transaction Manager ™: 事務管理器,TM需要嵌入應用程序中工作,它負責開啓一個全局事務,並最終 向TC發起全局提交或全局回滾的指令。
  3. Resource Manager (RM): 控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器TC的指令,驅動分 支(本地)事務的提交和回滾。

還拿新用戶註冊送積分舉例Seata的分佈式事務過程

具體的執行流程如下:

1. 用戶服務的 TM 向 TC 申請開啓一個全局事務,全局事務創建成功並生成一個全局唯一的XID。 
2. 用戶服務的 RM 向 TC 註冊 分支事務,該分支事務在用戶服務執行新增用戶邏輯,並將其納入 XID 對應全局 事務的管轄。 
3. 用戶服務執行分支事務,向用戶表插入一條記錄。 
4. 邏輯執行到遠程調用積分服務時(XID 在微服務調用鏈路的上下文中傳播)。積分服務的RM 向 TC 註冊分支事 務,該分支事務執行增加積分的邏輯,並將其納入 XID 對應全局事務的管轄。 
5. 積分服務執行分支事務,向積分記錄表插入一條記錄,執行完畢後,返回用戶服務。 
6. 用戶服務分支事務執行完畢。 
7. TM 向 TC 發起針對 XID 的全局提交或回滾決議。 
8. TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求。

Seata實現2PC與傳統2PC的差別:

架構層次方面,傳統2PC方案的 RM 實際上是在數據庫層,RM 本質上就是數據庫自身,通過 XA 協議實現,而 Seata的 RM 是以jar包的形式作爲中間件層部署在應用程序這一側的。

兩階段提交方面,傳統2PC無論第二階段的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成 才釋放。而Seata的做法是在Phase1 就將本地事務提交,這樣就可以省去Phase2持鎖的時間,整體提高效率。

3.3 小結

本節講解了傳統2PC(基於數據庫XA協議)和Seata實現2PC的兩種2PC方案,由於Seata的0侵入性並且解決了傳 統2PC長期鎖資源的問題,所以推薦採用Seata實現2PC。

Seata實現2PC要點:

1、全局事務開始使用 @GlobalTransactional標識 。 
2、每個本地事務方案仍然使用@Transactional標識。 
3、每個數據都需要創建undo_log表,此表是seata保證本地事務一致性的關鍵

4 分佈式事務解決方案之TCC

4.1.什麼是TCC事務

TCC是Try、Confirm、Cancel三個詞語的縮寫,TCC要求每個分支事務實現三個操作:預處理Try、確認 Confirm、撤銷Cancel。Try操作做業務檢查及資源預留,Confirm做業務確認操作,Cancel實現一個與Try相反的 操作即回滾操作。TM首先發起所有的分支事務的try操作,任何一個分支事務的try操作執行失敗,TM將會發起所 有分支事務的Cancel操作,若try操作全部成功,TM將會發起所有分支事務的Confirm操作,其中Confirm/Cancel 操作若執行失敗,TM會進行重試。

TCC分爲三個階段:

  1. Try 階段是做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和後續的Confirm 一起才能 真正構成一個完整的業務邏輯。
  2. Confirm 階段是做確認提交,Try階段所有分支事務執行成功後開始執行 Confirm。通常情況下,採用TCC則 認爲 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。若Confirm階段真的出錯了,需引 入重試機制或人工處理。
  3. Cancel 階段是在業務執行錯誤需要回滾的狀態下執行分支事務的業務取消,預留資源釋放。通常情況下,採 用TCC則認爲Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。
  4. TM事務管理器 TM事務管理器可以實現爲獨立的服務,也可以讓全局事務發起方充當TM的角色,TM獨立出來是爲了成爲公 用組件,是爲了考慮系統結構和軟件複用

TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分佈式事務調用鏈條,用來記錄事務上下文, 追蹤和記錄狀態,由於Confirm 和cancel失敗需進行重試,因此需要實現爲冪等,冪等性是指同一個操作無論請求 多少次,其結果都相同

4.2 TCC 解決方案

目前市面上的TCC框架衆多比如下面這幾種: (以下數據採集日爲2019年11月23日)

框架名稱 Gitbub地址 star數量
tcc-transaction https://github.com/changmingxie/tcc-transaction 3850
Hmily https://github.com/yu199195/hmily 2407
ByteTCC https://github.com/liuyangming/ByteTCC 1947
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction 1690

上面講的Seata也支持TCC,但Seata的TCC模式對Spring Cloud並沒有提供支持。我們的目標是理解TCC的原 理以及事務協調運作的過程,因此更請傾向於輕量級易於理解的框架,因此最終確定了Hmily。

Hmily是一個高性能分佈式事務TCC開源框架。基於Java語言來開發(JDK1.8),支持Dubbo,Spring Cloud等 RPC框架進行分佈式事務。它目前支持以下特性:

- 支持嵌套事務(Nested transaction support).
- 採用disruptor框架進行事務日誌的異步讀寫,與RPC框架的性能毫無差別
- 支持SpringBoot-starter 項目啓動,使用簡單
- RPC框架支持 : dubbo,motan,springcloud。
- 本地事務存儲支持 : redis,mongodb,zookeeper,file,mysql。
- 事務日誌序列化支持 :java,hessian,kryo,protostuff
- 採用Aspect AOP 切面思想與Spring無縫集成,天然支持集羣。
- RPC事務恢復,超時異常恢復等

Hmily利用AOP對參與分佈式事務的本地方法與遠程方法進行攔截處理,通過多方攔截,事務參與者能透明的 調用到另一方的Try、Confirm、Cancel方法;傳遞事務上下文;並記錄事務日誌,酌情進行補償,重試等。

Hmily不需要事務協調服務,但需要提供一個數據庫(mysql/mongodb/zookeeper/redis/file)來進行日誌存 儲。

Hmily實現的TCC服務與普通的服務一樣,只需要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務 邏輯,只是因爲全局事務提交/回滾的需要才提供的,因此Confirm/Cancel業務只需要被Hmily TCC事務框架 發現即可,不需要被調用它的其他業務服務所感知。

官網介紹:https://dromara.org/website/zh-cn/docs/hmily/index.html

TCC需要注意三種異常處理分別是空回滾、冪等、懸掛

空回滾:

在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回 滾,然後直接返回成功。

出現原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄爲失敗,這個時候其實是沒有執行Try階 段,當故障恢復後,分佈式事務進行回滾則會調用二階段的Cancel方法,從而形成空回滾。

解決思路是關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回 滾;如果沒執行,那就是空回滾。前面已經說過TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分 布式事務調用鏈條。再額外增加一張分支事務記錄表,其中有全局事務 ID 和分支事務 ID,第一階段 Try 方法裏會 插入一條記錄,表示一階段執行了。Cancel 接口裏讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存 在,則是空回滾。

冪等:

通過前面介紹已經瞭解到,爲了保證TCC二階段提交重試機制不會引發數據不一致,要求 TCC 的二階段 Try、 Confirm 和 Cancel 接口保證冪等,這樣不會重複使用或者釋放資源。如果冪等控制沒有做好,很有可能導致數據 不一致等嚴重問題。

解決思路在上述“分支事務記錄”中增加執行狀態,每次執行前都查詢該狀態

懸掛:

懸掛就是對於一個分佈式事務,其二階段 Cancel 接口比 Try 接口先執行

出現原因是在 RPC 調用分支事務try時,先註冊分支事務,再執行RPC調用,如果此時 RPC 調用的網絡發生擁堵, 通常 RPC 調用是有超時時間的,RPC 超時以後,TM就會通知RM回滾該分佈式事務,可能回滾完成後,RPC 請求 纔到達參與者真正執行,而一個 Try 方法預留的業務資源,只有該分佈式事務才能使用,該分佈式事務第一階段預 留的業務資源就再也沒有人能夠處理了,對於這種情況,我們就稱爲懸掛,即業務資源預留後沒法繼續處理。

解決思路是如果二階段執行完成,那一階段就不能再繼續執行。在執行一階段事務時判斷在該全局事務下,“分支 事務記錄”表中是否已經有二階段事務記錄,如果有則不執行Try。

舉例,場景爲 A 轉賬 30 元給 B,A和B賬戶在不同的服務

方案1:

賬戶A

    ```
    try:
        檢查餘額是否夠30元 
        扣減30元 
    confirm: 
        空 
    cancel:
        增加30元
    ```

賬戶B

 ```
 try:
    增加30元 
 confirm: 
    空 
 cancel:
    減少30元
 ```

方案1說明:

1)賬戶A,這裏的餘額就是所謂的業務資源,按照前面提到的原則,在第一階段需要檢查並預留業務資源,因此, 我們在扣錢 TCC 資源的 Try 接口裏先檢查 A 賬戶餘額是否足夠,如果足夠則扣除 30 元。 Confirm 接口表示正式 提交,由於業務資源已經在 Try 接口裏扣除掉了,那麼在第二階段的 Confirm 接口裏可以什麼都不用做。Cancel 接口的執行表示整個事務回滾,賬戶A回滾則需要把 Try 接口裏扣除掉的 30 元還給賬戶。

2)賬號B,在第一階段 Try 接口裏實現給賬戶B加錢,Cancel 接口的執行表示整個事務回滾,賬戶B回滾則需要把 Try 接口裏加的 30 元再減去。

方案1的問題分析:

1)如果賬戶A的try沒有執行在cancel則就多加了30元。 
2)由於try,cancel、confirm都是由單獨的線程去調用,且會出現重複調用,所以都需要實現冪等。 
3)賬號B在try中增加30元,當try執行完成後可能會其它線程給消費了。 
4)如果賬戶B的try沒有執行在cancel則就多減了30元。

問題解決:

1)賬戶A的cancel方法需要判斷try方法是否執行,正常執行try後方可執行cancel。 
2)try,cancel、confirm方法實現冪等。 
3)賬號B在try方法中不允許更新賬戶金額,在confirm中更新賬戶金額。 
4)賬戶B的cancel方法需要判斷try方法是否執行,正常執行try後方可執行cancel。

優化方案:

賬戶A

    ```
    try:
        try冪等校驗 
        try懸掛處理 
        檢查餘額是否夠30元 
        扣減30元 
    confirm: 
        空 
    cancel:
        cancel冪等校驗 
        cancel空回滾處理 
        增加可用餘額30元
    ````

賬戶B

 ```
 try:
    空 
 confirm: 
    confirm冪等校驗 
    正式增加30元 
 cancel:
    空
 ```

4.3 小結

如果拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處 理,需要通過業務邏輯來實現。這種分佈式事務的實現方式的優勢在於,可以讓應用自己定義數據操作的粒度,使 得降低鎖衝突、提高吞吐量成爲可能。

而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此 外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。

5 分佈式事務解決方案之可靠消息最終一致性

###5.1 什麼是可靠消息最終一致性事務

可靠消息最終一致性方案是指當事務發起方執行完成本地事務後併發出一條消息,事務參與方(消息消費者)一定能 夠接收消息並處理事務成功,此方案強調的是隻要消息發給事務參與方最終事務要達到一致

此方案是利用消息中間件完成,如下圖:

事務發起方(消息生產方)將消息發給消息中間件,事務參與方從消息中間件接收消息,事務發起方和消息中間件 之間,事務參與方(消息消費方)和消息中間件之間都是通過網絡通信,由於網絡通信的不確定性會導致分佈式事 務問題。

因此可靠消息最終一致性方案要解決以下幾個問題:

1.本地事務與消息發送的原子性問題
本地事務與消息發送的原子性問題即:事務發起方在本地事務執行成功後消息必鬚髮出去,否則就丟棄消息。即實 現本地事務和消息發送的原子性,要麼都成功,要麼都失敗。本地事務與消息發送的原子性問題是實現可靠消息最 終一致性方案的關鍵問題。

先來嘗試下這種操作,先發送消息,再操作數據庫:

begin transaction; 

    //1.發送MQ 
    //2.數據庫操作 
commit transation;

這種情況下無法保證數據庫操作與發送消息的一致性,因爲可能發送消息成功,數據庫操作失敗。

你立馬想到第二種方案,先進行數據庫操作,再發送消息:

begin transaction;
    //1.數據庫操作 
    //2.發送MQ 
commit transation;

這種情況下貌似沒有問題,如果發送MQ消息失敗,就會拋出異常,導致數據庫事務回滾。但如果是超時異常,數 據庫回滾,但MQ其實已經正常發送了,同樣會導致不一致。

2 事務參與方接收消息的可靠性

事務參與方必須能夠從消息隊列接收到消息,如果接收消息失敗可以重複接收消息。

3 消息重複消費的問題

由於網絡2的存在,若某一個消費節點超時但是消費成功,此時消息中間件會重複投遞此消息,就導致了消息的重 復消費。

要解決消息重複消費的問題就要實現事務參與方的方法冪等性。

5.2 解決方案

5.2.1 本地消息表方案

本地消息表這個方案最初是eBay提出的,此方案的核心是通過本地事務保證數據業務操作和消息的一致性,然後 通過定時任務將消息發送至消息中間件,待確認消息發送給消費方成功再將消息刪除。

下面以註冊送積分爲例來說明:

下面以註冊送積分爲例來說明:

下例共有兩個微服務交互,用戶服務和積分服務,用戶服務負責添加用戶,積分服務負責增加積分。

交互流程如下:
1、用戶註冊
用戶服務在本地事務新增用戶和增加 ”積分消息日誌“。(用戶表和消息表通過本地事務保證一致)
下邊是僞代碼

begin transaction; 
    //1.新增用戶 
    //2.存儲積分消息日誌 
commit transation;

這種情況下,本地數據庫操作與存儲積分消息日誌處於同一個事務中,本地數據庫操作與記錄消息日誌操作具備原子性

2、定時任務掃描日誌

如何保證將消息發送給消息隊列呢?

經過第一步消息已經寫到消息日誌表中,可以啓動獨立的線程,定時對消息日誌表中的消息進行掃描併發送至消息 中間件,在消息中間件反饋發送成功後刪除該消息日誌,否則等待定時任務下一週期重試。

3、消費消息

如何保證消費者一定能消費到消息呢?

這裏可以使用MQ的ack(即消息確認)機制,消費者監聽MQ,如果消費者接收到消息並且業務處理完成後向MQ 發送ack(即消息確認),此時說明消費者正常消費消息完成,MQ將不再向消費者推送消息,否則消費者會不斷重 試向消費者來發送消息。

積分服務接收到”增加積分“消息,開始增加積分,積分增加成功後向消息中間件迴應ack,否則消息中間件將重複 投遞此消息。

由於消息會重複投遞,積分服務的”增加積分“功能需要實現冪等性

5.2.2 RocketMQ事務消息方案

RocketMQ 是一個來自阿里巴巴的分佈式消息中間件,於 2012 年開源,並在 2017 年正式成爲 Apache 頂級項 目。據瞭解,包括阿里雲上的消息產品以及收購的子公司在內,阿里集團的消息產品全線都運行在 RocketMQ 之 上,並且最近幾年的雙十一大促中,RocketMQ 都有搶眼表現。Apache RocketMQ 4.3之後的版本正式支持事務消 息,爲分佈式事務實現提供了便利性支持。

RocketMQ 事務消息設計則主要是爲了解決 Producer 端的消息發送與本地事務執行的原子性問題,RocketMQ 的 設計中 broker 與 producer 端的雙向通信能力,使得 broker 天生可以作爲一個事務協調者存在;而 RocketMQ 本身提供的存儲機制爲事務消息提供了持久化能力;RocketMQ 的高可用機制以及可靠消息設計則爲事務消息在系 統發生異常時依然能夠保證達成事務的最終一致性。

在RocketMQ 4.3後實現了完整的事務消息,實際上其實是對本地消息表的一個封裝,將本地消息表移動到了MQ 內部,解決 Producer 端的消息發送與本地事務執行的原子性問題。

執行流程如下:
爲方便理解我們還以註冊送積分的例子來描述 整個流程。

Producer 即MQ發送方,本例中是用戶服務,負責新增用戶。MQ訂閱方即消息消費方,本例中是積分服務,負責 新增積分。

1、Producer 發送事務消息

Producer (MQ發送方)發送事務消息至MQ Server,MQ Server將消息狀態標記爲Prepared(預備狀態),注 意此時這條消息消費者(MQ訂閱方)是無法消費到的。
本例中,Producer 發送 ”增加積分消息“ 到MQ Server。

2、MQ Server迴應消息發送成功
MQ Server接收到Producer 發送給的消息則迴應發送成功表示MQ已接收到消息。

3、Producer 執行本地事務
Producer 端執行業務代碼邏輯,通過本地數據庫事務控制
本例中,Producer 執行添加用戶操作。

4、消息投遞
若Producer 本地事務執行成功則自動向MQServer發送commit消息,MQ Server接收到commit消息後將”增加積 分消息“ 狀態標記爲可消費,此時MQ訂閱方(積分服務)即正常消費消息;
若Producer 本地事務執行失敗則自動向MQServer發送rollback消息,MQ Server接收到rollback消息後 將刪 除”增加積分消息“ 。

MQ訂閱方(積分服務)消費消息,消費成功則向MQ迴應ack,否則將重複接收消息。這裏ack默認自動迴應,即 程序執行正常則自動迴應ack。

5、事務回查
如果執行Producer端本地事務過程中,執行端掛掉,或者超時,MQ Server將會不停的詢問同組的其他 Producer 來獲取事務執行狀態,這個過程叫事務回查。MQ Server會根據事務回查結果來決定是否投遞消息。

以上主幹流程已由RocketMQ實現,對用戶側來說,用戶需要分別實現本地事務執行以及本地事務回查方法,因此 只需關注本地事務的執行狀態即可。

RoacketMQ提供RocketMQLocalTransactionListener接口:

public interface RocketMQLocalTransactionListener {
 /** 
 ‐ 發送prepare消息成功此方法被回調,該方法用於執行本地事務 
 ‐ @param msg 回傳的消息,利用transactionId即可獲取到該消息的唯一Id 
 ‐ @param arg 調用send方法時傳遞的參數,當send時候若有額外的參數可以傳遞到send方法中,這裏能獲取到 
 ‐ @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調 
 */
 RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg); 
 /** 
 ‐ @param msg 通過獲取transactionId來判斷這條消息的本地事務執行狀態 
 ‐ @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調 
 */
 RocketMQLocalTransactionState checkLocalTransaction(Message msg); }

發送事務消息:

以下是RocketMQ提供用於發送事務消息的API:

    TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup"); 
    producer.setNamesrvAddr("127.0.0.1:9876"); 
    producer.start();
   //設置TransactionListener實現 
   producer.setTransactionListener(transactionListener); 
   //發送事務消息 
    SendResult sendResult = producer.sendMessageInTransaction(msg, null);


5.3 小結

可靠消息最終一致性就是保證消息從生產方經過消息中間件傳遞到消費方的一致性,
RocketMQ作爲 消息中間件,

RocketMQ主要解決了兩個功能:

1、本地事務與消息發送的原子性問題。 
2、事務參與方接收消息的可靠性。 可靠消息最終一致性事務適合執行週期長且實時性要求不高的場景。
引入消息機制後,同步的事務操作變爲基於消 息執行的異步操作, 避免了分佈式事務中的同步阻塞操作的影響,並實現了兩個服務的解耦。

6 分佈式事務解決方案之最大努力通知

6.1 什麼是最大努力通知

最大努力通知也是一種解決分佈式事務的方案,下邊是一個是充值的例子:

交互流程:

1、賬戶系統調用充值系統接口
2、充值系統完成支付處理向賬戶系統發起充值結果通知 若通知失敗,則充值系統按策略進行重複通知
3、賬戶系統接收到充值結果通知修改充值狀態。
4、賬戶系統未接收到通知會主動調用充值系統的接口查詢充值結果

通過上邊的例子我們總結最大努力通知方案的目標:

目標:發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。

具體包括:

1、有一定的消息重複通知機制。 因爲接收通知方可能沒有接收到通知,此時要有一定的機制對消息重複通知。
2、消息校對機制。 如果盡最大努力也沒有通知到接收方,或者接收方消費消息後要再次消費,此時可由接收方主動向通知方查詢消息 信息來滿足需求。

最大努力通知與可靠消息一致性有什麼不同

1、解決方案思想不同

可靠消息一致性,發起通知方需要保證將消息發出去,並且將消息發到接收通知方,消息的可靠性關鍵由發起通知 方來保證。

最大努力通知,發起通知方盡最大的努力將業務處理結果通知爲接收通知方,但是可能消息接收不到,此時需要接 收通知方主動調用發起通知方的接口查詢業務處理結果,通知的可靠性關鍵在接收通知方

兩者的業務應用場景不同

可靠消息一致性關注的是交易過程的事務一致,以異步的方式完成交易。

最大努力通知關注的是交易後的通知事務,即將交易結果可靠的通知出去。

3、技術解決方向不同
可靠消息一致性要解決消息從發出到接收的一致性,即消息發出並且被接收到。

最大努力通知無法保證消息從發出到接收的一致性,只提供消息接收的可靠性機制。可靠機制是,最大努力的將消 息通知給接收方,當消息無法被接收方接收時,由接收方主動查詢消息(業務處理結果)。

6.2 解決方案

通過對最大努力通知的理解,採用MQ的ack機制就可以實現最大努力通知

方案1:

本方案是利用MQ的ack機制由MQ向接收通知方發送通知,流程如下:

1、發起通知方將通知發給MQ。使用普通消息機制將通知發給MQ。 注意:如果消息沒有發出去可由接收通知方主動請求發起通知方查詢業務執行結果。(後邊會講)
2、接收通知方監聽 MQ
3、接收通知方接收消息,業務處理完成迴應ack
4、接收通知方若沒有迴應ack則MQ會重複通知。
5、接收通知方可通過消息校對接口來校對消息的一致性。

方案2:

本方案也是利用MQ的ack機制,與方案1不同的是應用程序向接收通知方發送通知,如下圖:

交互流程如下:

1、發起通知方將通知發給MQ。 使用可靠消息一致方案中的事務消息保證本地事務與消息的原子性,最終將通知先發給MQ。
2、通知程序監聽 MQ,接收MQ的消息。 方案1中接收通知方直接監聽MQ,方案2中由通知程序監聽MQ。通知程序若沒有迴應ack則MQ會重複通知。
3、通知程序通過互聯網接口協議(如http、webservice)調用接收通知方案接口,完成通知。 通知程序調用接收通知方案接口成功就表示通知成功,即消費MQ消息成功,MQ將不再向通知程序投遞通知消 息。
4、接收通知方可通過消息校對接口來校對消息的一致性。

方案1和方案2的不同點:

1、方案1中接收通知方與MQ接口,即接收通知方案監聽 MQ,此方案主要應用與內部應用之間的通知。

2、方案2中由通知程序與MQ接口,通知程序監聽MQ,收到MQ的消息後由通知程序通過互聯網接口協議調用接收 通知方。此方案主要應用於外部應用之間的通知,例如支付寶、微信的支付結果通知。

7 分佈式事務對比分析:

在瞭解各種分佈式事務的解決方案後,我們瞭解到各種方案的優缺點:

2PC 最大的詬病是一個阻塞協議。RM在執行分支事務後需要等待TM的決定,此時服務會阻塞並鎖定資源。由於其 阻塞機制和最差時間複雜度高, 因此,這種設計不能適應隨着事務涉及的服務數量增加而擴展的需要,很難用於並 發較高以及子事務生命週期較長 (long-running transactions) 的分佈式服務中。

如果拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處 理,需要通過業務邏輯來實現。這種分佈式事務的實現方式的優勢在於,可以讓應用自己定義數據操作的粒度,使 得降低鎖衝突、提高吞吐量成爲可能。而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現 try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實 現不同的回滾策略。典型的使用場景:滿,登錄送優惠券等。

可靠消息最終一致性事務適合執行週期長且實時性要求不高的場景。引入消息機制後,同步的事務操作變爲基於消 息執行的異步操作, 避免了分佈式事務中的同步阻塞操作的影響,並實現了兩個服務的解耦。典型的使用場景:注 冊送積分,登錄送優惠券等。

最大努力通知是分佈式事務中要求最低的一種,適用於一些最終一致性時間敏感度低的業務;允許發起通知方處理業 務失敗,在接收通知方收到通知後積極進行失敗處理,無論發起通知方如何處理結果都會不影響到接收通知方的後 續處理;發起通知方需提供查詢執行情況接口,用於接收通知方校對結果。典型的使用場景:銀行通知、支付結果 通知等。

最大努力通知是分佈式事務中要求最低的一種,適用於一些最終一致性時間敏感度低的業務;允許發起通知方處理業 務失敗,在接收通知方收到通知後積極進行失敗處理,無論發起通知方如何處理結果都會不影響到接收通知方的後 續處理;發起通知方需提供查詢執行情況接口,用於接收通知方校對結果。典型的使用場景:銀行通知、支付結果 通知等。

2PC TCC 可靠消息 最大努力通知
一致性 強一致性 最終一致 最終一致 最終一致
吞吐量
實現複雜度

總結:
在條件允許的情況下,我們儘可能選擇本地事務單數據源,因爲它減少了網絡交互帶來的性能損耗,且避免了數據 弱一致性帶來的種種問題。若某系統頻繁且不合理的使用分佈式事務,應首先從整體設計角度觀察服務的拆分是否 合理,是否高內聚低耦合?是否粒度太小?分佈式事務一直是業界難題,因爲網絡的不確定性,而且我們習慣於拿 分佈式事務與單機事務ACID做對比。

無論是數據庫層的XA、還是應用層TCC、可靠消息、最大努力通知等方案,都沒有完美解決分佈式事務問題,它們 不過是各自在性能、一致性、可用性等方面做取捨,尋求某些場景偏好下的權衡。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裏的人呀,都是人才

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇博客有任何錯誤,請批評指教,不勝感激 !

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