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