20191127 分佈式系統下最終一致性解決方案

分佈式事務+(冪等、重試、狀態機、恢復日誌、異步校驗)

一個操做需要在四個服務上寫數據,這四個服務對應不同的db;aMysql,bMysql,cMysql,dMysql;

如何保證同時成功,同時失敗?

A服務在寫sql的同時,調用服務B,這兩個操做在一個事務裏面。B服務也可以這樣處理。

 

如果接口不能保證冪等性,數據的唯一性將很難保證。 數據的唯一性。

 

賬戶表數據重複:接口進行冪等性處理,分佈式環境下使用redis鎖,唯一索引。

把無效數據遷移到另一張表中,對用戶的手機號做唯一索引。

 

如何保證消息投遞的可靠性? 使用mysql進行保證,發送消息前保存消息狀態,消費端接收完成或者消費完成後,在修改消息狀態。配合定時任務處理。重新發送。

 

業務邏輯保證冪等,如果業務邏輯無法保證冪等,則要增加一個去重表或者類似的實現。

 

要保證數據的一致性,首先數據不一致有哪些出現的場景?

1)接口沒做冪等,數據重複。

2)消息丟失,導致訂單支付狀態不一致等。

3)異步處理數據,不知道數據是否執行成功。

4)在四個db中需要落庫,如何保證都執行成功。 分佈式事務框架。

 

多副本數據的強一致性,弱一致性和最終一致性

1、一致性又可以分爲強一致性與弱一致性。

2、強一致性可以理解爲在任意時刻,所有節點中的數據是一樣的。同一時間點,你在節點A中獲取到key1的值與在節點B中獲取到key1的值應該都是一樣的。

3、弱一致性包含很多種不同的實現,目前分佈式系統中廣泛實現的是最終一致性

4、所謂最終一致性,就是不保證在任意時刻任意節點上的同一份數據都是相同的,但是隨着時間的遷移,不同節點上的同一份數據總是在向趨同的方向變化。也可以簡單的理解爲在一段時間後,節點間的數據會最終達到一致狀態。

BASE原則發展自CAP定理,捨棄了系統的強一致性而選擇AP,但每個應用可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性。用較通俗的話來描述就是 : “過程寬鬆,結果嚴格,你的老闆不關心過程,只看結果”。

 

在我原來所做系統中,要根據不同維度記錄用戶的賬本,如根據用戶名、銀行卡號記錄月賬、年賬。由於數據量大,需對記賬數據進行分庫分表,一致性的問題就產生了(數據庫分表分庫以後就會產生一致性問題)。

創建一張重試表,在記賬操作重試過若干次(3次)還不成功的情況下,給運營人員發送短信,由人爲介入的方式進行相應的調賬。至此,可保證記賬操作的最終一致性。

 

如何實現分佈式系統的最終一致性

在大型分佈式企業級應用中,分佈式最終一致性方案需要根據系統自身特點量身定製,是系統設計的重點。

大部分業務流程都需要跨多微服務的調用來協作完成,並且要求系統確保分佈式最終一致性。

可以選擇分佈式事務框架方案,目前主流的分佈式事務框架大致可分爲3類實現 :

