保證分布式系統(tǒng)數(shù)據(jù)一致性的6種方案

問題的起源

在電商等業(yè)務(wù)中,系統(tǒng)一般由多個(gè)獨(dú)立的服務(wù)組成,如何解決分布式調(diào)用時(shí)候數(shù)據(jù)的一致性?

具體業(yè)務(wù)場景如下,比如一個(gè)業(yè)務(wù)操作,如果同時(shí)調(diào)用服務(wù) A、B、C,需要滿足要么同時(shí)成功;要么同時(shí)失敗。A、B、C 可能是多個(gè)不同部門開發(fā)、部署在不同服務(wù)器上的遠(yuǎn)程服務(wù)。

在分布式系統(tǒng)來說,如果不想犧牲一致性,CAP 理論告訴我們只能放棄可用性,這顯然不能接受。為了便于討論問題,先簡單介紹下數(shù)據(jù)一致性的基礎(chǔ)理論。

強(qiáng)一致

當(dāng)更新操作完成之后,任何多個(gè)后續(xù)進(jìn)程或者線程的訪問都會(huì)返回最新的更新過的值。這種是對(duì)用戶最友好的,就是用戶上一次寫什么,下一次就保證能讀到什么。根據(jù) CAP 理論,這種實(shí)現(xiàn)需要犧牲可用性。

弱一致性

系統(tǒng)并不保證續(xù)進(jìn)程或者線程的訪問都會(huì)返回最新的更新過的值。系統(tǒng)在數(shù)據(jù)寫入成功之后,不承諾立即可以讀到最新寫入的值,也不會(huì)具體的承諾多久之后可以讀到。

最終一致性

弱一致性的特定形式。系統(tǒng)保證在沒有后續(xù)更新的前提下,系統(tǒng)最終返回上一次更新操作的值。在沒有故障發(fā)生的前提下,不一致窗口的時(shí)間主要受通信延遲,系統(tǒng)負(fù)載和復(fù)制副本的個(gè)數(shù)影響。DNS 是一個(gè)典型的最終一致性系統(tǒng)。

在工程實(shí)踐上,為了保障系統(tǒng)的可用性,互聯(lián)網(wǎng)系統(tǒng)大多將強(qiáng)一致性需求轉(zhuǎn)換成最終一致性的需求,并通過系統(tǒng)執(zhí)行冪等性的保證,保證數(shù)據(jù)的最終一致性。但在電商等場景中,對(duì)于數(shù)據(jù)一致性的解決方法和常見的互聯(lián)網(wǎng)系統(tǒng)(如 MySQL 主從同步)又有一定區(qū)別,群友的討論分成以下 6 種解決方案。

1. 規(guī)避分布式事務(wù)——業(yè)務(wù)整合

業(yè)務(wù)整合方案主要采用將接口整合到本地執(zhí)行的方法。拿問題場景來說,則可以將服務(wù) A、B、C 整合為一個(gè)服務(wù) D 給業(yè)務(wù),這個(gè)服務(wù) D 再通過轉(zhuǎn)換為本地事務(wù)的方式,比如服務(wù) D 包含本地服務(wù)和服務(wù) E,而服務(wù) E 是本地服務(wù) A ~ C 的整合。

優(yōu)點(diǎn):解決(規(guī)避)了分布式事務(wù)。

缺點(diǎn):顯而易見,把本來規(guī)劃拆分好的業(yè)務(wù),又耦合到了一起,業(yè)務(wù)職責(zé)不清晰,不利于維護(hù)。

由于這個(gè)方法存在明顯缺點(diǎn),通常不建議使用。

2. 經(jīng)典方案 - eBay 模式

此方案的核心是將需要分布式處理的任務(wù)通過消息日志的方式來異步執(zhí)行。消息日志可以存儲(chǔ)到本地文本、數(shù)據(jù)庫或消息隊(duì)列,再通過業(yè)務(wù)規(guī)則自動(dòng)或人工發(fā)起重試。人工重試更多的是應(yīng)用于支付場景,通過對(duì)賬系統(tǒng)對(duì)事后問題的處理。

消息日志方案的核心是保證服務(wù)接口的冪等性。

