一文徹底搞透分佈式一致性

分佈式系統下的數據一致性可以分爲兩大類:

  1. 事務一致性:當多個節點進行操作時,所有節點最終達成的狀態都是一致的。這需要通過協調來保證操作的正確性,避免出現數據不一致的情況;

  2. 副本一致性:數據的多個副本之間保持一致性,這需要保證在對數據進行修改時,所有副本都能夠及時更新,避免數據出現不同步的情況;

定義都比較抽象,舉個例子感受一下:

  1. 事務一致性:電商平臺使用優惠券下單場景:

  2. 下單成功,優惠券必須處於 “已鎖定” 狀態;

  3. 支付成功,優惠券必須處於 “已使用” 狀態;

  4. 訂單取消,優惠券需要恢復爲 “待使用” 狀態;

  5. 優惠券和訂單間就屬於 “事務一致”,兩者間存在強關聯關係。

  6. 副本一致性

  7. MySQL 主從複製:是指在主數據庫上進行數據操作後,將這些操作同步到一個或多個從數據庫上。從庫必須與主庫保持同步,以便從庫中的數據和主庫中的數據保持一致;

  8. Redis 與 MySQL 一致性:在將 Redis 作爲存儲使用時,可以將 MySQL 看做主節點,Redis 看做從節點,當 MySQL 數據發生變更時,自動同步到 Redis 中,並保持數據的一致性;

【注】本文着重介紹 “事務一致性”,多副本一致性,詳見 緩存 或 ES 篇。

1. 脫離數據庫事務的懷抱

在關係型數據庫中,事務(Transaction)是指一組數據庫操作,這些操作要麼全部成功要麼全部失敗。事務可以保證某些數據操作的一致性,當某一條操作失敗時,會進行回滾,即撤銷已執行的操作,使數據恢復到操作前的狀態。

提到事務一致性,不得不說數據庫事務 ACID:ACID 是指數據庫事務的四個關鍵特性,分別爲原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability):

  1. 原子性(Atomicity):事務應該被視爲一個原子操作,即事務中的所有操作要麼全部執行成功,要麼全部失敗回滾。如果事務執行過程中出現錯誤,所有修改操作將被回滾撤銷,不會對數據造成損壞;

  2. 一致性(Consistency):事務執行前後,數據應該保持一致狀態。所有數據修改操作都必須確保數據庫的約束條件、觸發器等規則不會被破壞,保持數據完整性;

  3. 隔離性(Isolation):多個事務同時對同一數據進行操作時,事務之間應該相互隔離,互不干擾。數據庫系統應該確保在併發情況下,事務的執行結果和串行執行的結果一致;

  4. 持久性(Durability):事務完成後,其對數據庫所作的所有修改都應該被永久保存,即使系統崩潰或重啓後,修改的數據也應該是可用的;

銀行轉賬應用程序就是典型的 ACID 模型的應用場景。假設用戶 A 要向用戶 B 轉賬 1000 元,轉賬過程就是一個事務,具有原子性、一致性、隔離性和持久性四大特性:

  1. 原子性:轉賬過程總共涉及兩個操作:從 A 賬戶中減去 1000 元,向 B 賬戶中加上 1000 元。如果這兩個操作中的任何一個失敗,整個事務都將失敗回滾;

  2. 一致性:轉賬前後所有賬戶的餘額總和應該是不變的,不會出現餘額不足或超額的情況;

  3. 隔離性:如果同時發起兩個轉賬事務,應該確保每個事務只訪問自己的數據,不會互相干擾;

  4. 持久性:一旦轉賬完成,更改數據的事務就必須寫入磁盤,保證即使系統崩潰或重啓後,這些數據仍然是可用的;

