分散式系列第一彈:分散式一致性!

ITPUB社群發表於2022-11-22

背景

網際網路時代和環境下,為了快速需求響應和提高系統吞吐,往往進行微服務化改造,將複雜系統和資料進行拆分;

這時候的一致性指分散式服務化系統之間的弱一致性,包括應用系統一致性和資料一致性;

一致性級別說明
強一致性要求系統寫入什麼,讀出來的也會是什麼,使用者體驗好,但實現起來往往對系統的效能影響大
弱一致性系統在寫入成功後,不承諾立即可以讀到寫入的值,也不久承諾多久之後資料能夠達到一致,但會盡可能地保證到某個時間級別(比如秒級別)後,資料能夠達到一致狀態
最終一致性最終一致性是弱一致性的一個特例,系統會保證在一定時間內,能夠達到一個資料一致的狀態。最終一致性是弱一致性中非常推崇的一種一致性模型,也是業界在大型分散式系統的資料一致性上比較推崇的模型

生活中的一致性例子:

銀行處理轉賬時,扣減你賬戶上的餘額,然後增加別人賬戶的餘額;

如果扣減你的賬戶餘額成功,增加別人賬戶餘額失敗,那麼你就會損失這筆資金。

反過來,如果扣減你的賬戶餘額失敗,增加別人賬戶餘額成功,那麼銀行就會損失這筆資金,銀行需要賠付;

下面透過理論和實際方案的介紹,來學習分散式一致性相關內容!

基礎理論

ACID

資料庫管理系統(DBMS)在寫入或更新資料的過程中,為保證事務是正確可靠的,所必須具備的四個特性:原子性(atomicity)、一致性(consistency)、隔離性(isolation)、永續性(durability)。

在資料庫系統中,一個事務是指:由一系列資料庫操作組成的一個完整的邏輯過程。

  • 例如銀行轉帳,從原賬戶扣除金額,以及向目標賬戶新增金額,這兩個資料庫操作的總和,構成一個完整的邏輯過程,不可拆分。

這個過程被稱為一個事務,具有ACID特性

ACID特性說明
原子性(Atomicity)原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾
一致性(Consistency)事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態
隔離性(Isolation)當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所干擾,多個併發事務之間要相互隔離
永續性(Durability)一個事務一旦被提交了,那麼對資料庫中的資料的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作

CAP理論

一致性(Consistency)

在分散式環境下,一致性是指資料在多個副本之間能否保持一致的特性。

在一致性的需求下,當一個系統在資料一致的狀態下執行更新操作後,應該保證系統的資料仍然處於一致的狀態

可用性(Availability)

可用性是指系統提供的服務必須一直處於可用的狀態,對於使用者的每一個操作請求總是能夠在有限的時間內返回結果。

  • "有限時間內"是指,對於使用者的一個操作請求,系統必須能夠在指定的時間內返回對應的處理結果,如果超過了這個時間範圍,那麼系統就被認為是不可用的

  • "返回結果"是可用性的另一個非常重要的指標,它要求系統在完成對使用者請求的處理後,返回一個正常的響應結果

分割槽容錯性(Partition Tolerance)

分散式系統在遇到任何網路分割槽故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務,除非是整個網路環境都發生了故障

實際情況

CAP理論證明,任何分散式系統只可同時滿足二點,沒法三者兼顧

選 擇說 明
CA放棄分割槽容錯性,加強一致性和可用性,其實就是傳統的單機資料庫的選擇
AP放棄一致性(這裡說的一致性是強一致性),追求分割槽容錯性和可用性,這是很多分散式系統設計時的選擇,例如很多NoSQL系統就是如此
CP放棄可用性,追求一致性和分割槽容錯性,基本不會選擇,網路問題會直接讓整個系統不可用

滿足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)

分散式系列第一彈:分散式一致性!

第一階段(投票階段):

  1. 協調者節點向所有參與者節點詢問是否可以執行提交操作,並開始等待各參與者節點的響應
  2. 參與者節點執行詢問發起為止的所有事務操作,並將Undo資訊和Redo資訊寫入日誌(注意:若成功這裡其實每個參與者已經執行了事務操作)
  3. 各參與者節點響應協調者節點發起的詢問,如果參與者節點的事務操作實際執行成功,則它返回一個"同意"訊息;
  4. 如果參與者節點的事務操作實際執行失敗,則它返回一個"中止"訊息