考慮到網(wǎng)絡(luò)通訊失敗、數(shù)據(jù)丟包等原因,如果接口不能保證冪等性,數(shù)據(jù)的唯一性將很難保證。

eBay 方式的主要思路如下。

Base:一種 Acid 的替代方案

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

如果 ACID 為分區(qū)的數(shù)據(jù)庫提供一致性的選擇,那么如何實(shí)現(xiàn)可用性呢?答案是

BASE (basically available, soft state, eventually consistent)

BASE 的可用性是通過支持局部故障而不是系統(tǒng)全局故障來實(shí)現(xiàn)的。下面是一個(gè)簡單的例子:如果將用戶分區(qū)在 5 個(gè)數(shù)據(jù)庫服務(wù)器上,BASE 設(shè)計(jì)鼓勵(lì)類似的處理方式,一個(gè)用戶數(shù)據(jù)庫的故障只影響這臺(tái)特定主機(jī)那 20% 的用戶。這里不涉及任何魔法,不過它確實(shí)可以帶來更高的可感知的系統(tǒng)可用性。

文章中描述了一個(gè)最常見的場景,如果產(chǎn)生了一筆交易,需要在交易表增加記錄,同時(shí)還要修改用戶表的金額。這兩個(gè)表屬于不同的遠(yuǎn)程服務(wù),所以就涉及到分布式事務(wù)一致性的問題。

文中提出了一個(gè)經(jīng)典的解決方法,將主要修改操作以及更新用戶表的消息放在一個(gè)本地事務(wù)來完成。同時(shí)為了避免重復(fù)消費(fèi)用戶表消息帶來的問題,達(dá)到多次重試的冪等性,增加一個(gè)更新記錄表 updates_applied 來記錄已經(jīng)處理過的消息。

系統(tǒng)的執(zhí)行偽代碼如下

基于以上方法,在第一階段,通過本地的數(shù)據(jù)庫的事務(wù)保障,增加了 transaction 表及消息隊(duì)列 。

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

通過以上方法,達(dá)到了分布式系統(tǒng)的最終一致性。進(jìn)一步了解 eBay 的方案可以參考文末鏈接。

3. 去哪兒網(wǎng)分布式事務(wù)方案

隨著業(yè)務(wù)規(guī)模不斷地?cái)U(kuò)大,電商網(wǎng)站一般都要面臨拆分之路。就是將原來一個(gè)單體應(yīng)用拆分成多個(gè)不同職責(zé)的子系統(tǒng)。比如以前可能將面向用戶、客戶和運(yùn)營的功能都放在一個(gè)系統(tǒng)里,現(xiàn)在拆分為訂單中心、代理商管理、運(yùn)營系統(tǒng)、報(bào)價(jià)中心、庫存管理等多個(gè)子系統(tǒng)。

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

最開始的單體應(yīng)用所有功能都在一起,存儲(chǔ)也在一起。比如運(yùn)營要取消某個(gè)訂單,那直接去更新訂單表狀態(tài),然后更新庫存表就 ok 了。因?yàn)槭菃误w應(yīng)用,庫在一起,這些都可以在一個(gè)事務(wù)里,由關(guān)系數(shù)據(jù)庫來保證一致性。

但拆分之后就不同了,不同的子系統(tǒng)都有自己的存儲(chǔ)。比如訂單中心就只管理自己的訂單庫,而庫存管理也有自己的庫。那么運(yùn)營系統(tǒng)取消訂單的時(shí)候就是通過接口調(diào)用等方式來調(diào)用訂單中心和庫存管理的服務(wù)了,而不是直接去操作庫。這就涉及一個(gè)『分布式事務(wù)』的問題。

分布式事務(wù)有兩種解決方式

1. 優(yōu)先使用異步消息。

上文已經(jīng)說過,使用異步消息 Consumer 端需要實(shí)現(xiàn)冪等。

冪等有兩種方式,一種方式是業(yè)務(wù)邏輯保證冪等。比如接到支付成功的消息訂單狀態(tài)變成支付完成,如果當(dāng)前狀態(tài)是支付完成,則再收到一個(gè)支付成功的消息則說明消息重復(fù)了,直接作為消息成功處理。