1、基於XA協議的兩階段提交(2PC)方案(兩階段提交協議,三階段提交協議

2、基於支付寶最早提出的TCC(Try、Confirm、Cancel)方案

3、基於ebay最早提出的消息隊列異步確保方案

此外還有較輕的解決方案,業務系統可以根據自身需要,選擇通過冪等/重試、狀態機、恢復日誌、異步校驗等技術來確保最終一致性。

 

採用分佈式事務框架的方案,最終一致性由分佈式事務框架保證,業務程序員對框架細節完全透明。

選擇這種方案,需要注意幾個點。首先,由於分階段提交協議本身的脆弱性,主流分階段提交協議如2PC,3PC, TCC都無法完全確保最終一致性,要採用異步校驗的手段兜底。其次,分階段提交協議帶來的高延遲,多次協議通信RTT帶來的時間損耗。第三,基於消息隊列異步確保的分佈式事務框架實現,需要考慮消息可靠性和業務侵入問題。分佈式事務框架也有巨大的優勢,首先,分佈式事務被框架封裝成切面,業務開發只需關心純業務。其次,分佈式事務的代碼開發量大大減少。對一致性和代碼質量有極高要求的銀行、金融領域,分佈式事務框架是最佳選擇。

 

不同於採用分佈式事務框架的最終一致性方案,程序員也可以選擇通過冪等/重試、狀態機、恢復日誌、異步校驗等技術來確保最終一致性。這種方案不受限於平臺和框架,系統較精簡靈活,初期業務系統大都基於這種分佈式一致性解決方案。不過這種方案對業務開發的要求更高,分佈式一致性邏輯要業務程序員代碼實現,容易出現bug。

其實,主流的分佈式事務框架也是通過這些基本的系統機制如冪等/重試、狀態機、恢復日誌、異步校驗等來確保的最終一致性,對比兩種方案,下文主要圍繞後一種展開論述,討論5點使系統達成分佈式最終一致性的技術實踐。

分佈式事務框架;(冪等/重試、狀態機、恢復日誌、異步校驗);

 

實踐 1、重試

重試機制可以使分佈式不一致數據自動恢復,前提是重試接口要提供冪等保證。重試機制是達成分佈式最終一致性的重要手段。例如,超時重傳是TCP協議保證數據可靠性的一個重要機制,核心思想其實就是重試。

1)同步重試 : 在上次請求失敗或超時,程序再次發起同步調用請求。後端程序不推薦同步重試,其一因爲同步等待佔用系統線程資源,其二因爲重試引起的流量放大,可能導致系統雪崩。

2)異步重試 : 通過異步系統(消息隊列或調度中間件)對失敗或超時請求再次發起調用。推薦這種方式的重試,重試的時間間隔可以設置爲根據重試次數指數增長,超過重試閾值仍未成功,可以報警通知並由人工訂正

重試也是提高系統可用性的一種有效手段。如果一個服務的可用性爲98%(有1個9),1次重試之後其可用性可達到99.96%(3個9),2次重試可以達到99.9992%(5個9)。

重試機制   1)保證接口冪等   2)重試最大次數,默認次數。   3)重試是否有可能造成服務器端壓力增大?

如果接口調用失敗了,是否進行請求重試;如果進行重試,默認重試幾次;多次重試是否會對服務器端造成壓力?

 

 

2、冪等

冪等的數學定義爲 f(f(x)) = f(x)

用通俗的話來說就是 : 相同的操作執行多次和執行一次產生的效果是一樣的。有的操作是天然冪等的,如查詢、刪除操作。有的操作是人爲使其冪等,例如TCP的超時重傳操作就是冪等的,無論客戶端將一個seq字節傳送多少次,服務端窗口只會用一次該字節。冪等實現方式有很多 :

1)基於記錄的悲觀鎖,MySQL中通過SELECT FOR UPDATE語句實現。這種實現方式要設置AUTOCOMMIT=0,加鎖和更新記錄在同一個事務中,長時間鎖定記錄會降低系統的TPS,高併發場景不推薦使用。

2)基於記錄版本號或狀態機的樂觀鎖方案,適用於更新數據場景。例如,用戶下單購買一個商品的扣庫存操作實現冪等,可以用如下SQL語句實現 : UPDATE stocktable SET stock = stock - 1, version = version + 1 WHERE product_id = 123 and version = 1

3)基於數據庫唯一索引的去重表,適用於插入和更新數據的場景,由數據庫惟一索引確保多次插入和更新操作只有一次生效。

4)基於全局唯一標識token實現,這種方案要注意幾點 : 1、這裏校驗token是否可使用和設置token爲已使用,是一個CAS原子操作,需要確保在一個原子操作中。 2、如果token存儲使用的是Redis,那麼驗證token的CAS操作可以使用原子自增操作incr,如果Redis值大於1則token不可使用,反之可使用。還有一種實現方式是token生成系統將token預先寫入Redis,用刪除操作來校驗token是否被使用,刪除成功代表token未被使用可執行操作。 3、如果token存儲使用的是MySQL,根據token分庫分表和建惟一索引,同時通過insert語句來判斷token是否存在,如果insert失敗則token不可使用,反之可使用。

 

3、狀態機(正常狀態,狀態機,異常狀態)

