面試必備的分佈式事務方案

背景

四月初,去面試了本市的一家以前在作辦公室無人貨架的公司,雖然他們如今在面臨着轉型,可是對於我這種想從傳統企業往互聯網行業走的孩子來講,仍是比較有吸引力的。java

在面試過程當中就提到了分佈式事務問題。我又一次在沒有好好整理的問題上吃了虧,記錄一下,仍是長記性 !!!git

先看面試過程

面試官先是在紙上先畫了這樣一張圖: github

讓我看這張圖按照上面的流程走,有沒有什麼問題?面試官並無直接說出來這裏面會有分佈式事務的問題,而是讓我來告訴他,這就是面試套路呀web

我回答了這中間可能存在分佈式事務的問題,當步驟 2 在調用 B 系統時,可能存在B 系統處理完成後,在響應的時候超時,致使 A 系統誤認爲 B 處理失敗了,從而致使A 系統回滾,跟 B 系統存在數據不一致的狀況。面試

ok ,我回答到這裏,應該回答了面試官的第一層意思,至少我有這種意識,他點了點頭。sql

接着,他繼續問:「那你有什麼好的解決方式嗎?」數據庫

此時我腦子裏面只有兩階段提交的大概流程圖的印象,而後巴卡巴拉的跟他說了一番,什麼中間來個協調者呀,先預提交什麼的,若是有失敗,就 rollback,若是 ok,再真正的提交事務,就是網上這些大神們說的這些理論。服務器

而後面試官就繼續問:那A 在調用 B 的這條線斷了,大家代碼具體是怎麼處理的呢 ?怎麼來作到 rollback 的呢 ?說說你代碼怎麼寫的。網絡

此時,我懵了。併發

最後結果,你們確定也能猜到,涼涼。

什麼是事務

這裏咱們說的事務通常是指 數據庫事務,簡稱 事務,是數據庫管理系統執行過程當中的一個邏輯單位,由一個有限的數據庫操做序列構成。維基百科中這麼說的。

用轉帳的例子來講,A 帳戶要給 B 帳戶轉 100塊,這中間至少包含了兩個操做:

  1. A 帳戶 減 100 塊
  2. B 帳戶 加 100 塊

在支持事務的數據庫管理系統來講,就是得確保上面兩個操做(整個「事務」)都能完成,不能存在,A 的100塊扣了,而後B 的帳戶又沒加上去的狀況。

數據庫事務包含了四個特性,分別是:

  • 原子性(Atomicity):事務做爲一個總體被執行,包含在其中的對數據庫的操做要麼所有被執行,要麼都不執行。對於轉帳來講,A帳戶扣錢,B 帳戶加錢,要麼同時成功,要麼同時失敗。
  • 一致性(Consistency):事務應確保數據庫的狀態從一個一致狀態轉變爲另外一個一致狀態。一致狀態的含義是數據庫中的數據應知足完整性約束
  • 隔離性(Isolation):多個事務併發執行時,一個事務的執行不該影響其餘事務的執行。其餘帳戶在轉帳的時候,不能影響到上面的 A 跟 B 以前的交易。
  • 持久性(Durability):已被提交的事務對數據庫的修改應該永久保存在數據庫中。

什麼是分佈式事務

咱們知道,上面的轉帳 咱們是在一個數據庫中的事務操做。咱們可使用一些框架 好比 Spring 的事務管理器來給咱們統一搞定。

可是若是咱們系統中出現垮庫操做,好比一個操做中,我須要操做多個庫,或者說這個操做會垮應用以前的調用,那麼Spring 的事務管理機制就對這種場景沒有辦法了。

就像上面面試題中出現的問題同樣,在系統 A 的步驟 2 在遠程調用 B 的時候,因爲網絡超時,致使B 沒有正常響應,可是A 這邊調用失敗,進行了回滾,而 B 又提交了事務。此時就可能會致使數據不一致的狀況,參生分佈式事務的問題。

分佈式事務的存在,就是解決數據不一致的狀況。

爲何咱們要保證一致性

CAP 理論

分佈式系統中有這麼一個廣爲流傳的理論:CAP 定理

這個定理呀,起源於加州大學柏克萊分校(University of California, Berkeley)的計算機科學家埃裏克·布魯爾在 2000年的分佈式計算原理研討會(PODC)上提出的一個猜測。後來在2002年,麻省理工學院(MIT)的賽斯·吉爾伯特和南希·林奇發表了布魯爾猜測的證實,使之成爲一個定理。【摘自維基百科】

他說呀,對於一個分佈式計算系統來講,不可能同時知足如下三點:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分區容錯性(Partition tolerance)

而一個分佈式系統最多隻能知足其中的兩項。

那麼,上面的三點分別是什麼玩意兒?爲何又只能同時知足兩項呢?

