最終一致性解決實例

1. 規避分佈式事務——業務整合

業務整合方案主要採用將接口整合到本地執行的方法。拿問題場景來說,則可以將服務 A、B、C 整合爲一個服務 D 給業務,這個服務 D 再通過轉換爲本地事務的方式,比如服務 D 包含本地服務和服務 E,而服務 E 是本地服務 A ~ C 的整合。

優點:解決(規避)了分佈式事務。

缺點:顯而易見,把本來規劃拆分好的業務,又耦合到了一起,業務職責不清晰,不利於維護。

由於這個方法存在明顯缺點,通常不建議使用。

2. 經典方案 - eBay 模式

此方案的核心是將需要分佈式處理的任務通過消息日誌的方式來異步執行。消息日誌可以存儲到本地文本、數據庫或消息隊列,再通過業務規則自動或人工發起重試。人工重試更多的是應用於支付場景,通過對賬系統對事後問題的處理。

消息日誌方案的核心是保證服務接口的冪等性。

考慮到網絡通訊失敗、數據丟包等原因,如果接口不能保證冪等性,數據的唯一性將很難保證。

eBay 方式的主要思路如下。

BASE:一種 ACID 的替代方案

此方案是 eBay 的架構師 Dan Pritchett 在 2008 年發表給 ACM 的文章,是一篇解釋 BASE 原則,或者說最終一致性的經典文章。文中討論了 BASE 與 ACID 原則在保證數據一致性的基本差異。

如果 ACID 爲分區的數據庫提供一致性的選擇,那麼如何實現可用性呢?

BASE (basically available, soft state, eventually consistent)

BASE 的可用性是通過 支持局部故障 而不是系統全局故障來實現的。下面是一個簡單的例子:如果將用戶分區在 5 個數據庫服務器上,BASE 設計鼓勵類似的處理方式,一個用戶數據庫的故障隻影響這臺特定主機那 20% 的用戶。這裏不涉及任何魔法,不過它確實可以帶來更高的可感知的系統可用性。

文章中描述了一個最常見的場景,如果產生了一筆交易,需要在交易表增加記錄,同時還要修改用戶表的金額。這兩個表屬於不同的遠程服務,所以就涉及到分佈式事務一致性的問題

文中提出了一個經典的解決方法,將主要修改操作以及更新用戶表的消息放在 一個本地事務 來完成。同時爲了避免重複消費用戶表消息帶來的問題,達到多次重試的冪等性,增加一個更新記錄表 updates_applied 來記錄已經處理過的消息。

系統的執行僞代碼如下:

 1 Begin transaction
 2     Insert into transaction(id, selller_id, buyer_id, amount);
 3     Queue message "update user('seller', selller_id, amount)";
 4     Queue message "update user('buyer', buyer_id, amount)";
 5 End transaction
 6 
 7 For each message in queue
 8     Peek message
 9     Begin transaction
10         Select count(*) as processed where trans_id = message.trans_id
11             and balance = message.balance and user_id = message.user_id
12         if processed == 0
13             if message.balance == "seller"
14                 Update user set amt_sold = amt_sold + message.amount
15                     where id = message.id;
16         
17             Else
18                 Update user set amt_bought = amt_bought + message.amount
19                     where id = message.id
20         
21             End if
22         
23         Insert int updates_applied
24             (message.trans_id, message.balance, message.user_id);
25         End if
26     End transaction
27     
28     if transaction successful
29         Remove message from queue
30     End if
31 End for

 

基於以上方法,在第一階段,通過本地的數據庫的事務保障,增加了 transaction 表及消息隊列 。

在第二階段,分別讀出消息隊列(但不刪除),通過判斷更新記錄表 updates_applied 來檢測相關記錄是否被執行,未被執行的記錄會修改 user 表,然後增加一條操作記錄到 updates_applied,事務執行成功之後再刪除隊列。

通過以上方法,達到了分佈式系統的最終一致性。進一步瞭解 eBay 的方案可以參考文末鏈接。

3. 去哪兒網分佈式事務方案

隨着業務規模不斷地擴大,電商網站一般都要面臨拆分之路。就是將原來一個單體應用拆分成多個不同職責的子系統。比如以前可能將面向用戶、客戶和運營的功能都放在一個系統裏,現在拆分爲訂單中心、代理商管理、運營系統、報價中心、庫存管理等多個子系統。

拆分首先要面臨的是什麼呢?

最開始的單體應用所有功能都在一起,存儲也在一起。比如運營要取消某個訂單,那直接去更新訂單表狀態,然後更新庫存表就 ok 了。因爲是單體應用,庫在一起,這些都可以在一個事務裏,由關係數據庫來保證一致性。

但拆分之後就不同了,不同的子系統都有自己的存儲。比如訂單中心就只管理自己的訂單庫,而庫存管理也有自己的庫。那麼運營系統取消訂單的時候就是通過接口調用等方式來調用訂單中心和庫存管理的服務了,而不是直接去操作庫。這就涉及一個『分佈式事務』的問題。