狀態機是表示實體的狀態根據條件轉移的數學模型。通過狀態機模型,系統可以判斷當前不一致狀態,以及如何校正不一致狀態到一致狀態。這樣說可能比較抽象,我們拿發微信羣紅包的例子來說明。當你點開發紅包按鈕,輸入總金額、紅包個數、標題,點擊支付成功後。其實根據時間先後紅包系統後臺至少經歷過這樣一個狀態機 :

 

1)、當輸入總金額、紅包個數、標題點擊提交,首先後臺創建一個初始化狀態(INIT)紅包

2)、接着系統將根據你輸入的總金額和個數n將紅包拆分成n分,此時紅包的狀態爲拆分成功(SPLITTED)

3)、此時紅包後臺會監聽異步支付消息,如果支付成功則將紅包置爲支付成功(PAID)

4)、之後紅包系統會通知微信IM系統,發送消息通知羣裏的用戶,此時紅包狀態爲(NOTIFIED)

5.1)、羣裏的用戶把紅包搶光了,紅包狀態被系統置爲已搶光(RUNOUT)

5.2)、還有一種可能,如果羣裏都是程序員,忙着擼代碼,沒時間搶紅包,一定時間後紅包自動退款到支付賬號,紅包狀態便爲(REFUND)。

 

這只是一個正常業務流程的紅包狀態機,異常情況如拆分失敗、支付失敗、通知失敗、退款失敗等情況也同理存在一個狀態機器。爲了方便業務實體狀態回滾和校正,狀態機要儘量設計精簡,轉移到下一個狀態的邊儘可能的只有一條路徑(終結狀態會例外),這樣在回滾和校正時能夠明確前一個狀態和後一個狀態。舉個例子,如果系統發現紅包一直處於PAID狀態,而並沒有流轉到NOTIFIED狀態,能夠判斷是通知羣用戶出現異常,可以根據實際情況重新通知羣用戶或者將超期紅包退款。

 

4、恢復日誌

恢復日誌是程序現場的記錄,也是業務數據恢復的重要依據。恢復日誌log要求全局唯一的requestId來標示請求(實際的業務場景可採用不會重複有含義的業務id),出現異常,可以根據requestId維度redo和undo業務操作,恢復日誌具體可分爲三部分 :

1)requestId請求開始時,記錄REQUEST START requestId

2)本地修改時,記錄全部的(requestId,x,originalValue, destValue)四元組,x代表操作對象,修改前x的值爲originalValue,本次修改的目的操作值爲destValue。

3)requestId結束時,記錄REQUEST End requestId

恢復日誌是系統從不一致的狀態恢復到一致狀態的重要數據,丟失恢復日誌,意味着不一致可能無法恢復。爲什麼是可能,因爲有時可以通過狀態機對不一致的狀態進行恢復。

 

5、異步校驗

徹底解決分佈式一致性問題,有著名的Paxos算法,通過該算法分佈式系統自發達成一致性。而在具體的業務場景,完全不需要系統自發的達成共識,我們只要在業務系統外部加上嚴格的業務約束,用來仲裁業務系統的狀態。通過異步校驗,可以發現分佈式系統中的異常狀態,並通過恢復日誌進行腳本批量恢復或者人工處理恢復,根據校驗的粒度有 :

  • 根據業務實體id校驗,使用消息隊列,將需要校驗業務id投遞給校驗系統,進行異步校驗。
  • 根據時間維度批量校驗,使用異步調度框架,根據時間粒度批量獲取進行異步校驗。

此外,並不是所有系統都有可靠消息隊列和調度服務支撐,業務系統可以增加一個本地業務id校驗回執字段,校驗系統根據校驗步驟回調設置校驗回執字段,並對校驗未通過的數據進行重校驗或者訂正。

 

總結

分佈式最終一致性問題,後端程序員在實際開發中經常遇到。在實際系統開發中爲了確保最終一致性,往往需要組合多個技術點打出組合拳,因爲招數是死的,程序員是活的。總結上面提到的技術點,我們可以通過冪等和重試機制,使得不一致數據能夠自動恢復通過異步校驗機制發現業務系統的不一致數據通過狀態機和恢復日誌,糾正不一致的業務數據

 

一、查詢模式:數據增刪改操作,對每個請求返回一個流水號,可以通過流水號查詢增刪改操作的結果。

表設計: id,請求url及參數,status,createTime,createId; 異步校驗機制

