關于微服務系統中數據一致性的總結
你好,我是看山。
從單體架構到分布式架構,從巨石架構到微服務架構。系統之間的交互越來越復雜,系統間的數據交互量級也是指數級增長。作為一個系統,我們要保證邏輯的自洽和數據的自洽。
數據自洽有兩方面要求:
- 拋開代碼,數據能夠自己驗證自己的準確性,也就是數據彼此之間不矛盾
- 所有數據準確且符合期望
為了實現這兩點,需要實現數據的一致性,為了實現一致性,就需要用到事務。
需要注意一下,本文所設計的數據一致性,不是多數據副本之間保持數據一致性,而是系統之間的業務數據保持一致性。
本地事務
在早期的系統中,我們可以通過關系型數據庫的事務保證數據的一致性。這種事務有四個基本要素:ACID。
- A(Atomicity,原子性):整個事務中的所有操作,要么全部完成,要么全部失敗,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- C(Consistency,一致性):一個事務可以封裝狀態改變(除非它是一個只讀的)。事務必須始終保持系統處于一致的狀態,不管在任何給定的時間并發事務有多少。
- I(Isolation,隔離性):隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。如果有兩個事務,運行在相同的時間內,執行相同的功能,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統。這種屬性有時稱為串行化,為了防止事務操作間的混淆,必須串行化或序列化請求,使得在同一時間僅有一個請求用于同一數據。
- D(Durability,持久性):在事務完成以后,該事務對數據庫所作的更改便持久的保存在數據庫之中,并不會被回滾。
這四個要素是關系型數據庫的根本。無論系統多么復雜,只要使用同一個關系型數據庫,我們就可以借助事務保證數據一致性?;趯﹃P系型數據庫的信任,我們可以認為本地事務是可靠的,開發過程中不需要額外的工作。從架構的角度,關系型數據庫也是一個單獨的系統,那關系型數據庫與應用之間也是形成了分布式。所以我們先研究一下這種簡單的分布式系統如何實現 ACID。
首先,A(原子性)和 D(持久性)是彼此之間密不可分的兩個屬性:原子性保證了事務的所有操作,要么全部完成,要么全部失敗,不可能停滯在中間某個環節;持久性保證了一旦事務完成,該事務對數據庫所作的更改便持久的保存在數據庫之中,不會因為任何原因而導致其修改的內容被撤銷或丟失。
眾所周知,數據必須寫入到磁盤后才能保證持久化,僅僅保存在內存中,一旦出現系統崩潰、主機斷電等情況,數據就會丟失。所以,關鍵是“寫入磁盤”要實現原子性和持久性,然而這個動作存在中間態:正在寫入。所以,現代的關系型數據庫通常采用追加日志記錄的方式。將修改數據所需的全部信息(包括修改什么數據、數據物理上位于哪個內存頁和磁盤塊中、從什么值改成什么值,等等),以順序追加的形式記錄到磁盤中。只有在日志記錄全部落盤,數據庫在日志中看到代表事務成功提交的“提交記錄”后,才會根據日志上的信息對真正的數據進行修改。修改完成后,再在日志中加入一條“結束記錄”表示事務已完成持久化,這種事務實現方法被稱為“提交日志”。
本地事務
我們能夠通過日志保證一個事務的原子性和持久性,那如果出現多個事務訪問同一個資源呢?作為程序猿都知道,多個線程/進程訪問同一個資源,這個資源就稱為臨界資源,想要解決臨界資源占用沖突的方式很簡單,就是加鎖。關系型數據庫為我們準備了三種鎖:
- 寫鎖(Write Lock):同一個時刻,只有有一個事務對數據加寫鎖,所以寫鎖也被稱為排它鎖(exclusive Lock)。數據被加了寫鎖后,其他事務不能寫入數據,也不能對其添加讀鎖(注意,是不能加讀鎖,但是可以讀取數據)。
- 讀鎖(Read Lock):同一時刻,多個事務可以對數據添加讀鎖,所以讀鎖也被稱為共享鎖(Shared Lock)。數據庫被添加讀鎖后,數據不能被添加寫鎖。
- 范圍鎖(Range Lock):對一個范圍的數據添加寫鎖,這個范圍的數據不能被寫入。也可以算作寫鎖的批量行為。
根據這三種鎖的不同組合,我們可以實現四種不同的事務隔離級別:
- 可串行化(Serializable):寫入的時候加寫鎖,讀取的時候加讀鎖,范圍讀寫的時候加范圍鎖。
- 可重復度(Repeatable Read):寫入的時候加寫鎖,讀取的時候加讀鎖,范圍讀寫的時候不加鎖,這樣會出現讀取相同范圍數據的時候,返回結果不同,即幻讀(Phantom Read)。
- 讀已提交(Read Committed):寫入的時候加寫鎖,讀取的時候加讀鎖,讀取完成后立馬釋放讀鎖。這樣會出現同一個事務多次讀取相同數據,返回結果不同,即不可重復讀(Non-Repeatable Read)。
- 讀未提交(Read Uncommitted):寫入的時候加寫鎖,讀取的時候不加鎖。這樣就會讀取到另一個還未提交的事務寫入的數據,即臟讀(Dirty Read)。
全局事務
隨著系統規模不斷擴大,業務量不斷增加。單體應用不再滿足需求,我們會拆分系統,然后拆分數據庫。此時,同一個請求中,就會出現同時訪問多個數據庫的情況。為了解決這種情況的數據一致性問題,X/Open 組織在 1991 年(那個時候我還小)提出了一套 X/Open XA 的處理事務的架構。XA 的核心內容是定義了全局的事務管理器(Transaction Manager,用于協調全局事務)和局部的資源管理器(Resource Manager,用于驅動本地事務)之間的通信接口,在一個事務管理器和多個資源管理器(Resource Manager)之間形成通信橋梁,通過協調多個數據源的一致動作,實現全局事務的統一提交或者統一回滾。與 XA 架構配套的是兩階段提交協議(2PC,Two Phase Commitment Protocol)。在這個協議中,最關鍵的點就是,多個數據庫的活動,均由一個事務協調器的組件來控制。具體的分為 5 個步驟:
- 應用程序調用事務管理器中的提交方法
- 事務管理器將聯絡事務中涉及的每個數據庫,并通知它們準備提交事務(這是第一階段的開始)
- 接收到準備提交事務通知后,數據庫必須確保能在被要求提交事務時提交事務,或在被要求回滾事務時回滾事務。如果數據庫無法準備事務,它會以一個否定響應來回應事務管理器。
- 事務管理器收集來自各數據庫的所有響應。
- 在第二階段,事務管理器將事務的結果通知給每個數據庫。如果任一數據庫做出否定響應,則事務管理器會將一個回滾命令發送給事務中涉及的所有數據庫。如果數據庫都做出肯定響應,則事務管理器會指示所有的資源管理器提交事務。一旦通知數據庫提交,此后的事務就不能失敗了。通過以肯定的方式響應第一階段,每個資源管理器均已確保,如果以后通知它提交事務,則事務不會失敗。
2PC
兩階段提交協議實現簡單,但存在幾個明顯缺陷:
- 單點問題:事務管理器在兩段提交中具有舉足輕重的作用,事務管理器等待資源管理器回復時可以有超時機制,允許資源管理器宕機,但資源管理器等待事務管理器指令時無法做超時處理。一旦宕機的不是其中某個資源管理器,而是事務管理器的話,所有資源管理器都會受到影響。如果事務管理器一直沒有恢復,沒有正常發送 Commit 或者 Rollback 的指令,那所有資源管理器都必須一直等待。
- 性能問題:兩段提交過程中,所有資源管理器相當于被綁定成為一個統一調度的整體,期間要經過兩次遠程服務調用,三次數據持久化(準備階段寫重做日志,事務管理器做狀態持久化,提交階段在日志寫入 Commit Record),整個過程將持續到資源管理器集群中最慢的那一個處理操作結束為止,這決定了兩段式提交的性能通常都較差。
- 一致性風險:盡管提交階段時間很短,但這仍是一段明確存在的危險期。如果事務管理器在發出準備指令后,根據收到各個資源管理器發回的信息確定事務狀態是可以提交的,事務管理器會先持久化事務狀態,并提交自己的事務,如果這時候網絡斷開,無法再通過網絡向所有資源管理器發出 Commit 指令的話,就會導致部分數據(事務管理器的)已提交,但部分數據(資源管理器的)既未提交,也沒有辦法回滾,產生了數據不一致的問題。
能夠發現問題,就能夠想到辦法解決。我們高中老師說了,只要意識不滑坡,辦法總比困難多。所以又發展出了三階段提交協議(3PC,Three Phase Commitment Protocol),能夠緩解單點問題和準備階段的性能問題。這個協議把 2PC 中的準備階段拆分為 CanCommit 和 PreCommit,把提交階段改名為 DoCommit。CanCommit 是詢問階段,讓每個資源管理器根據自身情況判斷該事務是否有可能完成。
3PC 本質是通過一次問詢,如果大家都說自己可以,那成事的可能性很大,減少了準備階段直接鎖定資源的重操作。由于事務失敗回滾概率變小的原因,在三段式提交中,如果在 PreCommit 階段之后發生了事務管理器宕機,即資源管理器沒有能等到 DoCommit 的消息的話,默認的操作策略將是提交事務而不是回滾事務或者持續等待,這就相當于避免了事務管理器單點問題的風險。
3PC
分布式事務
說到分布式事務,不得不提 CAP 理論:任何分布式系統只可同時滿足一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)中的兩點,沒法三者兼顧。
CAP 理論
- 一致性(Consistency):數據在任何時刻、任何分布式節點中所看到的都是符合預期的。
- 可用性(Availability):系統不間斷地提供服務的能力,可用性是由可靠性(Reliability)和可維護性(Serviceability)計算得出的比例值??煽啃酝ㄟ^平均無故障時間(Mean Time Between Failure,MTBF)來度量;可維護性通過平均可修復時間(Mean Time To Repair,MTTR)來度量。可用性衡量系統可以正常使用的時間與總時間之比,公式為:A=MTBF/(MTBF+MTTR)。
- 分區容錯性(Partition Tolerance):分布式環境中部分節點因網絡原因而彼此失聯后,系統仍能正確地提供服務的能力。
CAP 理論定義是經過幾次修改的,修改后的定義本質沒有區別,只是在邏輯上更加嚴謹。本文為了好理解,使用了最容易讓大眾接收并理解的定義。
既然 CAP 不能兼顧,那我們來看看缺少其中一環會出現什么情況:
- 選擇 CA 放棄 P:即我們認為網絡可靠不會出現分區情況,這種可靠是各個節點之間不會出現網絡延遲、中斷等情況,顯然是不成立的。
- 選擇 CP 放棄 A:這樣做就是拋棄了可用性,為了保證數據一致性,一旦出現網絡異常,節點之間的信息同步時間可以無限制地延長。使用 CP 組合的一般用于對數據質量要求很高的場合,也就是為了保證數據完全一致,暫時不提供服務,直到網絡完全恢復,這可能持續一個不確定的時間,尤其是在系統已經表現出高延遲時或者網絡故障導致失去連接時。
- 選擇 AP 放棄 C:意味著一旦發生網絡分區,優先提供服務可用,放棄數據一致性。這是目前分布式系統的主流選擇,因為網絡本身就是鏈接不同區域的服務器的,網絡又是不可靠的,所以 P 不能被舍棄。同時,我們實現分布式系統就是為了提高可用性,這是我們的目的,不能舍棄。
這里需要再說明一下,我們選擇 AP 放棄 C 不是放棄數據一致,而是暫時放棄強一致性(Strong Consistency),而是選擇弱一致性,即最終一致性(Eventual Consistency):系統中的所有數據副本經過一段時間后,最終能夠達到一致的狀態。這里所說的一段時間,也要是用戶可接受范圍內的一段時間。
最終一致性也有一個理論支撐:BASE 理論(不得不說,理論界的縮寫真牛啊,ACID 是酸,CAP 是帽子,BASE 是堿),內容主要包括:
- 基本可用(Basically Available):當系統在出現不可預知故障的時候,允許損失部分可用性。比如,允許響應時間增長,允許部分非關鍵接口降級或熔斷等。
- 軟狀態(Soft State):軟狀態也稱為弱狀態,和硬狀態相對。是指允許系統中的數據存在中間狀態,并認為該中間狀態的存在不會影響系統的整體可用性,即允許系統在不同節點的數據副本之間進行數據同步的過程存在延時。
- 最終一致性(Eventually Consistent):最終一致性強調的是系統中所有的數據副本,在經過一段時間的同步后,最終能夠達到一個一致的狀態。因此,最終一致性的本質是需要系統保證最終數據能夠達到一致,而不需要實時保證系統數據的強一致性。
在工程實踐中,最終一致性分為 5 種,這 5 種方式會結合使用,共同實現最終一致性:
- 因果一致性(Causal consistency):如果節點 A 在更新完某個數據后通知了節點 B,那么節點 B 之后對該數據的訪問和修改都是基于 A 更新后的值。于此同時,和節點 A 無因果關系的節點 C 的數據訪問則沒有這樣的限制。
- 讀己之所寫(Read your writes):節點 A 更新一個數據后,它自身總是能訪問到自身更新過的最新值,而不會看到舊值。
- 會話一致性(Session consistency):會話一致性將對系統數據的訪問過程框定在了一個會話當中,系統能保證在同一個有效的會話中實現“讀己之所寫”的一致性,也就是說,執行更新操作之后,客戶端能夠在同一個會話中始終讀取到該數據項的最新值。
- 單調讀一致性(Monotonic read consistency):如果一個節點從系統中讀取出一個數據項的某個值后,那么系統對于該節點后續的任何數據訪問都不應該返回更舊的值。
- 單調寫一致性(Monotonic write consistency):一個系統要能夠保證來自同一個節點的寫操作被順序的執行。
一致性關系模型
有了理論之后,我們來說一下實現最終一致性的幾種模式。
可靠事件模式
可靠事件模式屬于事件驅動架構:當某個事件發生時,例如更新一個業務實體,服務會向消息代理發布一個事件。消息代理會向訂閱事件的服務推送事件,當訂閱這些事件的服務接收此事件時,就可以完成自己的業務,也可能會引發更多的事件發布。
我們通過一個例子來解釋一下這種模式,用戶下單成功后,訂單系統需要通知庫存系統減庫存。
可靠事件模式
- 訂單系統根據用戶操作完成下單操作。此時會使用同一個本地事務保存訂單信息和寫入事件。
- 另外一個消息服務會輪詢事件表,將狀態是“進行中”的事件以消息形式發送到消息服務中。如果發送失敗,因為是輪詢任務,會在下一次輪詢的時候再次發送。(此處有一些優化點,本例為了簡化模型,不展開)
- 消息服務向訂閱下單消息的庫存服務發送下單成功消息,庫存服務開始處理。此時會有這么集中情況:
- 庫存服務扣減庫存成功,消息服務接收到處理成功響應。消息服務將響應結果返回給訂單服務,訂單服務中事件接收器將事件修改為“已完成”。
- 庫存服務扣減庫存失敗,消息服務接收到處理失敗響應。此時消息服務會再次向庫存服務發送消息,直到得到成功響應。如果失敗次數達到閾值,可以告警通知人工介入。
- 消息服務給訂單服務返回結果時,發生失敗,訂單服務沒有接收到成功響應。這個時候,事件輪詢邏輯會再次將事件發送給消息服務。這樣,庫存服務會重復收到扣減庫存的消息,所以要求庫存服務做好冪等。庫存服務發現消息已經處理過,直接返回成功。
這種靠著持續重試來保證可靠性的解決方案,叫做“最大努力交付”(Best-Effort Delivery),也是“可靠”兩個字的來源。
可靠事件模式還有一種更普通的形式,被稱為“最大努力一次提交”(Best-Effort 1PC),指的就是將最有可能出錯或最核心的業務以本地事務的方式完成后,采用不斷重試的方式(不限于消息服務)來促使同一個分布式事務中的其他關聯業務全部完成。找到最可能出錯的方式是提前做好出錯概率的先驗評估,才能夠知道哪塊最容易出錯。找到最核心的業務的方式是找到那種只要成功,其他業務必須成功的那塊業務。
這里我們再補充兩個概念:
- 業務異常:業務邏輯產生錯誤的情況,比如賬戶余額不足、商品庫存不足等。
- 技術異常:非業務邏輯產生的異常,如網絡連接異常、網絡超時等。
TCC 模式
TCC(Try-Confirm-Cancel)是一種業務侵入式較強的事務方案,要求業務處理過程必須拆分為“預留業務資源”和“確認/釋放消費資源”兩個子過程,由統一的服務協調調度不同業務系統的子過程。分為以下三個階段:
- Try:嘗試執行階段,完成所有業務可執行性的檢查(保障一致性),并且預留好全部需要用到的業務資源(保障隔離性)。
- Confirm:確認執行階段,不進行任何業務檢查,直接使用 Try 階段準備的資源來完成業務處理。Confirm 階段可能會重復執行,需要滿足冪等性。
- Cancel:取消執行階段,釋放 Try 階段預留的業務資源。Cancel 階段可能會重復執行,需要滿足冪等性。
TCC 模式
1.訂單系統創建事務,生成事務 ID(用于作為識別請求冪等的標識),通過活動管理器記錄活動日志。
2.進入 Try 階段
- 調用賬戶系統,檢查賬戶余額是否充足,如果充足,凍結需要的金額,此時賬戶余額是臨界資源,需要通過排它鎖或樂觀鎖保證凍結操作的安全性。
- 調用庫存系統,檢查商品庫存是否充足,如果充足,鎖定需要的庫存,鎖庫操作也需要加鎖保證安全
3.如果所有業務返回成功,記錄活動日志為 Confirm,進入 Confirm 階段:
- 調用賬戶系統,扣減凍結的金額
- 調用庫存系統,扣減鎖定的庫存
4.第 3 步操作中如果全部完成,事務宣告結束。如果第 3 步中任何一方出現異常,都會根據活動日志中的記錄,重復執行 Confirm 操作,即進行最大努力交付。所以各業務系統的 Confirm 操作需要實現冪等性。
5.如果第 2 步有任何一方失敗(包括業務異常和技術異常),將活動日志記錄為 Cancel,進入 Cancel 階段:
- 調用賬戶系統,釋放凍結的金額
- 調用庫存系統,釋放鎖定的庫存
6.第 5 步操作中如果全部完成,事務宣告失敗。如果第 5 步中任何一方出現異常(包括業務異常和技術異常),都會根據活動日志中的記錄,重復執行 Cacel 操作,即最大努力交付。所以各業務系統的 Cancel 操作也需要實現冪等性。
是不是感覺 TCC 與 2PC 的很像,兩者的區別在于,TCC 位于業務代碼層面,屬于白盒,2PC 位于基礎設施層面,屬于黑盒。所以 TCC 有更高的靈活性,可以根據需要,調整資源鎖定的粒度。
TCC 在業務執行過程中可以預留資源,解決了可靠事件模式的資源隔離問題。但是,TCC 還有兩個明顯缺點:
- TCC 將基礎設施層的邏輯上移到業務代碼,對業務有很高的侵入性,需要更高的開發成本,開發成本提升,相對應的維護成本、開發人員的素質等,都會有更高的要求。
- TCC 要求資源可以鎖定、占用或釋放,但是有的資源屬于外部系統,沒有辦法實現鎖定。
鑒于上面的兩個缺點,我們看看 SAGA 是否可以彌補。
SAGA 模式
SAGA 在英文中是“長篇故事、長篇記敘、一長串事件”的意思。SAGA 模式的提出遠早于分布式事務概念的提出(再次對前輩大佬佩服的五體投地),它源于 1987 年普林斯頓大學的 Hector Garcia-Molina 和 Kenneth Salem 在 ACM 發表的一篇論文《SAGAS》。文中提出了一種提升“長時間事務”(Long Lived Transaction)運作效率的方法,大致思路是把一個大事務分解為可以交錯運行的一系列子事務集合,后來發展成將一個分布式環境中的大事務分解為一系列本地事務的設計模式。在有的文章中,將這種模式稱為業務補償模式,SAGA 是對事務形式的描述,業務補償是對事務行為的描述,其本質是一樣的。
SAGA 模式有兩種實現:
- 正向恢復(Forward Recovery):順序執行各個子事務,如果遇到某個子事務執行失敗,將一直重試該操作,知道成功,然后繼續執行下一個子事務。比如用戶下單支付成功了,就一定要扣減庫存。
- 反向恢復(Backward Recovery):順序執行各個子事務,如果遇到某個子事務執行失敗,將執行該子事務的補償操作(避免因為技術異常造成的失敗,補償操作需要冪等),然后倒序執行已經成功的子事務的補償操作。這種一般是可取消的批量操作,比如出行訂票,需要購買飛機票、訂酒店、買門票,如果買門票失敗了,飛機票和酒店就可以取消了。
SAGA 模式
根據這兩種實現,SAGA 可以分為兩部分:
- 子事務(Normal Transactions):大事務拆分若干個小事務,將整個事務 T 分解為 n 個子事務,命名為 T1、T2、…、Tn。每個子事務都應該是或者能被視為是原子行為。如果分布式事務能夠正常提交,其對數據的影響(最終一致性)應與連續按順序成功提交 Ti 等價。
- 補償事務(Compensating Transactions):每個子事務對應的補償動作,命名為 C1、C2、…、Cn。
子事務與補償動作需要滿足一些條件:
- Ti與 Ci必須對應
- 補償動作 Ci一定會執行成功,即需要實現最大努力交付。
- Ti與 Ci需要具備冪等性
文末總結
本文主要總結了本地事務、全局事務、最終一致性等方式實現數據自洽。重點介紹了實現最終一致性的集中模式:可靠事件模式、TCC 模式、SAGA 模式等。數據的一致性一直是個難題,隨著微服務化之后,數據一致性更加困難,有困難不怕,只要不放棄,總會解決的。
本文轉載自微信公眾號「看山的小屋」,可以通過以下二維碼關注。轉載本文請聯系看山的小屋公眾號。








