另外一種方式如果業(yè)務(wù)邏輯無法保證冪等,則要增加一個(gè)去重表或者類似的實(shí)現(xiàn)。對(duì)于 producer 端在業(yè)務(wù)數(shù)據(jù)庫的同實(shí)例上放一個(gè)消息庫,發(fā)消息和業(yè)務(wù)操作在同一個(gè)本地事務(wù)里。發(fā)消息的時(shí)候消息并不立即發(fā)出,而是向消息庫插入一條消息記錄,然后在事務(wù)提交的時(shí)候再異步將消息發(fā)出,發(fā)送消息如果成功則將消息庫里的消息刪除,如果遇到消息隊(duì)列服務(wù)異常或網(wǎng)絡(luò)問題,消息沒有成功發(fā)出那么消息就留在這里了,會(huì)有另外一個(gè)服務(wù)不斷地將這些消息掃出重新發(fā)送。

2. 有的業(yè)務(wù)不適合異步消息的方式,事務(wù)的各個(gè)參與方都需要同步的得到結(jié)果。這種情況的實(shí)現(xiàn)方式其實(shí)和上面類似,每個(gè)參與方的本地業(yè)務(wù)庫的同實(shí)例上面放一個(gè)事務(wù)記錄庫。

比如 A 同步調(diào)用 B,C。A 本地事務(wù)成功的時(shí)候更新本地事務(wù)記錄狀態(tài),B 和 C 同樣。如果有一次 A 調(diào)用 B 失敗了,這個(gè)失敗可能是 B 真的失敗了,也可能是調(diào)用超時(shí),實(shí)際 B 成功。則由一個(gè)中心服務(wù)對(duì)比三方的事務(wù)記錄表,做一個(gè)最終決定。假設(shè)現(xiàn)在三方的事務(wù)記錄是 A 成功,B 失敗,C 成功。那么最終決定有兩種方式,根據(jù)具體場景:

  • 重試 B,直到 B 成功,事務(wù)記錄表里記錄了各項(xiàng)調(diào)用參數(shù)等信息;

  • 執(zhí)行 A 和 B 的補(bǔ)償操作(一種可行的補(bǔ)償方式是回滾)。

對(duì) b 場景做一個(gè)特殊說明:比如 B 是扣庫存服務(wù),在第一次調(diào)用的時(shí)候因?yàn)槟撤N原因失敗了,但是重試的時(shí)候庫存已經(jīng)變?yōu)?0,無法重試成功,這個(gè)時(shí)候只有回滾 A 和 C 了。

那么可能有人覺得在業(yè)務(wù)庫的同實(shí)例里放消息庫或事務(wù)記錄庫,會(huì)對(duì)業(yè)務(wù)侵入,業(yè)務(wù)還要關(guān)心這個(gè)庫,是否一個(gè)合理的設(shè)計(jì)?

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

總結(jié)起來,其實(shí)兩種方式的根本原理是類似的,也就是將分布式事務(wù)轉(zhuǎn)換為多個(gè)本地事務(wù),然后依靠重試等方式達(dá)到最終一致性。

4. 蘑菇街交易創(chuàng)建過程中的分布式一致性方案

交易創(chuàng)建的一般性流程

我們把交易創(chuàng)建流程抽象出一系列可擴(kuò)展的功能點(diǎn),每個(gè)功能點(diǎn)都可以有多個(gè)實(shí)現(xiàn)(具體的實(shí)現(xiàn)之間有組合/互斥關(guān)系)。把各個(gè)功能點(diǎn)按照一定流程串起來,就完成了交易創(chuàng)建的過程。

面臨的問題

每個(gè)功能點(diǎn)的實(shí)現(xiàn)都可能會(huì)依賴外部服務(wù)。那么如何保證各個(gè)服務(wù)之間的數(shù)據(jù)是一致的呢?比如鎖定優(yōu)惠券服務(wù)調(diào)用超時(shí)了,不能確定到底有沒有鎖券成功,該如何處理?再比如鎖券成功了,但是扣減庫存失敗了,該如何處理?

方案選型

服務(wù)依賴過多,會(huì)帶來管理復(fù)雜性增加和穩(wěn)定性風(fēng)險(xiǎn)增大的問題。試想如果我們強(qiáng)依賴 10 個(gè)服務(wù),9 個(gè)都執(zhí)行成功了,最后一個(gè)執(zhí)行失敗了,那么是不是前面 9 個(gè)都要回滾掉?這個(gè)成本還是非常高的。

