「分佈式事務」,此次完全懂了!

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


image.png

圖片來自包圖網安全


好比每一個人只須要維護本身單獨的服務,沒有了之前的各類代碼衝突。本身想測試、想發佈、想升級,只須要 Care 本身寫的代碼就 OK 了,很方便很貼心!網絡


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


什麼是事務?分佈式


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


確實如此,事務就是爲了保證一系列操做能夠正常執行,它必須同時知足 ACID 特性。微服務


可是今天咱們換個角度思考下,咱們不只要知道 What(好比什麼是事務),更要知道事務的 Why(好比爲何會有事務這個概念?事務是爲了解決什麼問題)。測試


有時候,換個角度說不定有不同的收穫。spa


換個角度看事務代理


就像經典的文學做品均來自於生活,卻又高於生活,事務的概念一樣來自於生活,引入「事務」確定是爲了解決某種問題,否則,誰又願意幹這麼無聊的事情呢?


最簡單最經典的例子:銀行轉帳,咱們要從 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 註解來控制事務是如此的方便,那爲啥還要搞一個分佈式事務的概念出來啊?


更進一步,分佈式事務和普通事務究竟是啥關係?有什麼區別?分佈式事務又是爲了解決什麼問題出現的?


各類疑問接踵而至,彆着急,帶着這些思考,我們接下來就詳細聊聊分佈式事務。


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


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

image.png

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


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


可是當咱們把單塊系統拆分紅分佈式系統或者微服務架構,事務就不是上面那麼玩兒了。


首先咱們來看看拆分紅分佈式系統以後的架構圖,以下所示:

image.png

上圖是同一個操做在分佈式系統中的執行狀況。員工模塊、財務模塊和請假模塊分別給拆分紅員工系統、財務系統和請假系統。


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


這 3 個系統的一系列操做實際上是須要所有被包裹在同一個分佈式事務中的,此時這 3 個系統的操做,要麼同時成功要麼同時失敗。


分佈式系統中完成一個操做一般須要多個系統間協同調用和通訊,好比上面的例子。


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


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


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


所以你直接加 @Transactional 註解是不行的,由於它只能控制同一個 JVM 進程中的事務,可是對於這種跨多個 JVM 進程的事務無能無力。


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


搞清楚了啥是分佈式事務,那麼分佈式事務究竟是怎麼玩兒的呢?下邊就來給你們介紹幾種分佈式事務的實現方案。


可靠消息最終一致性方案


整個流程圖以下所示:

image.png

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

  • A 系統先發送一個 Prepared 消息到 MQ,若是這個 Prepared 消息發送失敗那麼就直接取消操做別執行了,後續操做都再也不執行。

  • 若是這個消息發送成功了,那麼接着執行 A 系統的本地事務,若是執行失敗就告訴 MQ 回滾消息,後續操做都再也不執行。

  • 若是 A 系統本地事務執行成功,就告訴 MQ 發送確認消息。

  • 那若是 A 系統遲遲不發送確認消息呢?此時 MQ 會自動定時輪詢全部 Prepared 消息,而後調用 A 系統事先提供的接口,經過這個接口反查 A 系統的上次本地事務是否執行成功。

    若是成功,就發送確認消息給 MQ;失敗則告訴 MQ 回滾消息。(後續操做都再也不執行)

  • 此時 B 系統會接收到確認消息,而後執行本地的事務,若是本地事務執行成功則事務正常完成。

  • 若是系統 B 的本地事務執行失敗了咋辦?基於 MQ 重試咯,MQ 會自動不斷重試直到成功,若是實在是不行,能夠發送報警由人工來手工回滾和補償。


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


由於通常執行失敗的緣由是網絡抖動或者數據庫瞬間負載過高,都是暫時性問題。


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


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


最大努力通知方案


整個流程圖以下所示:

image.png

這個方案的大體流程:

  • 系統 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 階段:若是任何一個服務的業務方法執行出錯,那麼這裏就須要進行補償,就是執行已經執行成功的業務邏輯的回滾操做。


仍是給你們舉個例子:

image.png

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

  • Try 階段:先把兩個銀行帳戶中的資金給它凍結住就不讓操做了。

  • Confirm 階段:執行實際的轉帳操做,A 銀行帳戶的資金扣減,B 銀行帳戶的資金增長。

  • Cancel 階段:若是任何一個銀行的操做執行失敗,那麼就須要回滾進行補償,就是好比 A 銀行帳戶若是已經扣減了,可是 B 銀行帳戶資金增長失敗了,那麼就得把 A 銀行帳戶資金給加回去。


適用場景:這種方案說實話幾乎不多有人使用,咱們用的也比較少,可是也有使用的場景。


由於這個事務回滾其實是嚴重依賴於你本身寫代碼來回滾和補償了,會形成補償代碼巨大,很是之噁心。


好比說咱們,通常來講跟錢相關的,跟錢打交道的,支付、交易相關的場景,咱們會用 TCC,嚴格保證分佈式事務要麼所有成功,要麼所有自動回滾,嚴格保證資金的正確性,在資金上不容許出現問題。


比較適合的場景:除非你是真的一致性要求過高,是你係統中核心之核心的場景,好比常見的就是資金類的場景,那你能夠用 TCC 方案了。


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


並且最好是你的各個業務執行的時間都比較短。可是說實話,通常儘可能別這麼搞,本身手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務代碼很難維護。


總結


本篇介紹了什麼是分佈式事務,而後還介紹了最經常使用的 3 種分佈式事務方案

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


可是說實話極少有公司使用這些方案,鑑於篇幅所限,不作介紹。後續若是有機會再出篇文章,詳細聊聊這兩種方案的思路。

相關文章
相關標籤/搜索