分布式系列第一彈:分布式一致性!
背景
互聯網時代和環境下,為了快速需求響應和提高系統吞吐,往往進行微服務化改造,將復雜系統和數據進行拆分;
這時候的一致性指分布式服務化系統之間的弱一致性,包括應用系統一致性和數據一致性;
生活中的一致性例子:
銀行處理轉賬時,扣減你賬戶上的余額,然后增加別人賬戶的余額;
如果扣減你的賬戶余額成功,增加別人賬戶余額失敗,那么你就會損失這筆資金。
反過來,如果扣減你的賬戶余額失敗,增加別人賬戶余額成功,那么銀行就會損失這筆資金,銀行需要賠付;
下面通過理論和實際方案的介紹,來學習分布式一致性相關內容!
基礎理論
ACID
數據庫管理系統(DBMS)在寫入或更新資料的過程中,為保證事務是正確可靠的,所必須具備的四個特性:原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability)。
在數據庫系統中,一個事務是指:由一系列數據庫操作組成的一個完整的邏輯過程。
例如銀行轉帳,從原賬戶扣除金額,以及向目標賬戶添加金額,這兩個數據庫操作的總和,構成一個完整的邏輯過程,不可拆分。
這個過程被稱為一個事務,具有ACID特性
CAP理論
一致性(Consistency)
在分布式環境下,一致性是指數據在多個副本之間能否保持一致的特性。
在一致性的需求下,當一個系統在數據一致的狀態下執行更新操作后,應該保證系統的數據仍然處于一致的狀態
可用性(Availability)
可用性是指系統提供的服務必須一直處于可用的狀態,對于用戶的每一個操作請求總是能夠在有限的時間內返回結果。
- "有限時間內"是指,對于用戶的一個操作請求,系統必須能夠在指定的時間內返回對應的處理結果,如果超過了這個時間范圍,那么系統就被認為是不可用的
- "返回結果"是可用性的另一個非常重要的指標,它要求系統在完成對用戶請求的處理后,返回一個正常的響應結果
分區容錯性(Partition Tolerance)
分布式系統在遇到任何網絡分區故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務,除非是整個網絡環境都發生了故障
實際情況
CAP理論證明,任何分布式系統只可同時滿足二點,沒法三者兼顧
滿足C和A,那么P能不能滿足呢?
滿足C需要所有的服務器的數據要一樣,也就是說要實現數據的同步,那么同步要不要時間?肯定是要的,并且機器越多,同步的時間肯定越慢,這里問題就來了,我們同時也滿足了A,也就是說,我要同步時間短才行。這樣的話,機器就不能太多了,也就是說P是滿足不了的
滿足C和P,那么A能不能滿足呢?
滿足P需要很多服務器,假設有1000臺服務器,同時滿足了C,也就是說要保證每臺機器的數據都一樣,那么同步的時間可就很大,在這種情況下,我們肯定是不能保證用戶隨時訪問每臺服務器獲取到的數據都是最新的,想要獲取最新的,你就得等,等全部同步完了,你就可以獲取到了,但是我們的A要求短時間就可以拿到想要的數據啊,這不就是矛盾了,所以說這里A是滿足不了了
對于分布式系統而言,網絡問題又是一個必定會出現的異常情況,因此分區容錯性也就成為了一個分布式系統必然需要面對和解決的問題。
因此往往需要把精力花在如何根據業務特點在C(一致性)和A(可用性)之間尋求平衡
BASE理論
BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的縮寫。
BASE理論是對CAP中一致性和可用性權衡的結果,其來源于對大規模互聯網系統分布式實踐的總結,是基于CAP定理逐步演化而來的。
BASE理論的核心思想是:
- 即使無法做到強一致性,但每個應用都可以根據自身業務特點,采用適當的方式來使系統達到最終一致性。
接下來看一下BASE中的三要素:
基本可用(Basically Available)
基本可用是指分布式系統在出現不可預知故障的時候,允許損失部分可用性(注意,這絕不等價于系統不可用)。
比如:
- 響應時間上的損失。正常情況下,一個在線搜索引擎需要在0.5秒之內返回給用戶相應的查詢結果,但由于出現故障,查詢結果的響應時間增加了1~2秒
- 系統功能上的損失:正常情況下,在一個電子商務網站上進行購物的時候,消費者幾乎能夠順利完成每一筆訂單,但是在一些節日大促購物高峰的時候,由于消費者的購物行為激增,為了保護購物系統的穩定性,部分消費者可能會被引導到一個降級頁面
軟狀態(Soft State)
軟狀態指允許系統中的數據存在中間狀態,并認為該中間狀態的存在不會影響系統的整體可用性,即允許系統在不同節點的數據副本之間進行數據同步的過程存在延時
最終一致性(Eventually Consistent)
最終一致性強調的是所有的數據副本,在經過一段時間的同步之后,最終都能夠達到一個一致的狀態。
因此,最終一致性的本質是需要系統保證最終數據能夠達到一致,而不需要實時保證系統數據的強一致性。
總的來說,BASE理論面向的是大型高可用可擴展的分布式系統,和傳統的事務ACID特性是相反的,它完全不同于ACID的強一致性模型,而是通過犧牲強一致性來獲得可用性,并允許數據在一段時間內是不一致的,但最終達到一致狀態。
但同時,在實際的分布式場景中,不同業務和組件對數據一致性的要求是不同的,因此在具體的分布式系統架構設計過程中,ACID特性和BASE理論往往又會結合在一起。
分布式一致性協議
兩階段提交協議(2PC)
第一階段(投票階段):
- 協調者節點向所有參與者節點詢問是否可以執行提交操作,并開始等待各參與者節點的響應
- 參與者節點執行詢問發起為止的所有事務操作,并將Undo信息和Redo信息寫入日志(注意:若成功這里其實每個參與者已經執行了事務操作)
- 各參與者節點響應協調者節點發起的詢問,如果參與者節點的事務操作實際執行成功,則它返回一個"同意"消息;
- 如果參與者節點的事務操作實際執行失敗,則它返回一個"中止"消息
第二階段(提交執行階段):
當協調者節點從所有參與者節點獲得的相應消息都為"同意"時:
協調者節點向所有參與者節點發出"正式提交"的請求
參與者節點正式完成操作,并釋放在整個事務期間內占用的資源
參與者節點向協調者節點發送"完成"消息
協調者節點受到所有參與者節點反饋的"完成"消息后,完成事務
存在的問題:
資源被同步阻塞
在數據提交的過程中,所有參與處理的服務器都處于阻塞狀態,如果其他線程想訪問臨界區的資源,需要等待該條會話請求在本地執行完成后釋放臨界區資源。
因此,采用二階段提交算法也會降低程序并發執行的效率。
單點問題
此外,還會發生單點問題。單點問題也叫作單點服務器故障問題,它指的是當作為分布式集群系統的調度服務器發生故障時,整個集群因為缺少協調者而無法進行二階段提交算法。
單點問題也是二階段提交最大的缺點,因此使用二階段提交算法的時候通常都會進行一些改良,以滿足對系統穩定性的要求。
在Commit 階段出現數據不一致
當統計集群中的服務器可以進行事務操作時,協調服務器會向這些處理事務操作的服務器發送 commit 提交請求。
如果在這個過程中,其中的一臺或幾臺服務器發生網絡故障,無法接收到來自協調服務器的提交請求,導致這些服務器無法完成最終的數據變更,就會造成整個分布式集群出現數據不一致的情況。
三階段提交協議(3PC)
三階段提交其實是在二階段算法的基礎上進行了優化和改進。
為了解決二階段協議中的同步阻塞等問題,三階段提交協議在協調者和參與者中都引入了超時機制,并且把兩階段提交協議的第一個階段拆分成了兩步:詢問,然后再鎖資源,最后真正提交。
注意事項
一旦進入階段3,發生 協調者出現問題 或 協調者和參與者之間的網絡出現故障,即參與者無法及時接收到來自協調者的 DoCommit 或 abort 請求,針對這種異常情況,參與者都會在等待超時之后,繼續進行事務提交。
三階段提交協議存在的問題
參與者接收到 PreCommit 消息后,如果網絡出現分區,此時協調者和部分參與者無法進行正常的網絡通信,該部分參與者依然會進行事務的提交,必然出現數據的不一致性。
TCC
無論是 2PC 還是 3PC,都存在一個大粒度資源鎖定的問題。
我們先來想象這樣一種場景,用戶在電商網站購買商品1000元,使用余額支付800元,使用紅包支付200元。
我們看一下在 2PC 中的流程:
prepare 階段:
- 下單系統插入一條訂單記錄,不提交
- 余額系統減 800 元,給記錄加鎖,寫 redo 和 undo 日志,不提交
- 紅包系統減 200 元,給記錄加鎖,寫 redo 和 undo 日志,不提交
commit 階段:
- 下單系統提交訂單記錄
- 余額系統提交,釋放鎖
- 紅包系統提交,釋放鎖
為什么說這是一種大粒度的資源鎖定呢?
是因為在 prepare 階段,當數據庫給用戶余額減 800 元之后,為了維持隔離性,會給該條記錄加鎖,在事務提交前,其它事務無法再訪問該條記錄。
但實際上,我們只需要預留其中的 800 元,不需要鎖定整個用戶余額。這是 2PC 和 3PC 的局限,因為這兩者是資源層的協議,無法提供更靈活的資源鎖定操作。
為了解決這個問題,TCC 應運而生。TCC 本質上也是一個二階段提交協議,但和 JTA 中的二階段協議不同的是,它是一個服務層的協議,因此開發者可以根據業務自由控制資源鎖定的粒度。
我們先來看一下 TCC 協議的運行過程。
TCC 將事務的提交過程分為 try-confirm-cancel(實際上 TCC 就是 try、confirm、cancel 的簡稱) 三個階段:
- try:完成業務檢查、預留業務資源
- confirm:使用預留的資源執行業務操作(需要保證冪等性)
- cancel:取消執行業務操作,釋放預留的資源(需要保證冪等性)
流程如下:
- 事務發起方向事務協調器發起事務請求,事務協調器調用所有事務參與者的 try 方法完成資源的預留,這時候并沒有真正執行業務,而是為后面具體要執行的業務預留資源,這里完成了一階段。
- 如果事務協調器發現有參與者的 try 方法預留資源時候發現資源不夠,則調用參與方的 cancel 方法回滾預留的資源,需要注意 cancel 方法需要實現業務冪等,因為有可能調用失敗(比如網絡原因參與者接受到了請求,但是由于網絡原因事務協調器沒有接受到回執)會重試。
- 如果事務協調器發現所有參與者的 try 方法返回都 OK,則事務協調器調用所有參與者的 confirm 方法,不做資源檢查,直接進行具體的業務操作。
- 如果協調器發現所有參與者的 confirm 方法都 OK 了,則分布式事務結束。
- 如果協調器發現有些參與者的 confirm 方法失敗了,或者由于網絡原因沒有收到回執,則協調器會進行重試。這里如果重試一定次數后還是失敗,常見的是做事務補償。
通過一個支付場景看看 TCC 在該場景中的流程:
Try操作
- tryX 下單系統創建待支付訂單
- tryY 凍結賬戶紅包 200 元
- tryZ 凍結資金賬戶 800 元
Confirm操作
- confirmX 訂單更新為支付成功
- confirmY 扣減賬戶紅包 200 元
- confirmZ 扣減資金賬戶 800 元
Cancel操作
- cancelX 訂單處理異常,資金紅包退回,訂單支付失敗
- cancelY 凍結紅包失敗,賬戶余額退回,訂單支付失敗
- cancelZ 凍結余額失敗,賬戶紅包退回,訂單支付失敗
可以看到,我們使用了凍結代替了原先的賬號鎖定(實際操作中,凍結操作可以用數據庫減操作+日志實現),這樣在凍結操作之后,事務提交之前,其它事務也能使用賬戶余額,提高了并發性。
總結一下,相比于二階段提交協議,TCC 主要有以下區別:
- 2PC 位于資源層而 TCC 位于服務層。
- 2PC 的接口由第三方廠商實現,TCC 的接口由開發人員實現。
- TCC 可以更靈活地控制資源鎖定的粒度。
- TCC 對應用的侵入性強。業務邏輯的每個分支都需要實現 try、confirm、cancel 三個操作,應用侵入性較強,改造成本高。
比如,你的訂單服務中本來只有一個接口
- //修改代碼狀態
- orderClient.updateStatus();
都要拆為三個接口,即:
- orderClient.tryUpateStatus();
- orderClient.confirmUpateStatus();
- orderClient.cancelUpateStatus();
目前TCC的實現有如下幾個
- tcc-transaction:
- ByteTCC
- spring-cloud-rest-tcc
最終一致性模式
緩存一致性模式
高并發系統中一個常見的核心需求就是億級的讀需求,顯然,關系型數據庫并不是解決高并發讀需求的最佳方案,互聯網的經典做法就是使用緩存
常用緩存方式分為本地緩存和分布式緩存兩種;如果對性能要求不是非常的高,優先使用分布式緩存;對于數據實時性和分布式一直性要求不高的可以使用本地緩存,比如某些人員的配置,即使不同機器的配置短時間不相同也不影響正常業務流轉
數據庫與緩存只需要保持弱一致性,而不需要強一致性,常用的緩存方案參考:美團面試題:緩存一致性,我是這么回答的!
查詢模式
服務操作都需要提供一個查詢接口,用來向外部輸出操作執行的狀態。
服務操作的使用方可以通過查詢接口,得知服務操作執行的狀態,然后根據不同狀態來做不同的處理操作
舉個例子:
定時任務監聽生成中的訂單、單發送群消息,RD收到群消息查詢訂單的具體狀態判斷系統是否有問題,是否需要人工修復
補償模式
如果整個操作處于不正常的狀態,我們需要修正操作中有問題的子操作,這可能需要重新執行未完成的子操作,后者取消已經完成的子操作,通過修復使整個分布式系統達到一致,為了讓系統最終一致而做的努力都叫做補償
- 自動恢復:程序根據發生不一致的環境,通過繼續未完成的操作,或者回滾已經完成的操作,自動來達到一致
- 通知運營:如果程序無法自動恢復,并且設計時考慮到了不一致的場景,可以提供運營功能,通過運營手工進行補償
- 通知技術:如果很不巧,系統無法自動回復,又沒有運營功能,那必須通過技術手段來解決,技術手段包括走數據庫變更或者代碼變更來解決,這是最糟的一種場景
舉個例子:
監聽到生成中訂單后,系統自動重新推送入庫消息重新生成入庫單進行重試,如果系統沒法自動恢復需要RD接入定位修復問題
異步確保模式
異步確保模式是補償模式的一個典型案例,經常應用到使用方對響應時間要求并不太高,我們通常把這類操作從主流程中摘除,通過異步的方式進行處理,處理后把結果通過通知系統通知給使用方,這個方案最大的好處能夠對高并發流量進行削峰,例如:電商系統中的物流、配送,以及支付系統中的計費、入賬等
實踐中,將要執行的異步操作封裝后持久入庫,然后通過定時撈取未完成的任務進行補償操作來實現異步確保模式,只要定時系統足夠健壯,任何一個任務最終會被成功執行
舉個例子:
采購系統進行預算釋放和耗用,會同步記錄日志,后期通過異步和定時任務重試保證釋放和耗用成功
定期校對模式
在操作的主流程中的系統間執行校對操作,我們可以事后異步的批量校對操作的狀態,如果發現不一致的操作,則進行補償,補償操作與補償模式中的補償操作是一致的
實現定期校對的一個關鍵就是分布式系統中需要有一個自始至終唯一的ID,常用的唯一id生成方案
舉個例子:
財務那邊的對賬系統定期校對結算數據和業務單據數據的一致性
可靠消息模式
對于異步的操作可以使用消息隊列,通過消息隊列將調用方和被調用方進行解耦,提高系統響應速度,同時能夠達到消峰目的;
對于消息隊列,我們需要建立特殊的設施保證可靠的消息發送以及處理機的冪等
消息的可靠發送
發送消息之前,把消息持久到數據庫,狀態標記為待發送,然后發送消息,如果發送成功,將消息改為發送成功。定時任務定時從數據庫撈取一定時間內未發送的消息,將消息發送
使用第三方消息管理器,發送消息之前,先發送一個預消息給第三方消息管理器,消息管理器將其持久到數據庫,并標記狀態為待發送,發送成功后,標記消息為發送成功。定時任務定時從數據庫撈取一定時間內未發送的消息,回查業務系統是否要繼續發送,根據查詢結果來確定消息的狀態
消息處理器的冪等性
保證消息一定要發送出去,那么就需要有重試機制,有了重試機制,消息一定會重復,那么我們需要對重復做處,常用幾種方案
- 使用數據庫表的唯一索引進行防重,拒絕重復的請求
- 使用分布式中間件Redis進行防重
- 使用狀態機防重,單據相關的業務會涉及到狀態機,狀態在不同情況下會發生變更,如果狀態機已經處于下一個狀態,這時候來一個上一個狀態的變更,理論上是不能夠變更的,保證了有限狀態機的冪等
- 使用樂觀鎖防重,數據更新帶條件,這也是在系統設計的時候,合理的選擇樂觀鎖,通過version或者其他條件來做樂觀鎖,這樣保證更新及時在并發的情況下,也不會有太大的問題
舉個例子:
單據保存的http接口,前端提交時增加唯一id,通過Redis進行防重提交,防止重復建單
訂單對接出入庫系統進行mq異步交互,通過訂單的中間態狀態進行重試,下游做防重