分佈式事務有兩種解決方式

優先使用異步消息

上文已經說過,使用異步消息 Consumer 端需要實現冪等。

冪等有兩種方式,一種方式是業務邏輯保證冪等。比如接到支付成功的消息訂單狀態變成支付完成,如果當前狀態是支付完成,則再收到一個支付成功的消息則說明消息重複了,直接作爲消息成功處理。

另外一種方式如果業務邏輯無法保證冪等,則要增加一個去重表或者類似的實現。對於 producer 端在業務數據庫的同實例上放一個消息庫,發消息和業務操作在同一個本地事務裏。發消息的時候消息並不立即發出,而是向消息庫插入一條消息記錄,然後在事務提交的時候再異步將消息發出,發送消息如果成功則將消息庫裏的消息刪除,如果遇到消息隊列服務異常或網絡問題,消息沒有成功發出那麼消息就留在這裏了,會有另外一個服務不斷地將這些消息掃出重新發送。

有的業務不適合異步消息的方式,事務的各個參與方都需要同步的得到結果

這種情況的實現方式其實和上面類似,每個參與方的本地業務庫的同實例上面放一個事務記錄庫。

比如 A 同步調用 B,C。A 本地事務成功的時候更新本地事務記錄狀態,B 和 C 同樣。如果有一次 A 調用 B 失敗了,這個失敗可能是 B 真的失敗了,也可能是調用超時,實際 B 成功。則由一箇中心服務對比三方的事務記錄表,做一個最終決定。假設現在三方的事務記錄是 A 成功,B 失敗,C 成功。那麼最終決定有兩種方式,根據具體場景:

  • 重試 B,直到 B 成功,事務記錄表裏記錄了各項調用參數等信息;
  • 執行 A 和 B 的補償操作(一種可行的補償方式是回滾)。

對 b 場景做一個特殊說明:比如 B 是扣庫存服務,在第一次調用的時候因爲某種原因失敗了,但是重試的時候庫存已經變爲 0,無法重試成功,這個時候只有回滾 A 和 C 了。

那麼可能有人覺得在業務庫的同實例裏放消息庫或事務記錄庫,會對業務侵入,業務還要關心這個庫,是否一個合理的設計?

實際上可以依靠運維的手段來簡化開發的侵入,我們的方法是讓 DBA 在公司所有 MySQL 實例上預初始化這個庫,通過框架層(消息的客戶端或事務 RPC 框架)透明的在背後操作這個庫,業務開發人員只需要關心自己的業務邏輯,不需要直接訪問這個庫。

總結起來,其實兩種方式的根本原理是類似的,也就是將分佈式事務轉換爲多個本地事務,然後依靠重試等方式達到最終一致性。

4. 蘑菇街交易創建過程中的分佈式一致性方案

交易創建的一般性流程

我們把交易創建流程抽象出一系列可擴展的功能點,每個功能點都可以有多個實現(具體的實現之間有組合/互斥關係)。把各個功能點按照一定流程串起來,就完成了交易創建的過程。

面臨的問題

每個功能點的實現都可能會依賴外部服務。那麼如何保證各個服務之間的數據是一致的呢?比如鎖定優惠券服務調用超時了,不能確定到底有沒有鎖券成功,該如何處理?再比如鎖券成功了,但是扣減庫存失敗了,該如何處理?

方案選型

服務依賴過多,會帶來管理複雜性增加和穩定性風險增大的問題。試想如果我們強依賴 10 個服務,9 個都執行成功了,最後一個執行失敗了,那麼是不是前面 9 個都要回滾掉?這個成本還是非常高的。

所以在拆分大的流程爲多個小的本地事務的前提下,對於非實時、非強一致性的關聯業務寫入,在本地事務執行成功後,我們選擇發消息通知、關聯事務異步化執行的方案。

消息通知往往不能保證 100% 成功;且消息通知後,接收方業務是否能執行成功還是未知數。前者問題可以通過重試解決;後者可以選用事務消息來保證。

但是事務消息框架本身會給業務代碼帶來侵入性和複雜性,所以我們選擇基於 DB 事件變化通知到 MQ 的方式做系統間解耦,通過訂閱方消費 MQ 消息時的 ACK 機制,保證消息一定消費成功,達到最終一致性。由於消息可能會被重發,消息訂閱方業務邏輯處理要做好冪等保證。

所以目前只剩下需要實時同步做、有強一致性要求的業務場景了。在交易創建過程中,鎖券和扣減庫存是這樣的兩個典型場景。

要保證多個系統間數據一致,乍一看,必須要引入分佈式事務框架才能解決。但引入非常重的類似二階段提交分佈式事務框架會帶來複雜性的急劇上升;在電商領域,絕對的強一致是過於理想化的,我們可以選擇準實時的最終一致性。