數據庫事務絕對是程序員的一大利器,但由於各種原因,這把利器離我們越來越遠:

  1. 負載的挑戰:隨着業務的快速增長,數據庫中的數據量或負載也會達到單一實例的上線,此時,我們:

  2. 垂直拆分:將不同的表放到不同的數據庫實例,比如拆分出 User 實例,Order 實例;

  3. 水平拆分:數據量超過單表最大容量時,將數據分拆到不同的數據庫,比如 Order-1 實例、Order-2 實例;

  4. 垂直 + 水平拆分:先進行垂直拆分,在進行水平拆分;

  5. 微服務的挑戰:微服務已經成爲系統的事實架構,特別是 Spring Boot 和 Spring Cloud 的流行:

  6. 微服務的 “自治” 要求每個微服務都應該有自己的獨立數據存儲,避免與其他服務共享數據存儲,從而降低服務之間的耦合性;

  7. 微服務間通過服務發現、負載均衡等方式,將服務之間的關係解耦,從而使得每個服務都具備獨立的自治性;

不管觸發哪一種條件,都會產生跨數據庫事務,從而增加系統設計的難度。

2. 常見一致性保障機制

針對該問題前人已經提出來多種應對方案,特別是關係型數據庫。

2.1. MySQL 事務一致性

熟悉 MySQL 實現的夥伴知道,MySQL 是通過 Redo log 和 Undo log 來實現事務一致性的:

  1. Redo Log:Redo Log 記錄了事務對數據庫所作的修改,包括插入、更新、刪除等操作,它在事務提交前就被寫入磁盤。如果出現故障導致系統崩潰,MySQL 會從 Redo Log 中恢復數據;

  2. Undo Log:Undo Log 記錄了事務對數據庫所作的修改的「前置操作」,並且在事務回滾時用來撤銷事務所做的修改。當事務執行更新時,MySQL 會先將修改前的數據存儲到 Undo Log 中,當事務需要回滾時,MySQL 會根據 Undo Log 中的記錄將數據還原爲修改前的狀態。

具體的如下圖所示:

從圖中可知:

  1. 每一個 DML 語句都會爲其生成對應的 Redo log 和 Undo log。

  2. Redo log 記錄正向修改;

  3. Undo log 記錄逆向恢復;

  4. 事務提交應用全部 Redolog 以持久化正向修改;

  5. 事務回滾應用全部 Undolog 以逆向恢復;

其中,可以看出存在兩個核心流程:

  1. 向前補償:redo log 記錄了事務執行的過程,以及事務提交前的數據修改,可以通過重做日誌來恢復數據,實現向前補償;

  2. 向後補償:undo log 記錄了事務執行過程中對數據的修改,可以用於回滾事務,實現向後補償;

除了兩種補償機制外,還涉及一個重要的組件 “補償管理器”,用於對補償機制進行統一協調。

2.2. 2PC 和 XA

2PC(Two-Phase Commit)和 XA 是分佈式事務中常用的協議和接口:

  1. 2PC 是分佈式事務協議,用於在分佈式系統中協調多個參與者的事務提交或回滾。它包括兩個階段:準備階段和提交階段,參與者在準備階段告知協調者它們是否可以正常提交,如果都能正常提交,則在提交階段所有參與者都提交事務。如果有一個參與者無法正常提交,則所有參與者都需要回滾;

  2. XA 是一組應用程序接口(API),它使應用程序能夠參與分佈式事務,並與事務管理器協同工作,以保證事務的一致性。XA 接口包括三個接口:XA Transactions、XA Resource、XA Resource Manager,用於實現分佈式事務的協調和管理。

MySQL 採用了兩階段提交(Two-Phase Commit,簡稱 2PC)協議,保證 Redolog 和 Binlog 間的數據一致性,確保事務在所有相關節點(包括 Redolog 和 Binlog)執行的情況下,要麼全部提交成功,要麼全部回滾失敗。

2PC 只能應用於兩個事務參與者的場景,而 XA 可以應用於多個事務參與者的場景,具體如圖所示:

XA 定義了一組接口:

  1. XA 資源管理器(XA Resource Manager,RM):用於管理分佈式事務的資源,如數據庫、消息隊列等;

  2. XA 事務管理器(XA Transaction Manager,TM):用於協調各個資源管理器的事務處理;

  3. XA 接口:XA 接口允許應用程序參與到分佈式事務的協調中,包括開始、提交或回滾事務等操作;

對應的事務提交和回滾流程如下:

  1. 應用程序通過 XA 接口開始一個分佈式事務,XA 事務管理器爲該事務分配一個唯一的全局事務 ID;

  2. 應用程序使用 XA 接口將某些操作註冊爲分佈式事務的一部分,這些操作可以涉及多個 XA 資源管理器;

  3. 當應用程序執行到提交事務的代碼時,XA 事務管理器先協調各個 XA 資源管理器,檢查這些資源管理器是否都能夠提交事務;

  4. 如果所有的資源管理器都能夠提交事務,則 XA 事務管理器向各個資源管理器發送提交事務的請求,並等待它們的響應;

  5. 如果其中有任何一個資源管理器不能提交事務,則 XA 事務管理器向各個資源管理器發送回滾事務的請求,並等待它們的響應;

  6. 當所有的資源管理器都響應提交或回滾事務的請求後,XA 事務管理器將事務的狀態(提交或回滾)通知給應用程序,並釋放資源。

2PC (包括升級後的 3PC),在事務執行的整個流程中都需要對資源進行鎖定,在分佈式環境下將大幅增加系統響應時間,降低整個系統的吞吐,在實際工作中使用的非常少。

2.3. TCC

TCC 是實現分佈式事務解決方案的一種有效方法,更是真正應用於實際工作的一大解決方案。

TCC (try-confirm-cancel) 是一種分佈式事務解決方案,它將一個分佈式事務拆分成三個過程:

  1. Try 操作:嘗試執行分佈式事務中的操作,檢查所有參與方是否準備好執行事務。如果準備好,則鎖定資源,等待確認或取消操作;

  2. Confirm 操作:確認執行分佈式事務中的操作,提交所有參與方的操作。如果有任何錯誤,則回滾所有操作並釋放鎖定的資源;

  3. Cancel 操作:取消執行分佈式事務中的操作,回滾所有參與方的操作並釋放鎖定的資源;

TCC 的操作流程如下:

  1. 應用程序向協調者請求分佈式事務,並傳輸所有需要執行的操作;

  2. 協調者根據 TCC 的分佈式事務處理策略創建一個唯一的分佈式事務 ID,並將它分配給每個參與方;

  3. 各參與方執行 Try 操作,並鎖定需要訪問的資源;

  4. 協調者檢查所有參與方是否準備好執行操作,如果所有參與方都準備好,則進入 Confirm 階段;

  5. Confirm 階段中,各參與方確認執行操作,並將結果提交給協調者;

  6. 如果有任何錯誤,協調者將回滾所有操作並釋放鎖定的資源。否則,所有參與方之間的事務將得到確認執行,釋放資源並關閉事務;

  7. 如果任何參與方在 Try 階段失敗,則進入 Cancel 階段;

  8. Cancel 階段中,各參與方撤銷所有操作並釋放鎖定的資源;

  9. 協調者記錄每個階段的操作,以便處理異常情況;

TCC 是一種補償型事務機制,通過人工干預來處理異常,本身具備極佳的靈活性,適用於各種不同類型的應用場景。

2.4. 事務一致性本質

看了不少一致性解決方案,不知道有沒有發現一些規律?

核心組件基本一致:

  1. 應用程序:簡單理解爲開發的應用系統,藉助事務管理器和資源管理的的能力,完成事務一致性保障;

  2. 事務管理器:事務的協調者,接收應用程序的請求,對多個資源管理器進行協調,共同完成正向補償和逆向補償;

  3. 資源管理器:單一資源管理者,對外提供正向補償接口和逆向補充接口,供應用程序和事務管理器使用;