訂單服務,支付服務(是否支付成功,有訂單編號和支付結果,可能存在消息丟失,訂單關閉)

過了十五分鐘,訂單服務沒收到支付服務的mq消息,訂單服務主動去查一下支付服務支付狀態。這就是異步校驗機制。(定時任務,異步處理,訂單超時管理)。

mq消息丟失(支付狀態),導致訂單服務和支付服務的數據不一致

 

二、補償模式:通過修復使整個分佈式系統達到一致。爲了讓系統最終達到一致性狀態而做的努力都叫做補償。

補償操作根據發起形式分爲以下幾種。

1)自動恢復:程序根據發起不一致的環境,通過繼續進行未完成的操作,或者回滾已經完成的操作,來自動達到一致性狀態。

2)通知運營:如果程序無法自動恢復,並且設計時考慮到了不一致的場景,則可以提供運營功能,通過運營手工進行補償。

3)技術運營:如果很不巧,系統無法自動恢復,又沒有運營功能,那麼必須通過技術手段來解決,技術手段包括進行數據庫變更或者代碼變更,這是最糟的一種場景,也是我們在生產中儘量避免的場景。

 

三、異步確保模式

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

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

 

四、定期校對模式

定期校對模式多應用於金融系統中。金融系統由於涉及資金安全,需要保證準確性,所以需要多重的一致性保證機制,包括商戶交易對賬、系統間的一致性對賬、現金對賬、賬務對賬、手續費對賬等,這些都屬於定期校對模式。順便說一下,金融系統與社交應用在技術上的本質區別爲社交應用在於量大,而金融系統在於數據的準確性。

 

五、可靠消息模式

在分佈式系統中,對於主流程中優先級比較低的操作,大多采用異步的方式執行,也就是異步確保模型,爲了讓異步操作的調用方和被調用方充分解耦,也由於專業的消息隊列本身具有可伸縮、可分片、可持久等功能,我們通常通過消息隊列實現異步化。對於消息隊列,我們需要建立特殊的設施來保證可靠的消息發送及處理機的冪等性。

消息的可靠發送可以認爲是盡最大努力發送消息通知,在發送消息之前將消息持久到數據庫,狀態標記爲待發送,然後發送消息,如果發送成功,則將消息改爲發送成功。定時任務定時從數據庫撈取在一定時間內未發送的消息並將消息發送。通過mysql保證消息發送的可靠性。

一些公司把消息的可靠發送實現在了中間件裏,通過Spring的注入,在消息發送時自動持久消息記錄,如果有消息記錄沒有發送成功,則定時補償發送。

 

六、緩存一致性模式

在大規模、高併發系統中的一個常見的核心需求就是億級的讀需求,顯然,關係型數據庫並不是解決高併發讀需求的最佳方案,互聯網精單做法就是使用緩存來抗住讀流量。下面是使用緩存來保證一致性的最佳實踐。

1)如果性能要求不是非常高,則儘量使用分佈式緩存,而不要使用本地緩存。

2)寫緩存時數據一定要完整,如果緩存數據的一部分有效,另一部分無效,則寧可在需要時回源數據庫,也不要把部分數據放入緩存中。

3)使用緩存犧牲了一致性,爲了提高性能,數據庫與緩存只需要保持弱一致性,而不需要保持強一致性,否則違背了使用緩存的初衷。

4)讀的順序是先讀緩存,後讀數據庫,寫的順序要先寫數據庫,後寫緩存。

 

單數據庫情況下的事務

如果應用系統是單一的數據庫,那麼這個很好保證,利用數據庫的事務特性來滿足事務的一致性,這時候的一致性是強一致性的。對於java應用系統來講,很少直接通過事務的start和commit以及rollback來硬編碼,大多通過spring的事務模板或者聲明式事務來保證。

 

基於事務型消息隊列的最終一致性

藉助消息隊列,在處理業務邏輯的地方,發送消息,業務邏輯處理成功後,提交消息,確保消息是發送成功的,之後消息隊列投遞來進行處理,如果成功,則結束,如果沒有成功,則重試,直到成功,不過僅僅適用業務邏輯中,第一階段成功,第二階段必須成功的場景。對應上圖中的C流程。

 

發佈了303 篇原創文章 · 獲贊 104 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章