咱們先看這樣一個場景,如今咱們系統部署了兩份(兩個節點,web1 和 web2 ),一樣的業務代碼,可是維護的是本身這個節點生成的數據。可是用戶訪問進來,可能會訪問到不一樣的節點。可是無論是訪問web1 仍是web2 ,在用戶參數數據 事後,這個數據都必須得同步到另外的節點,讓用戶無論訪問哪一個節點,都是響應他須要的數據。以下圖:

2.png

分區容錯性

咱們先說 分區容錯性:也就是說呀,就算上面這兩個節點之間發生了網絡故障,沒法發生同步的問題,可是用戶訪問進來,無論到哪一個節點,這個節點都得單獨提供服務,這一點對於互聯網公司來講,是必需要知足的。

當 web1 和 web2 之間的網絡發生故障,致使數據沒法進行同步。用戶在web1 上寫了數據,立刻又訪問進來讀取數據,請求到了 web2,可是此時 web2 是沒有數據的。那麼咱們是 給用戶返回 null ?仍是說給一些提示,說系統不可用,稍後重試呢?

都不妥吧,兄弟。

一致性

若是要保證可用性,那麼有數據的節點返回數據,沒數據的節點返回 null ,就會出現用戶那裏看到的是一下子有數據,一下子沒有數據,此時就存在 一致性 的問題。

可用性

若是保證一致性,那麼在用戶訪問的時候,無論 web1 仍是web2 ,咱們可能會返回一些提示信息,說系統不可用,稍後再試等等,保證每次都是一致的。明明咱們有數據在,可是咱們系統卻響應的是提示信息,此時就是 可用性 的問題。

因爲分區容錯性(P)是必須保證的,那麼咱們分佈式系統就更可能是在一致性(CP) 和可用性(AP)上作平衡了,只能同時知足兩個條件。

其實,你們想一想,ZK 是否是就是嚴格實現了 CP ,而 Eureka 則是保證了 AP。

其實分佈式事務強調的就是一致性。

幾種分佈式事務解決方案

2PC

在說 2PC 以前,咱們先了解一下 XA規範 是個什麼東西?

XA規範 描述了全局的事務管理器與局部的資源管理器之間的接口。XA規範的目的是容許多個資源(如數據庫,應用服務器,消息隊列,等等)在同一事務中訪問,這樣可使ACID屬性跨越應用程序而保持有效。

XA 使用 兩階段提交(2PC) 來保證全部資源同時提交或回滾任何特定的事務。

你們想一個場景,在作單應用的時候,有的同窗連過兩個庫吧?在一個事務中會同時向兩個系統插入數據。可是對於普通事務來說,是管不了的。

看下圖(只是舉例這種操做的套路,不侷限於下面的業務):

3.png

一個服務裏面要去操做兩個庫,如何保證事務成功呢。

這裏咱們介紹一個框架 Atomikos ,他就是實現了這種 XA 的套路。看代碼:

4.png

具體代碼移步 Github AtomikosJTATest: github.com/heyxyw/lear…

看到上面的圖了哇,Atomikos 本身實現了一個事務管理器。咱們獲取的鏈接都從它哪裏拿。

  • 第一步先開啓事務,而後進行預提交,db1 和 db2 都先進行預先執行,注意:這裏沒有提交事務
  • 第二步纔是真正的提交事務,由 Atomikos 來發起提交的,若是出現異常則發起回滾操做。

這個過程是否是就有兩個角色了,一個 事務管理器,一個資源管理器(咱們這裏是 數據庫,也能夠是其餘的組件,消息隊列什麼的)。

整個執行過程是這樣:

5.png
上圖是正常狀況,下圖是一方出現故障的狀況。

6.png

圖片來自:XA 事務處理www.infoq.cn/article/xa-… ,具體關於XA 的詳細講解,能夠好好看看。整個2PC 的流程:

第一階段(提交請求階段)

  1. 協調者節點向全部參與者節點詢問是否能夠執行提交操做,並開始等待各參與者節點的響應。
  2. 參與者節點執行詢問發起爲止的全部事務操做,並將Undo信息和Redo信息寫入日誌。
  3. 各參與者節點響應協調者節點發起的詢問。若是參與者節點的事務操做實際執行成功,則它返回一個"贊成"消息;若是參與者節點的事務操做實際執行失敗,則它返回一個"停止"消息。

第二階段 (提交執行階段)

成功,當協調者節點從全部參與者節點得到的相應消息都爲"贊成"時:

  1. 協調者節點向全部參與者節點發出"正式提交"的請求。
  2. 參與者節點正式完成操做,並釋放在整個事務期間內佔用的資源。
  3. 參與者節點向協調者節點發送"完成"消息。
  4. 協調者節點收到全部參與者節點反饋的"完成"消息後,完成事務。