核心流程基本一致:

  1. 正向補償:應用流程向前推進,最終從一個狀態變化爲另一個狀態;

  2. 逆向補償:應用流程向後推進,將所有操作進行回滾,使其恢復到前一狀態;

簡單來說:事務一致性就是通過協調各個參與節點來實現分佈式事務的提交或回滾,確保所有涉及到的操作,要麼全部執行成功,要麼全部不執行。不同的實現方式只是不同的工具,其實現思路基本一致。

3. 業務一致性保障機制

前人已經爲我們提供足夠多的工具,如何更好的使用這些工具,就需要對業務場景進行深入分析。

業務系統一致性是指在多個系統或不同的環境中,不同用戶或系統操作所產生的數據在邏輯上是相同的。它的本質是確保在任何情況下,不同系統或用戶產生的數據都是一致的,並且在系統中的所有操作都是以預期方式進行的。業務系統一致性是確保數據的準確性和可靠性的關鍵因素,可以有效地避免數據錯誤和丟失,提高業務系統的可用性和可靠性,保障企業的持續發展。\
\
如下圖所示:

如果可重試性事務間不存在依賴關係,可以並行執行,具體如下:

在一個複雜的業務流程中,可以將事務分爲三類:

  1. 關鍵性事務:指的是系統中最爲關鍵的一步操作,如果事務提交失敗,則進行回滾操作;如果事務提交成功,則成爲事實,無法回滾;

  2. 可補償性事務:指的是在關鍵性事務之前的事務操作,通常提供正向和逆向兩組操作,正向操作失敗或關鍵事務失敗,在會逆序調用逆向接口,以對操作進行回滾;

  3. 可重試性事務:指的是關鍵事務之後的事務操作,關鍵事務提交成功,則事實已定,下游通過重試的方式完成事務;

我們以分佈式系統中的下單流程爲例:

  1. 關鍵性事務:就是下單操作,將用戶的信息保存到數據庫。保存失敗,對已經操作的庫存和優惠券進行逆向恢復;保存成功,通過重試保障下游事務的一致性;

  2. 可補償性事務:指的是優惠券和庫存服務提供的正向和逆向操作,正向操作可以通過逆向操作進行恢復;

  3. 可重試性事務:指的是添加自動取消任務、保存操作日誌、發送 MQ,當訂單數據保存成功後,這三者通過不斷重試保障最終都會執行;

3.1. 關鍵性事務

關鍵性事務:指在分佈式系統中,只有當某個事務被成功提交後,整個系統才能認爲這個事務是成功的。如果這個事務失敗了,那麼整個系統就會回滾到之前的狀態。例如支付、訂單提交等。

從關鍵性事務的使用場景出發,最適合的工具便是關係數據庫的事務保障。

  1. 事務提交成功:整個流程向前補償,推動可重試性事務通過不斷重試最終完成業務邏輯;

  2. 事務提交失敗:觸發整個流程回滾,逆序調用可補償事務的回滾接口恢復狀態;

3.2. 可補償事務

可補償事務指在某些業務操作中,如果其中一些子操作執行失敗,可以由後續補償操作進行補救,達到一定的業務目的,例如在資金交易中,如果賬戶餘額不足而支付子操作失敗,可以通過撤銷訂單等補償操作來保障交易的正確性。

對於可補償事務,需要提供兩組操作:

  1. 正向:標準的業務操作,比如庫存鎖定

  2. 逆向:針對正向操作的恢復操作,比如釋放鎖定庫存

3.2.1. Seata

Seata 是一個開源的分佈式事務解決方案,旨在解決分佈式系統中的事務一致性問題。在傳統的分佈式系統中,由於各個服務之間的數據交互和操作都是獨立進行的,因此很容易出現數據不一致的情況。這會導致系統出現各種異常情況,如數據丟失、重複提交等,從而影響系統的穩定性和可靠性。

