你必須瞭解的分散式事務解決方案

靚仔聊程式設計發表於2021-08-16

前言

上一篇文章《就這?分散式 ID 發號器實戰》之後,我朋友輝哥在後臺留言讓靚仔聊聊分散式事務,既然輝哥都開口了,那必須得滿足啊,安排!

溫馨提示:文章很乾,請多喝水

img

什麼是分散式事務

什麼是事務想必大多數朋友應該都很清楚了,不清楚的可以看前面的文章《就這?一篇文章讓你讀懂 Spring 事務》。

分散式事務就是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。

簡單來說,就是一個大的操作由 N 個小操作組成,這些小的操作分佈在不同的伺服器上,且屬於不同的應用,分散式事務需要保證這些小操作要麼全部成功,要麼全部失敗。比如存在一個訂單的微服務,一個庫存的微服務,當訂單完成需要同步減少庫存,這時候就要在事務上確保完整和一致。

相關理論

關於事務的特性(ACID)隔離級別這裡就不再重複介紹了,可以看前面的文章,這裡著重介紹下兩個新的知識:CAP 理論BASE 理論。

CAP 理論

  • 一致性Consistency)在分散式系統完成某寫操作後任何讀操作,都應該獲取到該寫操作寫入的那個最新的值。相當於要求分散式系統中的各節點時時刻刻保持資料的一致性。
  • 可用性Availability) 一直可以正常的做讀寫操作。簡單而言就是客戶端一直可以正常訪問並得到系統的正常響應。使用者角度來看就是不會出現系統操作失敗或者訪問超時等問題。
  • 分割槽容錯性PartitionTolerance)指的分散式系統中的某個節點或者網路分割槽出現了故障的時候,整個系統仍然能對外提供滿足一致性和可用性的服務。也就是說部分故障不影響整體使用。事實上我們在設計分散式系統是都會考慮到 bug、硬體、網路等各種原因造成的故障,所以即使部分節點或者網路出現故障,我們要求整個系統還是要繼續使用的

CAP 是一個已經被證實的理論,在分散式系統中最多隻能同時滿足這三項中的兩項,而分割槽容錯性是分散式系統必須滿足的,所以在分散式系統中常見的組合就是 CP 和 AP

  • CP:放棄可用性,注重一致性和分割槽容錯性,其實這就是所謂的強一致性,可能在銀行跨行轉賬這種強一致業務場景才會用到,具體得根據業務場景做取捨。
  • AP:放棄強一致性,注重可用性和分割槽容錯性,這是現在絕大多數分散式業務場景的選擇,只要最後能保證最終一致性( BASE 理論)即可。

BASE 理論

  • 基本可用Basically Available)基本可用是指分散式系統在出現故障的時候,允許損失部分可用性,即保證核心可用。電商大促時,為了應對訪問量激增,部分使用者可能會被引導到降級頁面,服務層也可能只提供降級服務。這就是損失部分可用性的體現。

  • 軟狀態Soft State)軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分散式儲存中一般一份資料至少會有三個副本,允許不同節點間副本同步的延時就是軟狀態的體現。MySQL Replication 的非同步複製也是一種體現。

    最終一致性Eventual Consistency)最終一致性是指系統中的所有資料副本經過一定時間後,最終能夠達到一致的狀態。弱一致性和強一致性相反,最終一致性是弱一致性的一種特殊情況。

常見解決方案

1、兩階段提交

兩階段提交(Two-phaseCommit),簡稱為 2PC,兩階段提交是一種強一致性設計,它引入一個事務協調者的角色來協調管理各個參與者(也可稱之為各本地資源)的提交和回滾。

所謂的兩個階段是指:第一階段:準備階段(投票階段)和第二階段:提交階段(執行階段)。

  • 準備階段(Prepare Phase):首先協調器會向所有的參與者傳送準備提交或者取消提交的請求,然後會收集參與者的決策。

  • 提交階段(Commit Phase):協調者會收集所有參與者的決策資訊,當且僅當所有的參與者向協調器傳送確認訊息時協調器才會提交請求,否則執行回滾或者取消請求。

img

