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

喬二爺發表於2019-04-20

背景

四月初,去面試了本市的一家之前在做辦公室無人貨架的公司,雖然他們現在在面臨著轉型,但是對於我這種想從傳統企業往網際網路行業走的孩子來說,還是比較有吸引力的。

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

先看面試過程

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

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

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

我回答了這中間可能存在分散式事務的問題,當步驟 2 在呼叫 B 系統時,可能存在B 系統處理完成後,在響應的時候超時,導致 A 系統誤認為 B 處理失敗了,從而導致A 系統回滾,跟 B 系統存在資料不一致的情況。

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

接著,他繼續問:“那你有什麼好的解決方式嗎?”

此時我腦子裡面只有兩階段提交的大概流程圖的印象,然後巴卡巴拉的跟他說了一番,什麼中間來個協調者呀,先預提交什麼的,如果有失敗,就 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了。

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

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

相關文章