Seata 提供了多種解決方案來解決分佈式事務一致性問題。其中包括 XA 模式、TCC 模式和 SAGA 模式等。

  1. XA 模式是一種基於數據庫的事務管理模式,Seata 通過與數據庫進行交互來實現分佈式事務的一致性。該模式適用於對數據一致性要求比較高的業務場景,如金融、電商等。但是,由於需要與數據庫進行交互,因此該模式的性能相對較低;

  2. AT 模式(基於應用層的兩階段提交方式):AT 模式實現在應用程序中嵌入事務語義,通過協調維護必要的鎖,實現多個業務節點之間跨多個數據庫表的事務。適用於關係型數據庫的應用場景,如電商下單等。

  3. TCC 模式是一種基於補償的事務管理模式,Seata 通過預留資源、嘗試執行、確認執行和回滾執行四個階段來實現分佈式事務的一致性。該模式適用於對性能要求比較高的業務場景,如遊戲、社交等。但是,由於需要進行多次異步通信,因此該模式的複雜度較高;

  4. SAGA 模式是一種基於事件驅動的事務管理模式,Seata 通過將一個大的分佈式事務拆分成多個小的本地事務,並通過異步消息傳遞來實現分佈式事務的一致性。該模式適用於對性能和可用性要求比較高的業務場景,如微服務架構下的系統。但是,由於需要進行多次異步通信和狀態管理,因此該模式的複雜度也較高。

Seata 還提供了一些重要的功能,如事務日誌記錄、故障恢復、動態擴展等,使得用戶可以更加方便地使用該框架來解決分佈式事務一致性問題。同時,Seata 還具有高性能、高可用性和易用性等特點,可以滿足各種不同場景下的需求。

【注】感興趣的話,可以找下 seata 的官方文檔。

3.2.2. Context + Rollback

Seata 雖好,但中間件的引入將大幅提升系統的複雜性,對於一些不太嚴謹的場景或者一些運維能力不足的小團隊可以自己實現回滾方案。

整體方案如下:

  1. 創建一個 Context 對象,用於保存整個流程的上下文數據。其中存在一個 List 屬性,維護待回滾任務列表;

  2. 每操作完一個正向流程,向 Context 中註冊一個逆向回調,及 Rollback 任務;

  3. 如果

  4. 關鍵事務提交成功,Context 註冊的 RollbackEntry 便失去意義;

  5. 關鍵事務提交失敗,調用 Context 的 fireFallback 方法進行逆向補償,fireFallback 方法逆向調用註冊的回滾方法,從而恢復業務狀態

該方案基於內存實現,存在失靈的情況,不建議使用在嚴謹的場景。

3.3. 可重試性事務

可重試型事務指在業務操作中,如果某些操作由於網絡波動等原因導致失敗,可以通過重新執行這些操作來達到其預期的結果,例如在發送短信驗證碼時,由於網絡狀況不佳而發送失敗,可以重新嘗試發送,直到發送成功爲止。

可重試性事務沒有失敗,只有成功,哪怕是短暫的失敗也會通過不限的重試使其最終達到成功狀態。

3.3.1. @Retry

@Retry 是 Spring 框架提供的一個註解,用於在方法調用失敗時自動進行重試。

通過 @Retry  註解,我們可以定義重試的次數、間隔時間和異常類型等信息,從而實現更可靠的方法調用。

具體來說,@Retry 註解可以通過以下屬性來配置:

  1. maxAttempts: 最大重試次數;

  2. value: 重試間隔時間的數值表示;

  3. fixedDelay: 是否固定等待重試間隔時間後再進行下一次嘗試;

  4. backoffPolicy: 重試間隔時間的退避策略;

  5. allowCoreThreadTimeOut: 是否允許在覈心線程上進行超時等待;

  6. excludeExceptions: 需要排除的異常類型;

  7. excludeClassNames: 需要排除的類名列表;

  8. loggerMessage: 日誌輸出格式;

  9. fallbackMethodName: 當所有重試都失敗後,執行的方法名稱;