2PC 存在的問題:

  • 同步阻塞:所有的參與者都是事務同步阻塞型的。當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。
  • 單點故障:一旦協調者發生故障,系統不可用。
  • 資料不一致:當協調者傳送 commit 之後,有的參與者收到 commit 訊息,事務執行成功,有的沒有收到,處於阻塞狀態,這段時間會產生資料不一致。
  • 不確定性:當協調者傳送 commit 之後,並且此時只有一個參與者收到了 commit,那麼當該參與者與協調器同時當機之後,重新選舉的協調器無法確定該條訊息是否提交成功。

2PC 的優勢在於對業務沒有侵入,可以利用資料庫自身機制來進行事務的提交和回滾。

常見的基於 2PC 的具體落地方案有:JTA(XA 規範) 和 Seata( AT 模式)。

2、三階段提交

三階段提交(Three-phase commit),簡稱為 3PC,是 2PC 的改進版本。同時在協調者和參與者都引入了超時機制,還在 2PC 中的準備階段和提交階段中間增加了一個預提交階段。

  • 準備階段(CanCommit):協調者向各個參與者傳送請求,詢問是否可以執行事務,但並不執行事務。
  • 預提交階段(PreCommit):如果從協調者得到的反饋是滿足執行條件,那麼就傳送預提交請求,並開始執行事務;如果從協調者得到的反饋是不滿足執行條件或者超時,則傳送事務中斷請求。
  • 提交階段(DoCommit):如果預提交階段傳送的是預提交請求,那麼正常提交事務;如果預提交階段傳送的是事務中斷請求,那麼直接中斷事務。

img

相對於 2PC,3PC 主要解決的單點故障問題,並減少阻塞,因為一旦參與者無法及時收到來自協調者的資訊之後,他會預設執行 commit。而不會一直持有事務資源並處於阻塞狀態。但是這種機制也會導致資料一致性問題,因為,由於網路原因,協調者傳送的中斷響應沒有及時被參與者接收到,那麼參與者在等待超時之後執行了 commit 操作。這樣就和其他接到中斷命令並執行回滾的參與者之間存在資料不一致的情況。而且 3PC 整體的互動過程更長,效能也會有所下降。

3PC 目前似乎只存在於理論,還沒有具體落地方案。

3、TCC

2PC 和 3PC 都是依賴於資料庫的事務提交和回滾,但是有時候很多業務並不僅僅只涉及到資料庫,可能還會傳送短息、訊息等等,而 TCC 就是屬於業務層面或者說是應用層面的分散式事務。

TCC 方案分為Try-Confirm-Cancel三個階段,屬於補償性分散式事務。

  • Try 階段:完成所有業務檢查(一致性),預留業務資源(準隔離性)
  • Confirm 階段:確認執行業務操作,不再做任何業務檢查, 只使用Try階段預留的業務資源。
  • Cancel 階段:取消Try階段預留的業務資源。

img

有的朋友可能會問了,Try 成功了會執行 Confirm,失敗了會執行 Cancel,那 Confirm 階段失敗了怎麼辦?這時候只能設定重試機制,不斷重試調失敗的 Confirm,直到成功為止,真有怎麼也不成功的,就只能人工介入了。

TCC 需要根據每個場景和業務邏輯來設計相應的操作,所以很大程度增加了業務程式碼的複雜度,對業務有很大的侵入。

雖說對業務有侵入,但是 TCC 沒有資源的阻塞,每一個方法都是直接提交事務的,如果出錯是通過業務層面的 Cancel 來進行補償,所以也稱補償性事務方法。

TCC 要注意的幾個問題:

  • 冪等問題:因為網路呼叫無法保證請求一定能到達,所以都會有重調機制,因此對於 Try、Confirm、Cancel 三個方法都需要冪等實現,避免重複執行產生錯誤。

  • 空回滾問題:指的是 Try 方法由於網路問題沒收到超時了,此時事務管理器就會發出 Cancel 命令,那麼需要支援 Cancel 在未執行 Try 的情況下能正常的 Cancel。

  • 懸掛問題:這個問題也是指 Try 方法由於網路阻塞超時觸發了事務管理器發出了 Cancel 命令,但是執行了 Cancel 命令之後 Try 請求到了。所以空回滾之後還得記錄一下,防止 Try 的再呼叫。