失敗,若是任一參與者節點在第一階段返回的響應消息爲"終止",或者 協調者節點在第一階段的詢問超時以前沒法獲取全部參與者節點的響應消息時:

  1. 協調者節點向全部參與者節點發出"回滾操做"的請求。
  2. 參與者節點利用以前寫入的Undo信息執行回滾,並釋放在整個事務期間內佔用的資源。
  3. 參與者節點向協調者節點發送"回滾完成"消息。
  4. 協調者節點收到全部參與者節點反饋的"回滾完成"消息後,取消事務。

有時候,第二階段也被稱做完成階段,由於不管結果怎樣,協調者都必須在此階段結束當前事務。

可靠消息最終一致性方案

基於普通的消息隊列中間件

上面咱們說了兩階段提交的方案,接下來咱們講講怎麼基於可靠消息最終一致性方案來解決分佈式事務的問題。

這個方案,就有消息服務中間件角色參與進來了。咱們先看一個大提的流程圖:

7.png

咱們以建立訂單下單過程和 後面出庫 的流程爲例來說述上面的圖。

在下單邏輯裏面(Producer 端),咱們先生成一個訂單的數據,好比訂單號,數量等關鍵的信息,先包裝成一條消息,並把消息的狀態置爲 init ,而後發送到 獨立消息服務中,而且入庫。

接下來繼續處理 下單的其餘本地的邏輯。

處理完成後,走到確認發送消息這一步,說明個人訂單是可以下成功的。那麼咱們再向消息服務裏面發送一條confirm 的消息,消息服務裏面就能夠把這個訂單的消息狀態修改成 send 而且,發送到消息隊列裏面。

接下來,消費者端去消費這條消息。處理本身這邊的邏輯,處理完成之後,而後反饋消息處理結果到獨立消息服務,獨立消息服務把消息狀態置爲 end 狀態 ,表示結束。可是得注意保證接口的冪等性,避免重複消費帶來的問題。

這裏面可能出現的問題,以及各個步驟怎麼解決的:

  1. 好比在 prepare 階段就發生異常,那麼這裏訂單這塊都不會下成功。可是咱們說,咱們這裏是基於可靠消息,得保證咱們的消息服務是正常的。
  2. comfirm 出現異常,此時發送確認失敗,可是咱們的單已經下成功了。這種狀況,咱們就能夠在獨立消息服務中起一個定時任務,定時去查詢 消息狀態爲 init 的數據,去反向查詢 訂單系統中的單號是否存在,若是存在,那麼咱們就把消息置爲 send 狀態,而後發送到 消息隊列裏面,若是查詢到不存在的訂單,那麼就直接拋棄掉這條消息。因此這裏咱們的訂單系統得提供批量查詢訂單的接口,還有下游的消費系統得保證冪等。保證重複消費的一致性。
  3. 消息隊列丟消息或者下游系統一直處理失敗,致使沒有消息反饋過來,出現一直是 send 狀態的消息。此時獨立消息咱們還須要一個定時任務,就是處理這種 send 狀態的消息,咱們能夠進行重發,直到後面系統消費成功爲止。
  4. 最後消費者這端,咱們在消費的時候,若是出現消費異常,或者是系統bug 致使異常的狀況。那麼這裏咱們還能夠去記錄日誌,若是不是系統代碼問題,是網絡抖動致使的,那麼在上面第三種狀況,消息系統會從新發送消息,咱們再處理就是。若是是一直失敗,你就要考慮是否是你的代碼真的有問題,有bug 了吧。
  5. 最後的保底方案,記錄日誌,出現問題人肉處理數據。如今咱們系統出現錯誤,以目前的技術手段是沒辦法作到都靠機器去解決的,都得靠咱們人。據我瞭解,如今不少大廠都會有這樣的人,專門處理這種類型的問題,手動去修改數據庫的方式。咱們以前待的小廠,基本上都是靠咱們本身去寫 sql 去修改數據的,想一想,是否是?

貼一下關鍵的獨立消息服務核心邏輯代碼框架

8.png

定時任務

9.png

基於 RocketMQ實現

這種方案,跟上面的獨立消息服務一致,這裏直接去掉獨立服務,只利用消息隊列來實現,也就是阿里的 RocketMQ

流程圖以下:

10.png

這裏的整個流程跟上面基於消息服務是一致的。這裏就不過多闡述,具體代碼實現請參考 :www.jianshu.com/p/453c6e7ff… ,寫得很是好。

針對這裏的 可靠消息最終一致性方案 來講,咱們說的 可靠 是指保證消息必定能發送到消息中間件裏面去,保證這裏可靠。

對於下游的系統來講,消費不成功,通常來講就是採起失敗重試,重試屢次不成功,那麼就記錄日誌,後續人工介入來進行處理。因此這裏得強調一下,後面的系統,必定要處理 冪等重試日誌 這幾個東西。