我們看下具體的使用:

  1. 基於計數器的重試實現
@Retryable(value = {Exception.class}maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void doSomething() throws Exception {
    // 業務邏輯代碼
}

該實現會在方法調用失敗時進行最多 3 次的重試,每次重試之間會等待 1 秒的時間。如果超過 3 次重試仍然失敗,則拋出異常。

  1. 基於自定義異常處理的重試實現
@Retryable(value = {Exception.class}maxAttempts = 3, backoff = @Backoff(delay = 1000)fallback = @Fallback(fallbackMethod = "doDefault"))
public void doSomething() throws Exception {
    // 業務邏輯代碼
}

private String doDefault(Exception e) {
    // 當出現指定異常時,執行該方法進行重試處理
}

該實現會在方法調用失敗時進行最多 3 次的重試,每次重試之間會等待 1 秒的時間。如果超過 3 次重試仍然失敗,則會執行 doDefault 方法來進行重試處理。在該方法中,我們可以自定義處理方式來處理異常情況。

@Retry 仍舊是一個內存解決方案,在極端場景下可能出現任務丟失的情況。因此在實際工作中,很少用於可重試性事務這種場景。

3.3.2. MQ

MQ(消息隊列)消費者重試機制是指在消費消息時,如果消費者無法成功消費消息(比如網絡異常、服務器故障等原因),會自動重試一定次數或間隔一定時間後再次嘗試消費消息,以保證消息的可靠性和可用性。

如下圖所示:

具有 MQ 的可重試性事務,需要以下保障:

  1. 保障業務操作與消費發送之間的一致性:業務操作成功,消息必須發送成功;業務操作失敗,消息不能發送;

  2. 保障消息投遞和消費消費之間的一致性:對於消費失敗的消息,MQ 會自動進行重試,直至消費成功;

一般情況下會採用多次投遞的方式來實現消息投遞和消息消費之間的一致性,所以消息消費者需要保障冪等性,避免多次投遞造成的業務問題。

3.3.2.1. 半消息

RocketMQ 事務消息是一種支持分佈式事務的消息模型,將消息生產和消費與業務邏輯綁定在一起,確保消息發送和事務執行的原子性,保證消息的可靠性。

事務消息分爲兩個階段:發送消息和確認消息,確認消息分爲提交和回滾兩個操作。在提交操作執行完畢後,消息纔會被消費端消費,而在回滾操作執行完畢後,消息會被刪除,從而達到了事務的一致性和可靠性。

事務消息的發生流程如下:

  1. 生產者發送 prepare 消息到 RocketMQ 服務端,RocketMQ 將消息存儲到本地並返回結果;

  2. 生產者開始執行本地事務,並根據本地事務的結果將狀態信息提交給 RocketMQ 服務端;

  3. 如果本地事務執行成功,生產者向 RocketMQ 服務端發送 commit 消息;

  4. 如果本地事務執行失敗,生產者向 RocketMQ 服務端發送 rollback 消息;

  5. RocketMQ 接收到 commit 或 rollback 消息後,對消息進行投放或刪除;

如果生成者發送 prepare 消息後,未在規定時間內發送 commit 或 rollback 消息,RocketMQ 將進入恢復流程,具體如下:

  1. 如果在回查的時間之前沒有收到相應的 commit 或 rollback 消息,則 RocketMQ 會將對該 prepare 消息進行回查;

  2. 應用程序接收到回查指令,從業務庫中獲取數據,並根據業務邏輯進行判斷,最終是 commit 還是 rollback;

  3. RocketMQ 接收到 commit 或 rollback 回覆後,進行相應動作,從而實現業務操作和消息發送的一致性;

使用 RocketMQ 的事務消息代碼示例如下:

// 編寫事務監聽器類
public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    // 執行本地事務
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        System.out.println("executeLocalTransaction " + value);
        // TODO 執行本地事務,並返回事務狀態
        // 本例假定 index 爲偶數的消息執行成功,奇數的消息執行失敗
        if (value % 2 == 0) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }

    // 檢查本地事務狀態
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("checkLocalTransaction " + msg.getTransactionId());
        // 模擬檢查本地事務狀態,返回事務狀態
        boolean committed = prepare(true);
        if (committed) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return LocalTransactionState.UNKNOW;
    }

    // 模擬操作預處理邏輯
    private boolean prepare(boolean commit) {
        System.out.println("prepare " + (commit ? "commit" : "rollback"));
        return commit;
    }

}