4、本地訊息表

本地訊息表分散式事務解決方案是國外的 eBay 提出的一套方案。其實就是利用了各系統本地的事務來實現分散式事務,在資料庫中存放一張事務訊息表,在執行業務操作的時候, 將業務的執行和將訊息放入訊息表中的操作放在同一個事務中。

本地事務執行成功之後再呼叫其他服務,如果成功了就將訊息表裡的訊息狀態改為成功,如果失敗了,則由定時任務去讀取本地事務表中未成功的訊息,再去呼叫相應的服務,成功後再次修改狀態。

img

這裡也要設定重試機制,一旦有實在不成功的,還需人工介入。這裡要注意的是,也要保證對應服務的方法冪等性。

可以看出,本地訊息表實現比較簡單,是一種最大努力通知思想,實現的是最終一致性,容忍了資料暫時不一致的情況。

缺點是嚴重依賴資料庫。

5、可靠訊息最終一致性方案

在上面的本地訊息表方案中,生產者需要額外建立訊息表,還需要對本地訊息表進行輪詢,業務負擔較重。阿里開源的 RocketMQ 4.3 之後的版本正式支援事務訊息,該事務訊息本質上是把本地訊息表放到 RocketMQ 上,解決生產端的訊息傳送與本地事務執行的原子性問題。

服務 A,先給 Broker (訊息中介軟體) 傳送一個 Half Message(半訊息),其實這個半訊息已傳送到 Broker 端,但是此訊息的狀態被標記為"不能投遞",消費者還看不到,處於這種狀態下的訊息稱為半訊息。

傳送完 半訊息後,服務A 執行業務操作(本地事務),再根據操作結果:如果成功,則向 Broker 傳送 一個 Commit 命令,這時半訊息就變成了可以被消費者訊息;如果失敗,則傳送一個 RollBack 命令,該訊息則會被刪除。

如果是 Commit 那麼服務 B 就能收到這條訊息,然後再做對應的操作,做完了之後再消費這條訊息即可。

如果 RocketMQ 沒有收到服務 A 確認狀態的訊息,那麼半訊息 RocketMQ 會自動定時輪詢回撥你的介面,詢問這個處理的處理情況。藉助這點,服務A實現一個回撥,根據實際處理結果 Commit 或者 Rollback,加強一致性判斷。

img

在服務 B 執行的過程中也可能會失敗,這時也是需要重試,一直執行不成功也需要人工介入,同時也需要保證服務 B 方法的冪等性。

6、最大努力通知

最大努力通知型( Best-effort delivery)是最簡單的一種柔性事務,適用於一些最終一致性時間敏感度低的業務,且被動方處理結果不影響主動方的處理結果。典型的使用場景:如銀行通知、商戶通知等。

就本地訊息表來說會有後臺任務定時去檢視未完成的訊息,然後去呼叫對應的服務,當一個訊息多次呼叫都失敗的時候可以記錄下然後引入人工,或者直接捨棄。這其實算是最大努力了

事務訊息也是一樣,當半訊息被commit了之後確實就是普通訊息了,如果訂閱者一直不消費或者消費不了則會一直重試,到最後進入死信佇列。其實這也算最大努力。

最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能訊息接收不到,此時需要接收通知方主動呼叫發起通知方的介面查詢業務處理結果,通知的可靠性關鍵在接收通知方。

總結

其實分散式事務解決方案還有很多,但是各自還是會存在很多問題,極端情況下也都需要人工去處理,而且大大提高了流程的複雜度,會帶來很多額外的開銷。

所以謹記,在真實的開發過程中,能不使用分散式事務就不要使用!

後面會給大家帶來分散式事務的實戰,沒點關注的可以點個關注,防止走丟了。

END

往期推薦

就這?分散式 ID 發號器實戰

略懂設計模式之工廠模式

就這?Spring 事務失效場景及解決方案

就這?一篇文章讓你讀懂 Spring 事務

SpringBoot+Redis 實現訊息訂閱釋出

相關文章