到底啥是“分布式事務”,看了這篇文章,你就懂了!


本文是讀者有故事的驢投稿到石杉的架構筆記的文章,謝謝該同學分享

在分布式、微服務大行其道的今天,相信大家對這些名詞都不會陌生。而說到使用分布式,或者拆分微服務的好處,你肯定能想到一大堆。

比如每個人只需要維護自己單獨的服務,沒有了以前的各種代碼沖突。自己想測試、想發布、想升級,只需要 Care 自己寫的代碼就 OK 了,很方便很貼心!

然而事物都有兩面性,它同時也會帶來一些問題,今天的文章談的就是分布式系統架構帶來的其中一個棘手的問題:分布式事務!

什么是事務?

首先拋出來一個問題:什么是事務?有人會說事務就是一系列操作,要么同時成功,要么同時失敗;然后會從事務的 ACID 特性(原子性、一致性、隔離性、持久性)展開敘述。

確實如此,事務就是為了保證一系列操作可以正常執行,它必須同時滿足 ACID 特性。

但是今天我們換個角度思考下,我們不僅要知道 What(比如什么是事務),更要知道事務的 Why(比如為什么會有事務這個概念?事務是為了解決什么問題)。

有時候,換個角度說不定有不一樣的收獲。

換個角度看事務

就像經典的文學作品均來自於生活,卻又高於生活,事務的概念同樣來自於生活,引入“事務”肯定是為了解決某種問題,不然,誰又願意干這么無聊的事情呢?

最簡單最經典的例子:銀行轉賬,我們要從 A 賬戶轉 1000 塊到 B 賬戶。

正常情況下如果從 A 轉出 1000 到 B 賬戶之后,A 賬戶余額減 1000(這個操作我們用 Action1 代表),B 賬戶余額加 1000(這個操作我們用 Action2 代表)

首先我們要明確一點,Action1 和 Action2 是兩個操作。既然是兩個操作那么就一定會存在執行的先后順序。

那么就可能會出現 Action1 執行完剛准備去執行 Action2 的時候出問題了(比如數據庫負載過大暫時拒絕訪問)。

類比到我們生活中,那就是我給朋友轉了 1000 塊錢,然后我卡里的余額少了 1000,但是我朋友卻沒有收到錢。

為解決這種“Money 去哪兒了”的問題,引入了“事務”的概念。也就是說,既然我轉賬的時候你保證不了 100% 能成功,比如銀行系統只能保證 99.99% 的高可用,那么在那 0.01% 的時間里如果出現了上述問題,銀行系統直接回滾 Action1 操作?(即把 1000 塊錢再加回余額中去)

對於銀行系統來說,可能在 0.01% 的時間里我保證不了 Action1 和 Action2 同時成功,那么在出問題的時候,我保證它倆同時失敗。(事務的原子性)

通過這個例子,就已經回答了剛開始提出的 2 個問題(為什么會有事務?事務是為了解決什么問題?)

總結一下:事務就是通過它的 ACID 特性,保證一系列的操作在任何情況下都可以安全正確的執行。

Java 中的事務

搞清楚了事務之后,我們來看點眼熟的,Java 中的事務是怎么玩的?

Java 中我們平時用的最多的就是在 Service 層的增刪改方法上添加 @Transactional 注解,讓 Spring 去幫我們管理事務。

它底層會給我們的 Service 組件生成一個對應的 Proxy 動態代理,這樣所有對 Service 組件的方法都由它對應的 Proxy 來接管。

當 Proxy 在調用對應業務方法比如 add() 時,Proxy 就會基於 AOP 的思想在調用真正的業務方法前執行 setAutoCommit(false)打開事務。

然后在業務方法執行完后執行 Commit 提交事務,當在執行業務方法的過程中發生異常時就會執行 Rollback 來回滾事務。

當然 @Transactional 注解具體的實現細節這里不再展開,這個不是本篇文章的重點,本文的 Topic 是“分布式事務”,關於 @Transactional 注解大家有興趣的話,可以自己打斷點 Debug 源碼研究下,源碼出真知。

啥又是分布式事務?

鋪墊了辣么久,終於到了本篇的第一個重點!首先大家想過沒:既然有了事務,並且使用 Spring 的 @Transactional 注解來控制事務是如此的方便,那為啥還要搞一個分布式事務的概念出來啊?