// 編寫發送消息的代碼
public class Producer {
    private static final String NAME_SERVER_ADDR = "localhost:9876";

    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer("MyGroup");
        producer.setNamesrvAddr(NAME_SERVER_ADDR);
        // 註冊事務監聽器
        producer.setTransactionListener(new TransactionListenerImpl());
        producer.start();

        // 發送事務消息
        String[] tags = {"TagA""TagB""TagC"};
        for (int i = 0; i < 3; i++) {
            Message msg = new Message("TopicTest", tags[i]("Hello RocketMQ " + i).getBytes(StandardCharsets.UTF_8));
            // 在消息發送時傳遞給事務監聽器的參數
            SendResult sendResult = producer.sendMessageInTransaction(msg, null);
            System.out.printf("%s%n", sendResult);
        }

        // 關閉生產者
        producer.shutdown();
    }
}

單看代碼很難理解,簡單畫了張圖,具體如下:

其核心部分就是 TransactionListener 實現,其他部分與正常的消息發送基本一致,TransactionListener 主要完成:

  1. 執行本地事務,也就是業務操作;

  2. 執行結果檢測,通過反查業務數據,決定消息的後續處理策略;

爲了使用事務消息,我們不得不在 TransactionListener 中編寫進行大量的適配邏輯,增加研發成本,同時由於邏輯被拆分到多處,也增加了代碼的理解成本。

事務消息存在一定的問題:

  1. 與 MQ 實現強相關,並不是每個 MQ 實現都對事務消息提供支持;

  2. API 比較晦澀,存在一定的學習成本,同時需要對業務邏輯拆分到 Listener 中,增加理解成本;

有沒有實用性強、使用簡單的方案,那可以使用 事務消息表 方案。

3.3.2.2. 事務消息表

事務消息表方案是一種常用的保證消息發送與業務操作一致性的方法。該方案基於數據庫事務和消息隊列,將消息發送和業務操作放入同一個事務中,並將業務操作和消息發送的狀態記錄在數據庫的消息表中,以實現消息的可靠性和冪等性。

如下圖所示:

核心流程如下:

  1. 應用程序開啓一個數據庫事務,並在事務中執行業務操作和消息發送;

  2. 在事務中,將業務操作和消息發送的狀態記錄到消息表中;

  3. 如果業務操作執行成功,並且消息發送成功,提交事務,否則回滾事務;

  4. 定時掃描消息表,並根據消息狀態重新發送未被確認的消息。如果消息發送成功,更新消息狀態;否則根據重試次數更新消息狀態或者丟棄消息;

通過事務消息表方案,可以保證消息的可靠性和冪等性。即使在消息發送失敗或應用程序崩潰的情況下,也可以通過重新發送消息將業務操作和消息發送的狀態同步。同時,該方案可以避免消息重複發送和漏發的情況。

作爲一種通用解決方案,lego 對其進行支持,可參考 reliable-message 模塊。

4. 業務補償

不管在設計時使用哪種方案,都是在盡力降低不一致出現的概率,但可怕的是不一致問題終究會發生。

