不就是分散式事務,這下徹底清楚了😎

三分惡 發表於 2021-09-18

大家好,我是老三,上次發文的時候還是上次發文的時候,這篇文章分享分散式事務,看完要是你們不懂,那一定是不明白。

從本地事務到分散式事務

事務大家應該都知道,事務將一組操作納入到一個不可分割的執行單元,這個執行單元裡的操作都成功時才能提交成功。

簡單地說,事務提供一種要麼不做,要麼全做機制。

ACID

我們先簡單瞭解一下事務的四大特性:

ACID

  • A 原子性(Atomicity)

一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會出現部分成功部分失敗的情況。。

  • C 一致性(Consistency)

事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。如果事務成功地完成,那麼系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那麼系統中的所有變化將自動地回滾,系統返回到原始狀態。

  • I 隔離性(Isolation)

指的是在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。事務檢視資料更新時,資料所處的狀態要麼是另一事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視到中間狀態的資料。

  • D 永續性(Durability)

指的是隻要事務成功結束,它對資料庫所做的更新就必須永久儲存下來。即使發生系統崩潰,重新啟動資料庫系統後,資料庫還能恢復到事務成功結束時的狀態。

單體事務

在單體架構時代,所有的業務只用一個資料庫。

單體服務

單體架構時代事務的是實現很簡單,我們操作的是同一個資料庫,利用資料庫本身提供的事務機制支援就可以了。

例如我們比較熟悉的MySQL資料庫:

  • 事務的隔離性是通過資料庫鎖的機制實現的。
  • 事務的一致性由undo log來保證:undo log是邏輯日誌,記錄了事務的insertupdatedeltete操作,回滾的時候做相反的deleteupdateinsert操作來恢復資料。
  • 事務的原子性和一永續性由redo log來保證:redolog被稱作重做日誌,是物理日誌,事務提交的時候,必須先將事務的所有日誌寫入redo log持久化,到事務的提交操作才算完成。

ACID實現

詳細瞭解建議閱讀《MySQL技術內幕 InnoDB儲存引擎》7.2節。

分散式事務

隨著業務發展,單體架構頂不住了,慢慢進入分散式時代——SOA或者粒度更細的微服務

當然伴隨而來的就是分庫分表。

  • 我們可能會根據業務服務拆分的方式,對應地垂直拆分大庫,例如原始大庫拆分成訂單庫、商品庫、支付庫。
  • 同時由於業務資料可能會高速增加,很快就成了億級,我們不得不又水平分庫,來減輕單個資料庫的壓力。

分散式情況下資料庫

不管是怎麼分庫的,最後的結果就是我們一個操作可能要橫跨多個資料庫。

資料庫本身的事務機制只能保證它自己這個庫的事務,但是沒法保證到其它的庫。我們要保證跨多個庫的操作還具備事務的特性,就不得不上分散式事務了。

在前面 分散式必備理論基礎:CAP和BASE 裡,講了分散式的理論基礎——CAPBASE,這裡就不再多講。

我們只需要知道,BASE理論是對CAP中AP的一個延申,在沒法保證強一致性的前提下,儘可能達到最終的一致性。

我們的分散式事務通常也做不到本地事務那麼強的一致性,一般都是對一致性(Consistency)適當做了一些放寬,只需要達到最終的一致性。

分散式事務解決方案

XA /2PC兩階段提交

XA

XA是一個分散式事務協議,由Tuxedo提出。

在這個協議裡,有三個角色:

  • AP(Application):應用系統(服務)
  • TM(Transaction Manager):事務管理器(全域性事務管理)
  • RM(Resource Manager):資源管理器(資料庫)

XA規範主要定義了 事務管理器(Transaction Manager)和資源管理器(Resource Manager)之間的介面。

XA規範

XA協議採用兩階段提交方式來管理分散式事務。XA介面提供資源管理器與事務管理器之間進行通訊的標準介面。

2PC 兩階段提交

兩階段提交的思路可以概括為: 參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情況決定各參與者是否要提交操作還是中止操作。

兩階段提交的兩個階段:第一階段:準備階段,第二階段:提交階段

兩階段-參考[2]

準備階段 Prepares

協調者向所有參與者詢問是否可以執行提交操作,所有參與者執行事務,將結果返回給協調者。

第一階段

提交階段 commit

  • 如果第一階段匯中所有參與者都返回yes響應,協調者向所有參與者發出提交請求,所有參與者提交事務
  • 如果第一階段中有一個或者多個參與者返回no響應,協調者向所有參與者發出回滾請求,所有參與者進行回滾操作

第二階段

兩階段提交優點:儘量保證了資料的強一致,但不是100%一致

兩階段提交同樣有一些缺點:

  • 單點故障

    由於協調者的重要性,一旦協調者發生故障,參與者會一直阻塞,尤其時在第二階段,協調者發生故障,那麼所有的參與者都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。

  • 同步阻塞

    它是一個強一致性的同步阻塞協議,也就是所謂剛性事務,事務執⾏過程中需要將所需資源全部鎖定,會比較影響效能。

  • 資料不一致

    在第二階段中,當協調者想參與者傳送提交事務請求之後,由於網路抖動,如果第二階段只有部分參與者收到提交請求,那麼就會導致資料不一致。

3PC 三階段提交

三階段提交(3PC)是二階段提交(2PC)的一種改進版本 ,為解決兩階段提交協議的單點故障和同步阻塞問題。上邊提到兩階段提交,當協調者崩潰時,參與者不能做出最後的選擇,就會一直保持阻塞鎖定資源。

2PC 中只有協調者有超時機制,3PC 在協調者和參與者中都引入了超時機制,協調者出現故障後,參與者就不會一直阻塞。而且在第一階段和第二階段中又插入了一個預提交階段,保證了在最後提交階段之前各參與節點的狀態是一致的。

三階段提交的三個階段:CanCommitPreCommitDoCommit三個階段

三階段協議-參考[2]

準備階段 CanCommit

協調者向參與者傳送commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。

預提交階段 PreCommit

協調者根據參與者在準備階段的響應判斷是否執行事務還是中斷事務

  • 如果所有參與者都返回Yes,則執行事務
  • 如果參與者有一個或多個參與者返回No或者超時,則中斷事務

參與者執行完操作之後返回ACK響應,同時開始等待最終指令。

提交階段 DoCommit

協調者根據參與者在準備階段的響應判斷是否執行事務還是中斷事務

  • 如果所有參與者都返回正確的ACK響應,則提交事務
  • 如果參與者有一個或多個參與者收到錯誤的ACK響應或者超時,則中斷事務
  • 如果參與者無法及時接收到來自協調者的提交或者中斷事務請求時,在等待超時之後,會繼續進行事務提交

協調者收到所有參與者的ACK響應,完成事務。

3PC

可以看出,三階段提交解決的只是兩階段提交中 單體故障和同步阻塞的問題,因為加入了超時機制,這裡的超時的機制作用於 預提交階段提交階段。如果等待 預提交請求 超時,參與者直接回到準備階段之前。如果等到提交請求超時,那參與者就會提交事務了。

無論是2PC還是3PC都不能保證分散式系統中的資料100%一致

TCC補償事務

TCC Try-Confirm-Cancel 的簡稱,是兩階段提交的一個變種,針對每個操作,都需要有一個其對應的確認和取消操作,當操作成功時呼叫確認操作,當操作失敗時呼叫取消操作,類似於二階段提交,只不過是這裡的提交和回滾是針對業務上的,所以基於TCC實現的分散式事務也可以看做是對業務的一種補償機制。

TCC的三階段:

  1. Try 階段:對業務系統做檢測及資源預留
  2. Confirm 階段:對業務系統做確認提交,Try階段執行成功並開始執行 Confirm階段時,預設 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功
  3. Cancel 階段:在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放

在Try階段,是對業務系統進行檢查及資源預覽,比如訂單和庫存操作,需要檢查庫存剩餘數量是否夠用,並進行預留,預留操作的話就是新建一個可用庫存數量欄位,Try階段操作是對這個可用庫存數量進行操作。

例如下單減庫存的操作:

TCC下單減庫存

執行流程:

  1. Try階段:訂單系統將當前訂單狀態設定為支付中,庫存系統校驗當前剩餘庫存數量是否大於1,然後將可用庫存數量設定為庫存剩餘數量-1,
  2. 如果Try階段執行成功,執行Confirm 階段,將訂單狀態修改為支付成功,庫存剩餘數量修改為可用庫存數量
  3. 如果Try階段執行失敗,執行Cancel 階段,將訂單狀態修改為支付失敗,可用庫存數量修改為庫存剩餘數量

TCC 不存在資源阻塞的問題,因為每個方法都直接進行事務的提交,一旦出現異常通過則 Cancel 來進行回滾補償,這也就是常說的補償性事務。

但是,使用TCC,原本一個方法,現在卻需要三個方法來支援,可以看到 TCC 對業務的侵入性很強,而且這種模式並不能很好地被複用,會導致開發量激增。還要考慮到網路波動等原因,為保證請求一定送達都會有重試機制,所以還需要考慮介面的冪等性。

本地訊息表

本地訊息表的核心思想是將分散式事務拆分成本地事務進行處理。

例如,可以在訂單庫新增一個訊息表,將新增訂單和新增訊息放到一個事務裡完成,然後通過輪詢的方式去查詢訊息表,將訊息推送到MQ,庫存系統去消費MQ。

本地訊息表

執行流程:

  1. 訂單服務,新增一條訂單和一條訊息,在一個事務裡提交
  2. 訂單服務,使用定時任務輪詢查詢狀態為未同步的訊息表,傳送到MQ,如果傳送失敗,就重試傳送
  3. 庫存服務,接收MQ訊息,修改庫存表,需要保證冪等操作
  4. 如果修改成功,呼叫rpc介面修改訂單系統訊息表的狀態為已完成或者直接刪除這條訊息
  5. 如果修改失敗,可以不做處理,等待重試

訂單服務中的訊息有可能由於業務問題會一直重複傳送,所以為了避免這種情況可以記錄一下傳送次數,當達到次數限制之後報警,人工接入處理;庫存服務需要保證冪等,避免同一條訊息被多次消費造成資料不一致。

本地訊息表這種方案實現了最終一致性,需要在業務系統裡增加訊息表,業務邏輯中多一次插入的DB操作,所以效能會有損耗,而且最終一致性的間隔主要有定時任務的間隔時間決定。

MQ訊息事務

訊息事務的原理是將兩個事務通過訊息中介軟體進行非同步解耦。

訂單服務執行自己的本地事務,併傳送MQ訊息,庫存服務接收訊息,執行自己的本地事務,乍一看,好像跟本地訊息表的實現方案類似,只是省去 了對本地訊息表的操作和輪詢傳送MQ的操作,但實際上兩種方案的實現是不一樣的。

訊息事務一定要保證業務操作與訊息傳送的一致性,如果業務操作成功,這條訊息也一定投遞成功。

MQ訊息事務

執行流程:

  1. 傳送prepare訊息到訊息中介軟體
  2. 傳送成功後,執行本地事務
  3. 如果事務執行成功,則commit,訊息中介軟體將訊息下發至消費端
  4. 如果事務執行失敗,則回滾,訊息中介軟體將這條prepare訊息刪除
  5. 消費端接收到訊息進行消費,如果消費失敗,則不斷重試

訊息事務依賴於訊息中介軟體的事務訊息,例如我們熟悉的RocketMQ就支援事務訊息(半訊息),也就是隻有收到傳送方確定才會正常投遞的訊息。

這種方案也是實現了最終一致性,對比本地訊息表實現方案,不需要再建訊息表,對效能的損耗和業務的入侵更小。

最大努力通知

最大努力通知相比實現會簡單一些,適用於一些最終一致性要求較低的業務,比如支付通知,簡訊通知這種業務。

以支付通知為例,業務系統呼叫支付平臺進行支付,支付平臺進行支付,進行操作支付之後支付平臺會去同步通知業務系統支付操作是否成功,如果不成功,會一直非同步重試,但是會有一個最大通知次數,如果超過這個次數後還是通知失敗,就不再通知,業務系統自行呼叫支付平臺提供一個查詢介面,供業務系統進行查詢支付操作是否成功

最大努力通知

執行流程:

  1. 業務系統呼叫支付平臺支付介面, 並在本地進行記錄,支付狀態為支付中
  2. 支付平臺進行支付操作之後,無論成功還是失敗,同步給業務系統一個結果通知
  3. 如果通知一直失敗則根據重試規則非同步進行重試,達到最大通知次數後,不再通知
  4. 支付平臺提供查詢訂單支付操作結果介面
  5. 業務系統根據一定業務規則去支付平臺查詢支付結果

Saga事務

Saga事務,核心思想是將長事務拆分為多個本地短事務,由Saga事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次呼叫補償操作。

和本地事務undo log有點像,出問題了,逆向操作來挽救。

Sega簡介:

  • Saga = Long Live Transaction (LLT,長活事務)
  • LLT = T1 + T2 + T3 + ... + Ti(Ti為本地短事務)
  • 每個本地事務Ti 有對應的補償 Ci

Sega的執行順序:

  • 正常情況:T1 T2 T3 ... Tn
  • 異常情況:T1 T2 T3 C3 C2 C1

Saga兩種恢復策略

  • 向後恢復,如果任意本地子事務失敗,補償已完成的事務。如異常情況的執行順序T1 T2 Ti Ci C2 C1.
  • 向前恢復,即重試失敗的事務,假設最後每個子事務都會成功。執行順序:T1, T2, ..., Tj(失敗), Tj(重試),..., Tn。

舉個例子,假設使用者下訂單,花50塊錢購買了10瓶可樂,則有這麼一些短事務和回滾操作:

T1=下訂單 => T2=使用者扣50塊錢 => T3=使用者加10瓶可樂= > T4=庫存減10瓶可樂

C1=取消訂單 => C2= 給使用者加50塊錢 => C3 =使用者減10朵玫瑰 = > C4=庫存加10朵玫瑰

Sega事務

Seata

看了這麼些事務的方案,介紹了相關的原理,但是這些原理怎麼落地呢?各種各樣的坑怎麼處理呢?

—— 人生苦短,我用開源。

阿里巴巴開源了一套開源分散式事務解決方案——Seata。Seata可能並不稱之為完美,但對程式碼入侵性非常小,基本環境搭建完成的話,使用的時候在只需要方法上新增一個註解@GlobalTransactional就可以開啟全域性事務。

Seata 也是從兩段提交演變而來的一種分散式事務解決方案,提供了 ATTCCSAGAXA 等事務模式,我們來看一下AT模式。

Seata 中主要有這麼幾種角色:

TC(Transaction Coordinator):事務協調者。管理全域性的分支事務的狀態,用於全域性性事務的提交和回滾。

TM(Transaction Manager):事務管理者。用於開啟、提交或回滾事務。

RM(Resource Manager):資源管理器。用於分支事務上的資源管理,向 TC 註冊分支事務,上報分支事務的狀態,接收 TC 的命令來提交或者回滾分支事務。

我們看一下Seata大概的一個工作流程:

Seata

執行流程:

  1. 服務A中的 TMTC 申請開啟一個全域性事務,TC 就會建立一個全域性事務並返回一個唯一的 XID
  2. 服務A中的 RMTC 註冊分支事務,然後將這個分支事務納入 XID 對應的全域性事務管轄中
  3. 服務A開始執行分支事務
  4. 服務A開始遠端呼叫B服務,此時 XID 會根據呼叫鏈傳播
  5. 服務B中的 RM 也向 TC 註冊分支事務,然後將這個分支事務納入 XID 對應的全域性事務管轄中
  6. 服務B開始執行分支事務
  7. 全域性事務呼叫處理結束後,TM 會根據有誤異常情況,向 TC 發起全域性事務的提交或回滾
  8. TC 協調其管轄之下的所有分支事務,決定是提交還是回滾

關於Seata的使用,和更詳細的原理,這裡挖個坑,以後有時間再細講。

總結

上邊簡單介紹了 2PC3PCTCC本地訊息表最大努力通知MQSegaSeata 這8種分散式事務解決方案,但不管我們選哪一種方案,我們可以看到真要落地要考慮的點都很多,一個不慎,可能踩坑。

即使是看起來很省心的Seata,我之前的專案花了不少w買了它的商業化版本GTS,但是支援方仍然列出了一些“禁忌”,像長事務、大量資料、熱點資料、非同步呼叫等等,都可能會出現問題。

所以在專案中應用分散式事務要謹慎再謹慎,除非真的有一致性要求比較強的場景,能不用就儘量不用。

儘量別用分散式事務


如果覺得文章有幫助, 點贊關注收藏 素質三連,抱拳!



參考:

[1]. 再有人問你分散式事務,把這篇扔給他

[2]. 讓我們聊一聊分散式事務

[3]. 分散式事務,這一篇就夠了

[4].從分散式事務解決到Seata使用,一梭子給你整明白了

[5]. 看了 5種分散式事務方案,我司最終選擇了 Seata,真香!

[6]. 後端程式設計師必備:分散式事務基礎篇