1. 程式人生 > 程式設計 >從0到1一文帶你瞭解分析分散式事務

從0到1一文帶你瞭解分析分散式事務

目錄

  1. 什麼是事務?
  2. 換個角度看事務
  3. Java 中的事務
  4. 什麼是分散式事務?
  5. 分散式事務的幾種實現思路
  6. 總結

前言

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

比如每個人只需要維護自己單獨的服務,沒有了以前的各種程式碼衝突。自己想測試、想釋出、想升級,只需要 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 註解來控制事務是如此的方便,那為啥還要搞一個分散式事務的概念出來啊? 更進一步,分散式事務和普通事務到底是啥關係?有什麼區別?分散式事務又是為瞭解決什麼問題出現的?

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

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

首先來看看下面的圖:

阿里Java崗二面:熟悉分散式事務?講講對分散式事務的理解和實現

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

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

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

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

阿里Java崗二面:熟悉分散式事務?講講對分散式事務的理解和實現

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

比如一個使用者進行一個操作,這個操作需要先呼叫員工系統預先處理一下,然後通過 http 或者 rpc 的方式分別呼叫財務系統和請假系統的介面做進一步的處理,它們的操作都需要分別落地到資料庫中。

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

分散式系統中完成一個操作通常需要多個系統間協同呼叫和通訊,比如上面的例子。

三個子系統:員工系統、財務系統、請假系統之間就通過 http 或者 rpc 進行通訊,而不再是一個單塊系統中不同模組之間的呼叫,這就是分散式系統和單塊系統最大的區別。

一些平時不太關注分散式架構的同學,看到這裡可能會說:我直接用 spring 的@Transactional 註解就 OK 了啊,管那麼多幹嘛!

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

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

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

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

可靠訊息最終一致性方案

整個流程圖如下所示:

阿里Java崗二面:熟悉分散式事務?講講對分散式事務的理解和實現

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

  1. A 系統先傳送一個 prepared 訊息到 mq,如果這個 prepared 訊息傳送失敗那麼就直接取消操作別執行了,後續操作都不再執行。
  2. 如果這個訊息傳送成功過了,那麼接著執行 A 系統的本地事務,如果執行失敗就告訴 mq 回滾訊息,後續操作都不再執行。
  3. 如果 A 系統本地事務執行成功,就告訴 mq 傳送確認訊息。
  4. 那如果 A 系統遲遲不傳送確認訊息呢? 此時 mq 會自動定時輪詢所有 prepared 訊息,然後呼叫 A 系統事先提供的介面,通過這個介面反查 A 系統的上次本地事務是否執行成功 如果成功,就傳送確認訊息給 mq;失敗則告訴 mq 回滾訊息(後續操作都不再執行)。
  5. 此時 B 系統會接收到確認訊息,然後執行本地的事務,如果本地事務執行成功則事務正常完成。
  6. 如果系統 B 的本地事務執行失敗了咋辦? 基於 mq 重試咯,mq 會自動不斷重試直到成功,如果實在是不行,可以傳送報警由人工來手工回滾和補償。 這種方案的要點就是可以基於 mq 來進行不斷重試,最終一定會執行成功的。 因為一般執行失敗的原因是網路抖動或者資料庫瞬間負載太高,都是暫時性問題。 通過這種方案,99.9%的情況都是可以保證資料最終一致性的,剩下的 0.1%出問題的時候,就人工修復資料唄。

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

最大努力通知方案

整個流程圖如下所示:

阿里Java崗二面:熟悉分散式事務?講講對分散式事務的理解和實現

這個方案的大致流程:

  1. 系統 A 本地事務執行完之後,傳送個訊息到 MQ。
  2. 這裡會有個專門消費 MQ 的最大努力通知服務,這個服務會消費 MQ,然後寫入資料庫中記錄下來,或者是放入個記憶體佇列。接著呼叫系統 B 的介面。
  3. 假如系統 B 執行成功就萬事 ok 了,但是如果系統 B 執行失敗了呢? 那麼此時最大努力通知服務就定時嘗試重新呼叫系統 B,反覆 N 次,最後還是不行就放棄。

這套方案和上面的可靠訊息最終一致性方案的區別:

可靠訊息最終一致性方案可以保證的是隻要系統 A 的事務完成,通過不停(無限次)重試來保證系統 B 的事務總會完成。

但是最大努力方案就不同,如果系統 B 本地事務執行失敗了,那麼它會重試 N 次後就不再重試,系統 B 的本地事務可能就不會完成了。

至於你想控制它究竟有“多努力”,這個需要結合自己的業務來配置。

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

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

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

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

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

tcc 強一致性方案

TCC 的全稱是:

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

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

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

還是給大家舉個例子:

阿里Java崗二面:熟悉分散式事務?講講對分散式事務的理解和實現

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

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

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

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

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

比較適合的場景:除非你是真的一致性要求太高,是你係統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用 TCC 方案了。 你需要自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否 ok,不 ok 就執行補償/回滾程式碼。

而且最好是你的各個業務執行的時間都比較短。

但是說實話,一般儘量別這麼搞,自己手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務程式碼很難維護。

總結

本篇介紹了什麼是分散式事務,然後還介紹了最常用的 3 種分散式事務方案。但除了上邊的方案外,其實還有兩階段提交方案(XA 方案)和本地訊息表等方案。但是說實話極少有公司使用這些方案,鑑於篇幅所限,不做介紹。

歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支援!