是不是有些奇怪,做了這麼多還是無法從根源上徹底解決一致性問題,在實際工作中就是這樣:

  1. 並不是所有的可補償事務都能回滾成功:在正向流程中我們都會對資源進行鎖定,如果其他操作破壞了鎖定資源或者破壞了准入條件,程序將無法正常回滾,必須人工介入進行解決。比如,生單時成功鎖定優惠券,但超管發現優惠券發放錯誤對其進行回收,在進行優惠券回滾時,由於優惠券處於不可用狀態,導致無法正常回滾;

  2. 並不是所有的可重試事務都能重試成功:業務執行到可重試事務,只能證明其滿足關鍵事務之前的條件,並不一定滿足下游可重試事務的條件。比如,支付成功後需要給用戶發送微信消息,但用戶授權信息已經過期導致消息無法發送;

  3. 業務迭代引入 bug 會破壞事務機制:這個就更常見了,由於 bug 導致流程錯誤,不得不修復問題和數據

除了主動降低不一致性概率,還需要添加一些被動保護機制,也就是常說的業務補償。

4.1. 查詢模式

查詢模型是最常用的一種方式,主要用於應對網絡傳輸中的第三態問題。

第三態指的是在分佈式系統中,在進行跨網絡調用時,調用方無法確定被調用方的狀態是否改變了,因爲這兩者之間存在一段未知而不可控的網絡延遲時間,導致調用方無法立即得到被調用方的結果。這種情況下,第三態可以看做是一個未知的狀態,需要通過一些機制來解決這個問題。

當網絡調用出現第三態時,最簡單的方式便是對不確定的狀態進行查詢,如上圖所示:

  1. 調用方調用服務完成業務操作,如果成功拿到執行結果,則直接進行後續流程;

  2. 如果發生網絡超時,將通過狀態查詢接口來檢查之前的操作是否完成,如果:

  3. 已完成,則繼續執行後續流程;

  4. 未完成,在重新發起業務調用;

RocketMQ 的事務消息便是基於該機制進行實現。

4.2. 任務檢測模式

當一個業務操作完成後,需要處理多個後續任務,爲了保障所有任務都會被執行,可以使用該模式。

如下圖所示:

  1. 業務操作後,將業務變更和檢測任務在同一事務保護下進行入庫;

  2. 系統繼續執行後續任務,執行完成後對任務狀態進行更新;

  3. 系統週期性對超時未執行的任務進行加載,並進行檢測,如果

  4. 已經執行,則更新任務狀態

  5. 如果未執行,則觸發任務執行

本地消息表就是基於該模式進行構建。

4.3. 對賬模式

對賬模式經常出現在與銀行等金融機構對接的場景。

業務對賬思路非常簡單:

  1. 從不同的業務系統獲取對賬數據;

  2. 按照規則進行雙向對賬,如果

  3. 一致,則說明系統一致

  4. 不一致,進行報警,人工介入進行處理

必須是雙向對賬,單向對賬會出現數據丟失情況。

5. 小結

一致性是分佈式系統面臨的巨大挑戰,根據不同場景可以將一致性分爲:

  1. 事務一致性。在一個事務內的所有操作,要麼全部完成,要麼全部不完成,即保證這些操作是對數據的一致更新,避免數據出現不一致的情況。主要通過使用事務保證來實現,例如:關係型數據庫的 ACID 事務。

  2. 副本一致性。各個副本之間的數據保持一致。當數據發生變化時,需要將這個變化同步到所有的副本中。主要使用副本同步技術來實現,例如 MySQL 的主從複製、MySQL 到 Redis 的數據同步;

本文重點對事務一致性進行全方位的闡述,包括:

  1. 技術視角,常見的解決方案:

  2. MySQL 實現

  3. 2PC 和 XA 協議

  4. TCC 解決方案

  5. 業務視角,將不同的事務進行分類,以便更好的解決:

  6. 關鍵事務

  7. 可補償性事務

  8. 可重試性事務

有了這些方案後,很多場景下仍需落地業務補充,常見方案包括:

  1. 查詢模型

  2. 任務檢查模式

  3. 對賬模型

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/J_x5HP9jd4CuEsv0JV4Qfw