我們在交易創建流程中,首先創建一個不可見訂單,然後在同步調用鎖券和扣減庫存時,針對調用異常(失敗或者超時),發出廢單消息到MQ。如果消息發送失敗,本地會做時間階梯式的異步重試;優惠券系統和庫存系統收到消息後,會進行判斷是否需要做業務回滾,這樣就準實時地保證了多個本地事務的最終一致性。

5. 支付寶及螞蟻金融雲的分佈式服務 DTS 方案

業界常用的還有支付寶的一種 xts 方案,由支付寶在 2PC 的基礎上改進而來。主要思路如下,大部分信息引用自官方網站。

分佈式事務服務簡介

分佈式事務服務 (Distributed Transaction Service, DTS) 是一個分佈式事務框架,用來保障在大規模分佈式環境下事務的最終一致性。DTS 從架構上分爲 xts-client 和 xts-server 兩部分,前者是一個嵌入客戶端應用的 JAR 包,主要負責事務數據的寫入和處理;後者是一個獨立的系統,主要負責異常事務的恢復。

核心特性

傳統關係型數據庫的事務模型必須遵守 ACID 原則。在單數據庫模式下,ACID 模型能有效保障數據的完整性,但是在大規模分佈式環境下,一個業務往往會跨越多個數據庫,如何保證這多個數據庫之間的數據一致性,需要其他行之有效的策略。在 JavaEE 規範中使用 2PC (2 Phase Commit, 兩階段提交) 來處理跨 DB 環境下的事務問題,但是 2PC 是反可伸縮模式,也就是說,在事務處理過程中,參與者需要一直持有資源直到整個分佈式事務結束。這樣,當業務規模達到千萬級以上時,2PC 的侷限性就越來越明顯,系統可伸縮性會變得很差。基於此,我們採用 BASE 的思想實現了一套類似 2PC 的分佈式事務方案,這就是 DTS。DTS在充分保障分佈式環境下高可用性、高可靠性的同時兼顧數據一致性的要求,其最大的特點是保證數據最終一致 (Eventually consistent)。

簡單的說,DTS 框架有如下特性:

  • 最終一致:事務處理過程中,會有短暫不一致的情況,但通過恢復系統,可以讓事務的數據達到最終一致的目標。

  • 協議簡單:DTS 定義了類似 2PC 的標準兩階段接口,業務系統只需要實現對應的接口就可以使用 DTS 的事務功能。

  • 與 RPC 服務協議無關:在 SOA 架構下,一個或多個 DB 操作往往被包裝成一個一個的 Service,Service 與 Service 之間通過 RPC 協議通信。DTS 框架構建在 SOA 架構上,與底層協議無關。

  • 與底層事務實現無關: DTS 是一個抽象的基於 Service 層的概念,與底層事務實現無關,也就是說在 DTS 的範圍內,無論是關係型數據庫 MySQL,Oracle,還是 KV 存儲 MemCache,或者列存數據庫 HBase,只要將對其的操作包裝成 DTS 的參與者,就可以接入到 DTS 事務範圍內。

以下是分佈式事務框架的流程圖

實現

  • 一個完整的業務活動由一個主業務服務與若干從業務服務組成。

  • 主業務服務負責發起並完成整個業務活動。

  • 從業務服務提供 TCC 型業務操作。

  • 業務活動管理器控制業務活動的一致性,它登記業務活動中的操作,並在活動提交時確認所有的兩階段事務的 confirm 操作,在業務活動取消時調用所有兩階段事務的 cancel 操作。”

與 2PC 協議比較

  • 沒有單獨的 Prepare 階段,降低協議成本

  • 系統故障容忍度高,恢復簡單

6. 農信網數據一致性方案

電商業務

公司的支付部門,通過接入其它第三方支付系統來提供支付服務給業務部門,支付服務是一個基於 Dubbo 的 RPC 服務。

對於業務部門來說,電商部門的訂單支付,需要調用

  • 支付平臺的支付接口來處理訂單;

  • 同時需要調用積分中心的接口,按照業務規則,給用戶增加積分。

從業務規則上需要同時保證業務數據的實時性和一致性,也就是支付成功必須加積分。

我們採用的方式是同步調用,首先處理本地事務業務。考慮到積分業務比較單一且業務影響低於支付,由積分平臺提供增加與回撤接口。

具體的流程是先調用積分平臺增加用戶積分,再調用支付平臺進行支付處理,如果處理失敗,catch 方法調用積分平臺的回撤方法,將本次處理的積分訂單回撤。

#用戶信息變更

公司的用戶信息,統一由用戶中心維護,而用戶信息的變更需要同步給各業務子系統,業務子系統再根據變更內容,處理各自業務。用戶中心作爲 MQ 的 producer,添加通知給 MQ。APP Server 訂閱該消息,同步本地數據信息,再處理相關業務比如 APP 退出下線等。

我們採用異步消息通知機制,目前主要使用 ActiveMQ,基於 Virtual Topic 的訂閱方式,保證單個業務集羣訂閱的單次消費。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章