第二階段(提交執行階段):

當協調者節點從所有參與者節點獲得的相應訊息都為"同意"時:

  1. 協調者節點向所有參與者節點發出"正式提交"的請求
  2. 參與者節點正式完成操作,並釋放在整個事務期間內佔用的資源
  3. 參與者節點向協調者節點傳送"完成"訊息
  4. 協調者節點受到所有參與者節點反饋的"完成"訊息後,完成事務

存在的問題:

資源被同步阻塞

在資料提交的過程中,所有參與處理的伺服器都處於阻塞狀態,如果其他執行緒想訪問臨界區的資源,需要等待該條會話請求在本地執行完成後釋放臨界區資源。

因此,採用二階段提交演算法也會降低程式併發執行的效率。

單點問題

此外,還會發生單點問題。單點問題也叫作單點伺服器故障問題,它指的是當作為分散式叢集系統的排程伺服器發生故障時,整個叢集因為缺少協調者而無法進行二階段提交演算法。

單點問題也是二階段提交最大的缺點,因此使用二階段提交演算法的時候通常都會進行一些改良,以滿足對系統穩定性的要求。

在Commit 階段出現資料不一致

當統計叢集中的伺服器可以進行事務操作時,協調伺服器會向這些處理事務操作的伺服器傳送 commit 提交請求。

如果在這個過程中,其中的一臺或幾臺伺服器發生網路故障,無法接收到來自協調伺服器的提交請求,導致這些伺服器無法完成最終的資料變更,就會造成整個分散式叢集出現資料不一致的情況。

三階段提交協議(3PC)

三階段提交其實是在二階段演算法的基礎上進行了最佳化和改進。

為了解決二階段協議中的同步阻塞等問題,三階段提交協議在協調者和參與者中都引入了超時機制,並且把兩階段提交協議的第一個階段拆分成了兩步:詢問,然後再鎖資源,最後真正提交。

分散式系列第一彈:分散式一致性!
階段說明
CanCommit事務詢問,協調者向所有的參與者傳送一個包含事務內容的 CanCommit 請求,詢問是否可以執行事務提交操作,並開始等待各參與者的響應。各參與者向協調者反饋事務詢問的響應 參與者在接收到來自協調者的 CanCommit 請求後,正常情況下,如果其自身認為可以順利執行事務,那麼會反饋 Yes 響應,並進入預備狀態,否則反饋 No 響應。
PreCommit如果在詢問階段所有的參與者都返回可以執行操作,協調者向參與者傳送預執行請求,然後參與者寫redo和undo日誌,執行操作,但是不提交操作;如果在詢問階段任何參與者返回不能執行操作的結果,則協調者向參與者傳送中止請求
DoCommit如果每個參與者在準備階段返回準備成功,也就是預留資源和執行操作成功,協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何一個參與者返回準備失敗,也就是預留資源或者執行操作失敗,協調者向參與者發起中止指令,參與者取消已經變更的事務,執行undo日誌,釋放鎖定的資源

注意事項

一旦進入階段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:取消執行業務操作,釋放預留的資源(需要保證冪等性)

分散式系列第一彈:分散式一致性!

流程如下:

  1. 事務發起方向事務協調器發起事務請求,事務協調器呼叫所有事務參與者的 try 方法完成資源的預留,這時候並沒有真正執行業務,而是為後面具體要執行的業務預留資源,這裡完成了一階段。
  2. 如果事務協調器發現有參與者的 try 方法預留資源時候發現資源不夠,則呼叫參與方的 cancel 方法回滾預留的資源,需要注意 cancel 方法需要實現業務冪等,因為有可能呼叫失敗(比如網路原因參與者接受到了請求,但是由於網路原因事務協調器沒有接受到回執)會重試。
  3. 如果事務協調器發現所有參與者的 try 方法返回都 OK,則事務協調器呼叫所有參與者的 confirm 方法,不做資源檢查,直接進行具體的業務操作。
  4. 如果協調器發現所有參與者的 confirm 方法都 OK 了,則分散式事務結束。
  5. 如果協調器發現有些參與者的 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收到群訊息查詢訂單的具體狀態判斷系統是否有問題,是否需要人工修復

補償模式