所以在拆分大的流程為多個(gè)小的本地事務(wù)的前提下,對(duì)于非實(shí)時(shí)、非強(qiáng)一致性的關(guān)聯(lián)業(yè)務(wù)寫入,在本地事務(wù)執(zhí)行成功后,我們選擇發(fā)消息通知、關(guān)聯(lián)事務(wù)異步化執(zhí)行的方案。

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

但是事務(wù)消息框架本身會(huì)給業(yè)務(wù)代碼帶來侵入性和復(fù)雜性,所以我們選擇基于 DB 事件變化通知到 MQ 的方式做系統(tǒng)間解耦,通過訂閱方消費(fèi) MQ 消息時(shí)的 ACK 機(jī)制,保證消息一定消費(fèi)成功,達(dá)到最終一致性。由于消息可能會(huì)被重發(fā),消息訂閱方業(yè)務(wù)邏輯處理要做好冪等保證。

所以目前只剩下需要實(shí)時(shí)同步做、有強(qiáng)一致性要求的業(yè)務(wù)場景了。在交易創(chuàng)建過程中,鎖券和扣減庫存是這樣的兩個(gè)典型場景。

要保證多個(gè)系統(tǒng)間數(shù)據(jù)一致,乍一看,必須要引入分布式事務(wù)框架才能解決。但引入非常重的類似二階段提交分布式事務(wù)框架會(huì)帶來復(fù)雜性的急劇上升;在電商領(lǐng)域,絕對(duì)的強(qiáng)一致是過于理想化的,我們可以選擇準(zhǔn)實(shí)時(shí)的最終一致性。

我們在交易創(chuàng)建流程中,首先創(chuàng)建一個(gè)不可見訂單,然后在同步調(diào)用鎖券和扣減庫存時(shí),針對(duì)調(diào)用異常(失敗或者超時(shí)),發(fā)出廢單消息到MQ。如果消息發(fā)送失敗,本地會(huì)做時(shí)間階梯式的異步重試;優(yōu)惠券系統(tǒng)和庫存系統(tǒng)收到消息后,會(huì)進(jìn)行判斷是否需要做業(yè)務(wù)回滾,這樣就準(zhǔn)實(shí)時(shí)地保證了多個(gè)本地事務(wù)的最終一致性。

5. 支付寶及螞蟻金融云的分布式服務(wù) DTS 方案

業(yè)界常用的還有支付寶的一種 xts 方案,由支付寶在 2PC 的基礎(chǔ)上改進(jìn)而來。主要思路如下,大部分信息引用自官方網(wǎng)站。

分布式事務(wù)服務(wù)簡介

分布式事務(wù)服務(wù) (Distributed Transaction Service, DTS) 是一個(gè)分布式事務(wù)框架,用來保障在大規(guī)模分布式環(huán)境下事務(wù)的最終一致性。DTS 從架構(gòu)上分為 xts-client 和 xts-server 兩部分,前者是一個(gè)嵌入客戶端應(yīng)用的 JAR 包,主要負(fù)責(zé)事務(wù)數(shù)據(jù)的寫入和處理;后者是一個(gè)獨(dú)立的系統(tǒng),主要負(fù)責(zé)異常事務(wù)的恢復(fù)。

核心特性

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

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

  • 最終一致:事務(wù)處理過程中,會(huì)有短暫不一致的情況,但通過恢復(fù)系統(tǒng),可以讓事務(wù)的數(shù)據(jù)達(dá)到最終一致的目標(biāo)。

  • 協(xié)議簡單:DTS 定義了類似 2PC 的標(biāo)準(zhǔn)兩階段接口,業(yè)務(wù)系統(tǒng)只需要實(shí)現(xiàn)對(duì)應(yīng)的接口就可以使用 DTS 的事務(wù)功能。

  • 與 RPC 服務(wù)協(xié)議無關(guān):在 SOA 架構(gòu)下,一個(gè)或多個(gè) DB 操作往往被包裝成一個(gè)一個(gè)的 Service,Service 與 Service 之間通過 RPC 協(xié)議通信。DTS 框架構(gòu)建在 SOA 架構(gòu)上,與底層協(xié)議無關(guān)。

  • 與底層事務(wù)實(shí)現(xiàn)無關(guān): DTS 是一個(gè)抽象的基于 Service 層的概念,與底層事務(wù)實(shí)現(xiàn)無關(guān),也就是說在 DTS 的范圍內(nèi),無論是關(guān)系型數(shù)據(jù)庫 MySQL,Oracle,還是 KV 存儲(chǔ) MemCache,或者列存數(shù)據(jù)庫 HBase,只要將對(duì)其的操作包裝成 DTS 的參與者,就可以接入到 DTS 事務(wù)范圍內(nèi)。