若是是對於資金類的業務,後續系統回滾了之後,得想辦法去通知前面的系統也進行回滾,或者是發送報警由人工來手工回滾和補償。

TCC 方案

TCC 的全程分爲三個階段,分別是 Try、Confirm、Cancel:

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

仍是以轉帳的例子爲例,在跨銀行進行轉帳的時候,須要涉及到兩個銀行的分佈式事務,從A 銀行向 B 銀行轉 1 塊,若是用TCC 方案來實現:

11.png

大概思路就是這樣的:

  1. Try 階段:先把A 銀行帳戶先凍結 1 塊,B銀行帳戶中的資金給預加 1 塊。
  2. Confirm 階段:執行實際的轉帳操做,A銀行帳戶的資金扣減 1塊,B 銀行帳戶的資金增長 1 塊。
  3. Cancel 階段:若是任何一個銀行的操做執行失敗,那麼就須要回滾進行補償,就是好比A銀行帳戶若是已經扣減了,可是B銀行帳戶資金增長失敗了,那麼就得把A銀行帳戶資金給加回去。

這種方案就比較複雜了,一步操做要作多個接口來配合完成。

以 ByteTCC 框架的實現例子來大概描述一下上面的流程,示例地址 gitee.com/bytesoft/By…

最開始 A 銀行帳戶 與 B 銀行帳戶都分別爲:amount(數量)=1000,frozen(凍結金額)= 0

從A銀行帳戶發起轉帳到 B 銀行帳戶 1 塊:

12.png

try 階段:A 銀行帳戶金額減 1,凍結金額 加 1,B 銀行 帳戶 凍結金額加 1 。

13.png

此時:

  • A 銀行帳戶:amount(數量)= 1000 - 1 = 999,frozen(凍結金額)= 0 + 1 = 1
  • B 銀行帳戶:amount(數量)= 1000,frozen(凍結金額)= 0 + 1 = 1

confirm 階段 : A銀行帳戶凍結金額 減 1,B 銀行帳戶金額 加 1,凍結金額 減 1

14.png

此時:

  • A 銀行帳戶:amount(數量)= 999,frozen(凍結金額)= 1 - 1 = 0
  • B 銀行帳戶:amount(數量)= 1000 + 1 = 1001,frozen(凍結金額)= 1 - 1 = 0

cancel 階段: A 銀行帳戶金額 + 1,凍結金額 -1 ,B 銀行 凍結金額 -1

15.png

此時:

  • A 銀行帳戶:amount(數量)= 999 + 1 = 1000,frozen(凍結金額)= 1 - 1 = 0
  • B 銀行帳戶:amount(數量)= 1000,frozen(凍結金額)= 1 - 1 = 0

至此,整個過程就演示完畢,你們記得跑一遍代碼。其實仍是蠻複雜的,有許多接口一塊兒來配合完成整個業務,試想一下,若是咱們項目中大量用到 TCC 來寫,你受得了?

再提一下 BASE理論

BASE 理論是 Basically Available(基本可用),Soft State(軟狀態)和Eventually Consistent(最終一致性)三個短語的縮寫。

  1. 基本可用(Basically Available): 指分佈式系統在出現不可預知故障的時候,容許損失部分可用性。
  2. 軟狀態( Soft State):指容許系統中的數據存在中間狀態,並認爲該中間狀態的存在不會影響系統的總體可用性,即容許系統在不一樣節點的數據副本之間進行數據同步的過程存在延時。
  3. 最終一致( Eventual Consistency):強調的是全部的數據更新操做,在通過一段時間的同步以後,最終都可以達到一個一致的狀態。所以,最終一致性的本質是須要系統保證最終數據可以達到一致,而不須要實時保證系統數據的強一致性。

其核心思想是:

即便沒法作到強一致性(Strong consistency),但每一個應用均可以根據自身的業務特色,採用適當的方式來使系統達到最終一致性(Eventual consistency)

到這裏你們再想一想, 上面 TCC 方案中的帳戶設計了一個凍結字段 frozen ,這裏是否是就是 BASE理論 中間的 軟狀態 呢 ?

最後

對存在很是多的微服務的公司來講,服務之間的調用異常的複雜,那麼在引入分佈式事務的過程當中,你須要考慮加入分佈式事務後,系統實現起來的複雜性和開發成本,或者說哪些地方根本就不須要搞分佈式事務。

其實不必處處都搞分佈式事務,對於大多數的業務來講,其實咱們並不須要作分佈式事務,直接作日誌,作監控就行了。而後出現問題,手工去處理,一個月也不會有那麼多的問題的。若是你每天都出現這些問題,你是否是要好好去排查排查你的代碼Bug了。

對於資金類的場景,那麼基本上會採用分佈式事務方案來保證,像其餘的服務,會員,積分,商品信息呀這些,可能就不須要這麼去搞了。

相關文章
相關標籤/搜索