如果整個操作處於不正常的狀態,我們需要修正操作中有問題的子操作,這可能需要重新執行未完成的子操作,後者取消已經完成的子操作,透過修復使整個分散式系統達到一致,為了讓系統最終一致而做的努力都叫做補償

  1. 自動恢復:程式根據發生不一致的環境,透過繼續未完成的操作,或者回滾已經完成的操作,自動來達到一致
  2. 通知運營:如果程式無法自動恢復,並且設計時考慮到了不一致的場景,可以提供運營功能,透過運營手工進行補償
  3. 通知技術:如果很不巧,系統無法自動回覆,又沒有運營功能,那必須透過技術手段來解決,技術手段包括走資料庫變更或者程式碼變更來解決,這是最糟的一種場景

舉個例子:

監聽到生成中訂單後,系統自動重新推送入庫訊息重新生成入庫單進行重試,如果系統沒法自動恢復需要RD接入定位修復問題

非同步確保模式

非同步確保模式是補償模式的一個典型案例,經常應用到使用方對響應時間要求並不太高,我們通常把這類操作從主流程中摘除,透過非同步的方式進行處理,處理後把結果透過通知系統通知給使用方,這個方案最大的好處能夠對高併發流量進行削峰,例如:電商系統中的物流、配送,以及支付系統中的計費、入賬等

實踐中,將要執行的非同步操作封裝後持久入庫,然後透過定時撈取未完成的任務進行補償操作來實現非同步確保模式,只要定時系統足夠健壯,任何一個任務最終會被成功執行

舉個例子:

採購系統進行預算釋放和耗用,會同步記錄日誌,後期透過非同步和定時任務重試保證釋放和耗用成功

定期校對模式

在操作的主流程中的系統間執行校對操作,我們可以事後非同步的批次校對操作的狀態,如果發現不一致的操作,則進行補償,補償操作與補償模式中的補償操作是一致的

實現定期校對的一個關鍵就是分散式系統中需要有一個自始至終唯一的ID,常用的唯一id生成方案

舉個例子:

財務那邊的對賬系統定期校對結算資料和業務單據資料的一致性

可靠訊息模式

對於非同步的操作可以使用訊息佇列,透過訊息佇列將呼叫方和被呼叫方進行解耦,提高系統響應速度,同時能夠達到消峰目的;

對於訊息佇列,我們需要建立特殊的設施保證可靠的訊息傳送以及處理機的冪等

訊息的可靠傳送

傳送訊息之前,把訊息持久到資料庫,狀態標記為待傳送,然後傳送訊息,如果傳送成功,將訊息改為傳送成功。定時任務定時從資料庫撈取一定時間內未傳送的訊息,將訊息傳送

使用第三方訊息管理器,傳送訊息之前,先傳送一個預訊息給第三方訊息管理器,訊息管理器將其持久到資料庫,並標記狀態為待傳送,傳送成功後,標記訊息為傳送成功。定時任務定時從資料庫撈取一定時間內未傳送的訊息,回查業務系統是否要繼續傳送,根據查詢結果來確定訊息的狀態

訊息處理器的冪等性

保證訊息一定要傳送出去,那麼就需要有重試機制,有了重試機制,訊息一定會重複,那麼我們需要對重複做處,常用幾種方案

  • 使用資料庫表的唯一索引進行防重,拒絕重複的請求

  • 使用分散式中介軟體Redis進行防重

  • 使用狀態機防重,單據相關的業務會涉及到狀態機,狀態在不同情況下會發生變更,如果狀態機已經處於下一個狀態,這時候來一個上一個狀態的變更,理論上是不能夠變更的,保證了有限狀態機的冪等

  • 使用樂觀鎖防重,資料更新帶條件,這也是在系統設計的時候,合理的選擇樂觀鎖,透過version或者其他條件來做樂觀鎖,這樣保證更新及時在併發的情況下,也不會有太大的問題

舉個例子:

  1. 單據儲存的http介面,前端提交時增加唯一id,透過Redis進行防重提交,防止重複建單
  2. 訂單對接出入庫系統進行mq非同步互動,透過訂單的中間態狀態進行重試,下游做防重

參考

  • 書籍:分散式服務架構:原理、設計與實戰

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2924569/,如需轉載,請註明出處,否則將追究法律責任。

相關文章