更進一步,分布式事務和普通事務到底是啥關系?有什么區別?分布式事務又是為了解決什么問題出現的?

各種疑問接踵而至,別着急,帶着這些思考,咱們接下來就詳細聊聊分布式事務。

既然叫分布式事務,那么必然和分布式有點關系啦!簡單來說,分布式事務指的就是分布式系統中的事務。

好,那咱們繼續,首先來看看下面的圖:

如上圖所示,一個單塊系統有 3 個模塊:員工模塊、財務模塊和請假模塊。我們現在有一個操作需要按順序去調用完成這 3 個模塊中的接口。

這個操作是一個整體,包含在一個事務中,要么同時成功要么同時失敗回滾。不成功便成仁,這個都沒有問題。

但是當我們把單塊系統拆分成分布式系統或者微服務架構,事務就不是上面那么玩兒了。

首先我們來看看拆分成分布式系統之后的架構圖,如下所示:

1561715985841

上圖是同一個操作在分布式系統中的執行情況。員工模塊、財務模塊和請假模塊分別給拆分成員工系統、財務系統和請假系統。

比如一個用戶進行一個操作,這個操作需要先調用員工系統預先處理一下,然后通過 HTTP 或者 RPC 的方式分別調用財務系統和請假系統的接口做進一步的處理,它們的操作都需要分別落地到數據庫中。

這 3 個系統的一系列操作其實是需要全部被包裹在同一個分布式事務中的,此時這 3 個系統的操作,要么同時成功要么同時失敗。

分布式系統中完成一個操作通常需要多個系統間協同調用和通信,比如上面的例子。

三個子系統:員工系統、財務系統、請假系統之間就通過 HTTP 或者 RPC 進行通信,而不再是一個單塊系統中不同模塊之間的調用,這就是分布式系統和單塊系統最大的區別。

一些平時不太關注分布式架構的同學,看到這里可能會說:我直接用 Spring 的 @Transactional 注解就 OK 了啊,管那么多干嘛!

但是這里極其重要的一點:單塊系統是運行在同一個 JVM 進程中的,但是分布式系統中的各個系統運行在各自的 JVM 進程中。

因此你直接加 @Transactional 注解是不行的,因為它只能控制同一個 JVM 進程中的事務,但是對於這種跨多個 JVM 進程的事務無能無力。

分布式事務的幾種實現思路

搞清楚了啥是分布式事務,那么分布式事務到底是怎么玩兒的呢?下邊就來給大家介紹幾種分布式事務的實現方案。

可靠消息最終一致性方案

整個流程圖如下所示:

1561716009605

我們來解釋一下這個方案的大概流程:

  • A 系統先發送一個 Prepared 消息到 MQ,如果這個 Prepared 消息發送失敗那么就直接取消操作別執行了,后續操作都不再執行。
  • 如果這個消息發送成功了,那么接着執行 A 系統的本地事務,如果執行失敗就告訴 MQ 回滾消息,后續操作都不再執行。
  • 如果 A 系統本地事務執行成功,就告訴 MQ 發送確認消息。
  • 那如果 A 系統遲遲不發送確認消息呢?此時 MQ 會自動定時輪詢所有 Prepared 消息,然后調用 A 系統事先提供的接口,通過這個接口反查 A 系統的上次本地事務是否執行成功。
  • 如果成功,就發送確認消息給 MQ;失敗則告訴 MQ 回滾消息。(后續操作都不再執行)
  • 此時 B 系統會接收到確認消息,然后執行本地的事務,如果本地事務執行成功則事務正常完成。
  • 如果系統 B 的本地事務執行失敗了咋辦?基於 MQ 重試咯,MQ 會自動不斷重試直到成功,如果實在是不行,可以發送報警由人工來手工回滾和補償。

這種方案的要點就是可以基於 MQ 來進行不斷重試,最終一定會執行成功的。

因為一般執行失敗的原因是網絡抖動或者數據庫瞬間負載太高,都是暫時性問題。

通過這種方案,99.9% 的情況都是可以保證數據最終一致性的,剩下的 0.1% 出問題的時候,就人工修復數據唄。