以下是分布式事務(wù)框架的流程圖

實(shí)現(xiàn)

  • 一個(gè)完整的業(yè)務(wù)活動(dòng)由一個(gè)主業(yè)務(wù)服務(wù)與若干從業(yè)務(wù)服務(wù)組成。

  • 主業(yè)務(wù)服務(wù)負(fù)責(zé)發(fā)起并完成整個(gè)業(yè)務(wù)活動(dòng)。

  • 從業(yè)務(wù)服務(wù)提供 TCC 型業(yè)務(wù)操作。

業(yè)務(wù)活動(dòng)管理器控制業(yè)務(wù)活動(dòng)的一致性,它登記業(yè)務(wù)活動(dòng)中的操作,并在活動(dòng)提交時(shí)確認(rèn)所有的兩階段事務(wù)的 confirm 操作,在業(yè)務(wù)活動(dòng)取消時(shí)調(diào)用所有兩階段事務(wù)的 cancel 操作?!?/p>

與 2PC 協(xié)議比較

  • 沒有單獨(dú)的 Prepare 階段,降低協(xié)議成本

  • 系統(tǒng)故障容忍度高,恢復(fù)簡單

6. 農(nóng)信網(wǎng)數(shù)據(jù)一致性方案

1. 電商業(yè)務(wù)

公司的支付部門,通過接入其它第三方支付系統(tǒng)來提供支付服務(wù)給業(yè)務(wù)部門,支付服務(wù)是一個(gè)基于 Dubbo 的 RPC 服務(wù)。

對(duì)于業(yè)務(wù)部門來說,電商部門的訂單支付,需要調(diào)用

  • 支付平臺(tái)的支付接口來處理訂單;

  • 同時(shí)需要調(diào)用積分中心的接口,按照業(yè)務(wù)規(guī)則,給用戶增加積分。

從業(yè)務(wù)規(guī)則上需要同時(shí)保證業(yè)務(wù)數(shù)據(jù)的實(shí)時(shí)性和一致性,也就是支付成功必須加積分。

我們采用的方式是同步調(diào)用,首先處理本地事務(wù)業(yè)務(wù)。考慮到積分業(yè)務(wù)比較單一且業(yè)務(wù)影響低于支付,由積分平臺(tái)提供增加與回撤接口。

具體的流程是先調(diào)用積分平臺(tái)增加用戶積分,再調(diào)用支付平臺(tái)進(jìn)行支付處理,如果處理失敗,catch 方法調(diào)用積分平臺(tái)的回撤方法,將本次處理的積分訂單回撤。

2. 用戶信息變更

公司的用戶信息,統(tǒng)一由用戶中心維護(hù),而用戶信息的變更需要同步給各業(yè)務(wù)子系統(tǒng),業(yè)務(wù)子系統(tǒng)再根據(jù)變更內(nèi)容,處理各自業(yè)務(wù)。用戶中心作為 MQ 的 producer,添加通知給 MQ。APP Server 訂閱該消息,同步本地?cái)?shù)據(jù)信息,再處理相關(guān)業(yè)務(wù)比如 APP 退出下線等。

我們采用異步消息通知機(jī)制,目前主要使用 ActiveMQ,基于 Virtual Topic 的訂閱方式,保證單個(gè)業(yè)務(wù)集群訂閱的單次消費(fèi)。

總結(jié)

分布式服務(wù)對(duì)衍生的配套系統(tǒng)要求比較多,特別是我們基于消息、日志的最終一致性方案,需要考慮消息的積壓、消費(fèi)情況、監(jiān)控、報(bào)警等。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容