適用場景:這個方案的使用還是比較廣,目前國內互聯網公司大都是基於這種思路玩兒的。

最大努力通知方案

整個流程圖如下所示:

1561716023324

這個方案的大致流程:

  • 系統 A 本地事務執行完之后,發送個消息到 MQ。
  • 這里會有個專門消費 MQ 的最大努力通知服務,這個服務會消費 MQ,然后寫入數據庫中記錄下來,或者是放入個內存隊列。接着調用系統 B 的接口。
  • 假如系統 B 執行成功就萬事 OK 了,但是如果系統 B 執行失敗了呢?
  • 那么此時最大努力通知服務就定時嘗試重新調用系統 B,反復 N 次,最后還是不行就放棄。

這套方案和上面的可靠消息最終一致性方案的區別:可靠消息最終一致性方案可以保證的是只要系統 A 的事務完成,通過不停(無限次)重試來保證系統 B 的事務總會完成。

但是最大努力方案就不同,如果系統 B 本地事務執行失敗了,那么它會重試 N 次后就不再重試,系統 B 的本地事務可能就不會完成了。至於你想控制它究竟有“多努力”,這個需要結合自己的業務來配置。

比如對於電商系統,在下完訂單后發短信通知用戶下單成功的業務場景中,下單正常完成,但是到了發短信的這個環節由於短信服務暫時有點問題,導致重試了 3 次還是失敗。

那么此時就不再嘗試發送短信,因為在這個場景中我們認為 3 次就已經算是盡了“最大努力”了。

簡單總結:就是在指定的重試次數內,如果能執行成功那么皆大歡喜,如果超過了最大重試次數就放棄,不再進行重試。

適用場景:一般用在不太重要的業務操作中,就是那種完成的話是錦上添花,但失敗的話對我也沒有什么壞影響的場景。

比如上邊提到的電商中的部分通知短信,就比較適合使用這種最大努力通知方案來做分布式事務的保證。

TCC 強一致性方案

TCC的 全稱是:

  • Try(嘗試)
  • Confirm(確認/提交)
  • Cancel(回滾)

這個其實是用到了補償的概念,分為了三個階段:

  • Try 階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留。
  • Confirm 階段:這個階段說的是在各個服務中執行實際的操作。
  • Cancel 階段:如果任何一個服務的業務方法執行出錯,那么這里就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作。

還是給大家舉個例子:

1561716039146

比如跨銀行轉賬的時候,要涉及到兩個銀行的分布式事務,如果用 TCC 方案來實現,思路是這樣的:

  • Try 階段:先把兩個銀行賬戶中的資金給它凍結住就不讓操作了。
  • Confirm 階段:執行實際的轉賬操作,A 銀行賬戶的資金扣減,B 銀行賬戶的資金增加。
  • Cancel 階段:如果任何一個銀行的操作執行失敗,那么就需要回滾進行補償,就是比如 A 銀行賬戶如果已經扣減了,但是 B 銀行賬戶資金增加失敗了,那么就得把 A 銀行賬戶資金給加回去。

適用場景:這種方案說實話幾乎很少有人使用,我們用的也比較少,但是也有使用的場景。

因為這個事務回滾實際上是嚴重依賴於你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之惡心。

比如說我們,一般來說跟錢相關的,跟錢打交道的,支付、交易相關的場景,我們會用 TCC,嚴格保證分布式事務要么全部成功,要么全部自動回滾,嚴格保證資金的正確性,在資金上不允許出現問題。

比較適合的場景:除非你是真的一致性要求太高,是你系統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用 TCC 方案了。

你需要自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否 OK,不 OK 就執行補償/回滾代碼。

而且最好是你的各個業務執行的時間都比較短。但是說實話,一般盡量別這么搞,自己手寫回滾邏輯,或者是補償邏輯,實在太惡心了,那個業務代碼很難維護。

總結

本篇介紹了什么是分布式事務,然后還介紹了最常用的 3 種分布式事務方案

但除了上邊的方案外,其實還有兩階段提交方案(XA 方案)和本地消息表等方案。

但是說實話極少有公司使用這些方案,鑒於篇幅所限,不做介紹。后續如果有機會再出篇文章,詳細聊聊這兩種方案的思路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM