分佈式事務解決方案彙總:2PC、3PC、消息中間件、TCC、狀態機+重試+冪等(轉)

  數據一致性問題很是多樣,下面舉一些常見例子。好比在更新數據的時候,先更新了數據庫,後更新了緩存,一旦緩存更新失敗,此時數據庫和緩存數據會不一致。反過來,若是先更新緩存,再更新數據庫,一旦緩存更新成功,數據庫更新失敗,數據仍是不一致;java

  好比數據庫中的參照完整性,從表引用了主表的主鍵,對從表來講,也就是外鍵。當主表的記錄刪除後,從表是字段置空,仍是級聯刪除。一樣,當要建立從表記錄時,主表記錄是否要先建立,仍是能夠直接建立從表的記錄;算法

   好比數據庫中的原子性:同時修改兩條記錄,一條記錄修改爲功了,一條記錄沒有修改爲功,數據就會不一致,此時必須回滾,不然會出現髒數據。 數據庫

  好比數據庫的Master-Slave異步複製,Master宕機切換到Slave,致使部分數據丟失,數據會不一致。緩存

   發送方發送了消息一、二、三、四、5,由於消息中間件的不穩定,致使丟了消息4,接收方只收到了消息一、二、三、5,發送方和接收方數據會不一致。網絡

   從以上案例能夠看出,數據一致性問題幾乎無處不在。本書把一致性問題分爲了兩大類:事務一致性和多副本一致性。這兩類一致性問題基本涵蓋了實踐中所遇到的絕大部分場景,本章和下一章將分別針對這兩類一致性問題進行詳細探討。架構

 

隨處可見的分佈式事務問題併發

   在「集中式」的架構中,不少系統用的是Oracle這種大型數據庫,把整個業務數據放在這樣一個強大的數據庫裏面,利用數據庫的參照完整性機制、事務機制,避免出現數據一致性問題。這正是數據庫之因此叫「數據庫」而不是「存儲」的一個重要緣由,就是數據庫強大的數據一致性保證。框架

  但到了分佈式時代,人們對數據庫進行了分庫分表,同時在上面架起一個個的服務。到了微服務時代,服務的粒度拆得更細,致使一個沒法避免的問題:數據庫的事務機制無論用了,由於數據庫自己只能保證單機事務,對於分佈式事務,只能靠業務系統解決。 異步

  例如作一個服務,最初底下只有一個數據庫,用數據庫自己的事務來保證數據一致性。隨着數據量增加到必定規模,進行了分庫,這時數據庫的事務就無論用了,如何保證多個庫之間的數據一致性呢?分佈式

   再以電商系統爲例,好比有兩個服務,一個是訂單服務,背後是訂單數據庫;一個是庫存服務,背後是庫存數據庫,下訂單的時候須要扣庫存。不管先建立訂單,後扣庫存,仍是先扣庫存,後建立訂單,都沒法保證兩個服務必定會調用成功,如何保證兩個服務之間的數據一致性呢? 

  這樣的案例在微服務架構中隨處可見:凡是一個業務操做,須要調用多個服務,而且都是寫操做的時候,就可能會出現有的服務調用成功,有的服務調用失敗,致使只部分數據寫入成功,也就出現了服務之間的數據不一致性。 

分佈式事務解決方案彙總

   接下來,以一個典型的分佈式事務問題——「轉帳」爲例,詳細探討分佈式事務的各類解決方案。

  以支付寶爲例,要把一筆錢從支付寶的餘額轉帳到餘額寶,支付寶的餘額在系統A,背後有對應的DB1;餘額寶在系統B,背後有對應的DB2;螞蟻借唄在系統C,背後有對應的DB3,這些系統之間都要支持相關轉帳。所謂「轉帳」,就是轉出方的系統裏面帳號要扣錢,轉入方的系統裏面帳號要加錢,如何保證兩個操做在兩個系統中同時成功呢? 

1. 2PC

 (1)2PC理論。在講MySQL Binlog和Redo Log的一致性問題時,已經用到了2PC。固然,那個場景只是內部的分佈式事務問題,只涉及單機的兩個日誌文件之間的數據一致性;2PC是應用在兩個數據庫或兩個系統之間。 

  2PC有兩個角色:事務協調者和事務參與者。具體到數據庫的實現來講,每個數據庫就是一個參與者,調用方也就是協調者。2PC是指事務的提交分爲兩個階段,如圖10-1所示。

    階段1:準備階段。協調者向各個參與者發起詢問,說要執行一個事務,各參與者可能回覆YES、NO或超時。

    階段2:提交階段。若是全部參與者都回復的是YES,則事務協調者向全部參與者發起事務提交操做,即Commit操做,全部參與者各自執行事務,而後發送ACK。

 

  若是有一個參與者回覆的是NO,或者超時了,則事務協調者向全部參與者發起事務回滾操做,全部參與者各自回滾事務,而後發送ACK,如圖10-2所示。

  

  因此,不管事務提交,仍是事務回滾,都是兩個階段。

 

 (2)2PC的實現。經過分析能夠發現,要實現2PC,全部參與者都要實現三個接口:Prepare、Commit、Rollback,這也就是XA協議,在Java中對應的接口是javax.transaction.xa.XAResource,一般的數據庫也都實現了這個協議。開源的Atomikos也基於該協議提供了2PC的解決方案,有興趣的讀者能夠進一步研究。

 

 (3)2PC的問題。2PC在數據庫領域很是常見,但它存在幾個問題:

  問題1:性能問題。在階段1,鎖定資源以後,要等全部節點返回,而後才能一塊兒進入階段2,不能很好地應對高併發場景。

  問題2:階段1完成以後,若是在階段2事務協調者宕機,則全部的參與者接收不到Commit或Rollback指令,將處於「懸而不決」狀態。

  問題3:階段1完成以後,在階段2,事務協調者向全部的參與者發送了Commit指令,但其中一個參與者超時或出錯了(沒有正確返回ACK),則其餘參與者提交仍是回滾呢? 也不能肯定。 

 爲了解決2PC的問題,又引入了3PC。3PC存在相似宕機如何解決的問題,所以仍是沒能完全解決問題,

  2PC除自己的算法侷限外,還有一個使用上的限制,就是它主要用在兩個數據庫之間(數據庫實現了XA協議)。但以支付寶的轉帳爲例,是兩個系統之間的轉帳,而不是底層兩個數據庫之間直接交互,因此沒有辦法使用2PC。

  不只支付寶,其餘業務場景基本都採用了微服務架構,不會直接在底層的兩個業務數據庫之間作一致性,而是在兩個服務上面實現一致性。 

  正由於2PC有諸多問題和不便,在實踐中通常不多使用。

 

2. 3PC(三階段提交)

  三階段提交協議(3PC)主要是爲了解決兩階段提交協議的阻塞問題,2pc存在的問題是當協做者崩潰時,參與者不能作出最後的選擇。所以參與者可能在協做者恢復以前保持阻塞。三階段提交(Three-phase commit),是二階段提交(2PC)的改進版本。

  與兩階段提交不一樣的是,三階段提交有兩個改動點。

  也就是說,除了引入超時機制以外,3PC把2PC的準備階段再次一分爲二,這樣三階段提交就有CanCommit、PreCommit、DoCommit三個階段。

一、CanCommit階段

  以前2PC的一階段是本地事務執行結束後,最後不Commit,等其它服務都執行結束並返回Yes,由協調者發生commit才真正執行commit。而這裏的CanCommit指的是 嘗試獲取數據庫鎖 若是能夠,就返回Yes。

 

這階段主要分爲2步

  • 事務詢問 協調者 向 參與者 發送CanCommit請求。詢問是否能夠執行事務提交操做。而後開始等待 參與者 的響應。
  • 響應反饋 參與者 接到CanCommit請求以後,正常狀況下,若是其自身認爲能夠順利執行事務,則返回Yes響應,並進入預備狀態。不然反饋No

二、PreCommit階段

  在階段一中,若是全部的參與者都返回Yes的話,那麼就會進入PreCommit階段進行事務預提交。這裏的PreCommit階段 跟上面的第一階段是差很少的,只不過這裏 協調者和參與者都引入了超時機制 (2PC中只有協調者能夠超時,參與者沒有超時機制)。

三、DoCommit階段

  這裏跟2pc的階段二是差很少的。

總結

相比較2PC而言,3PC對於協調者(Coordinator)和參與者(Partcipant)都設置了超時時間,而2PC只有協調者才擁有超時機制。這解決了一個什麼問題呢?

  這個優化點,主要是避免了參與者在長時間沒法與協調者節點通信(協調者掛掉了)的狀況下,沒法釋放資源的問題,由於參與者自身擁有超時機制會在超時後,

  自動進行本地commit從而進行釋放資源。而這種機制也側面下降了整個事務的阻塞時間和範圍。

  另外,經過CanCommit、PreCommit、DoCommit三個階段的設計,相較於2PC而言,多設置了一個緩衝階段保證了在最後提交階段以前各參與節點的狀態是一致的。

  以上就是3PC相對於2PC的一個提升(相對緩解了2PC中的前兩個問題),可是3PC依然沒有徹底解決數據不一致的問題。

 

 

3. 最終一致性(消息中間件) 

  通常的思路是經過消息中間件來實現「最終一致性」,如圖10-3所示。

  系統A收到用戶的轉帳請求,系統A先本身扣錢,也就是更新DB1;而後經過消息中間件給系統B發送一條加錢的消息,系統B收到此消息,對本身的帳號進行加錢,也就是更新DB2。

 這裏面有一個關鍵的技術問題:

  系統A給消息中間件發消息,是一次網絡交互;更新DB1,也是一次網絡交互。系統A是先更新DB1,後發送消息,仍是先發送消息,後更新DB1?

  假設先更新DB1成功,發送消息網絡失敗,重發又失敗,怎麼辦?又假設先發送消息成功,更新DB1失敗。消息已經發出去了,又不能撤回,怎麼辦?或者消息中間件提供了消息撤回的接口,可是又調用失敗怎麼辦?

  由於這是兩次網絡調用,兩個操做不是原子的,不管誰先誰後,都是有問題的。

 

下面來看最終一致性的幾種具體實現思路:

  a.最終一致性:錯誤的方案0

  有人可能會想,能夠把「發送加錢消息」這個網絡調用和更新DB1放在同一個事務裏面,若是發送消息失敗,更新DB自動回滾。這樣不就能夠保證兩個操做的原子性了嗎? 

  這個方案看似正確,實際上是錯誤的,緣由有兩點: 

(1)網絡的2將軍問題:發送消息失敗,發送方並不知道是消息中間件沒有收到消息,仍是消息已經收到了,只是返回response的時候失敗了?

  若是已經收到消息了,而發送端認爲沒有收到,執行update DB的回滾操做,則會致使帳戶A的錢沒有扣,帳戶B的錢卻被加了。

(2)把網絡調用放在數據庫事務裏面,可能會由於網絡的延時致使數據庫長事務。嚴重的會阻塞整個數據庫,風險很大。 

 

 b.最終一致性:第1種實現方式(業務方本身實現)

  假設消息中間件沒有提供「事務消息」功能,好比用的是Kafka。該如何解決這個問題呢?

  消息中間件實現最終一致性示意圖如圖10-4所示。

  (1)系統A增長一張消息表,系統A再也不直接給消息中間件發送消息,而是把消息寫入到這張消息表中。把DB1的扣錢操做(表1)和寫入消息表(表2)這兩個操做放在一個數據庫事務裏,保證二者的原子性。 

  (2)系統A準備一個後臺程序,源源不斷地把消息表中的消息傳送給消息中間件。若是失敗了,也不斷嘗試重傳。由於網絡的2將軍問題,系統A發送給消息中間件的消息網絡超時了,消息中間件可能已經收到了消息,也可能沒有收到。系統A會再次發送該消息,直到消息中間件返回成功。因此,系統A容許消息重複,但消息不會丟失,順序也不會打亂。

  (3)經過上面的兩個步驟,系統A保證了消息不丟失,但消息可能重複。系統B對消息的消費要解決下面兩個問題:

 問題1:丟失消費。系統B從消息中間件取出消息(此時還在內存裏面),若是處理了一半,系統B宕機並再次重啓,此時這條消息未處理成功,怎麼辦? 

  答案是經過消息中間件的ACK機制,凡是發送ACK的消息,系統B重啓以後消息中間件不會再次推送;凡是沒有發送ACK的消息,系統B重啓以後消息中間件會再次推送。

  但這又會引起一個新問題,就是下面問題2的重複消費:即便系統B把消息處理成功了,可是正要發送ACK的時候宕機了,消息中間件覺得這條消息沒有處理成功,系統B再次重啓的時候又會收到這條消息,系統B就會重複消費這條消息(對應加錢類的場景,帳號裏面的錢就會加兩次)

 問題2:重複消費。除了ACK機制,可能會引發重複消費;系統A的後臺任務也可能給消息中間件重複發送消息。

 

  爲了解決重複消息的問題,系統B增長一個判重表。判重表記錄了處理成功的消息ID和消息中間件對應的offset(以Kafka爲例),系統B宕機重啓,能夠定位到offset位置,從這以後開始繼續消費。 

  每次接收到新消息,先經過判重表進行判重,實現業務的冪等。一樣,對DB2的加錢操做和消息寫入判重表兩個操做,要在一個DB的事務裏面完成。 

  這裏要補充的是,消息的判重不止判重表一種方法。若是業務自己就有業務數據,能夠判斷出消息是否重複了,就不須要判重表了。

  經過上面三步,實現了消息在發送方的不丟失、在接收方的不重複,聯合起來就是消息的不漏不重,嚴格實現了系統A和系統B的最終一致性。

 

 但這種方案有一個缺點:系統A須要增長消息表,同時還須要一個後臺任務,不斷掃描此消息表,會致使消息的處理和業務邏輯耦合,額外增長業務方的開發負擔。

 

 c.最終一致性:第二種實現方式(基於RocketMQ事務消息)

  爲了能經過消息中間件解決該問題,同時又不和業務耦合,RocketMQ提出了「事務消息」的概念,如圖10-5所示。

 

 RocketMQ不是提供一個單一的「發送」接口,而是把消息的發送拆成了兩個階段,Prepare階段(消息預發送)和Confirm階段(確認發送)。具體使用方法以下: 

  步驟1:系統A調用Prepare接口,預發送消息。此時消息保存在消息中間件裏,但消息中間件不會把消息給消費方消費,消息只是暫存在那。

  步驟2:系統A更新數據庫,進行扣錢操做。

  步驟3:系統A調用Comfirm接口,確認發送消息。此時消息中間件纔會把消息給消費方進行消費。

 

 顯然,這裏有兩種異常場景: 

  場景1:步驟1成功,步驟2成功,步驟3失敗或超時,怎麼處理?

  場景2:步驟1成功,步驟2失敗或超時,步驟3不會執行。怎麼處理?

  這就涉及RocketMQ的關鍵點:RocketMQ會按期(默認是1min)掃描全部的預發送但尚未確認的消息,回調給發送方,詢問這條消息是要發出去,仍是取消。發送方根據本身的業務數據,知道這條消息是應該發出去(DB更新成功了),仍是應該取消(DB更新失敗)。

  對比最終一致性的兩種實現方案會發現,RocketMQ最大的改變實際上是把「掃描消息表」這件事不讓業務方作,而是讓消息中間件完成。 

  至於消息表,其實仍是沒有省掉。由於消息中間件要詢問發送方事物是否執行成功,還須要一個「變相的本地消息表」,記錄事務執行狀態和消息發送狀態。

  同時對於消費方,仍是沒有解決系統重啓可能致使的重複消費問題,這隻能由消費方解決。須要設計判重機制,實現消息消費的冪等。

 

 d.人工介入

  不管方案1,仍是方案2,發送端把消息成功放入了隊列中,但若是消費端消費失敗怎麼辦?

  若是消費失敗了,則能夠重試,但還一直失敗怎麼辦?是否要自動回滾整個流程? 

  答案是人工介入。從工程實踐角度來說,這種整個流程自動回滾的代價是很是巨大的,不但實現起來很複雜,還會引入新的問題。好比自動回滾失敗,又如何處理? 

  對應這種發生機率極低的事件,採起人工處理會比實現一個高複雜的自動化回滾系統更加可靠,也更加簡單。 

4. TCC  

提及分佈式事務的概念,很多人都會搞混淆,彷佛好像分佈式事務就是TCC。實際上TCC與2PC、3PC同樣,只是分佈式事務的一種實現方案而已。

TCC(Try-Confirm-Cancel)又稱補償事務。其核心思想是:"針對每一個操做都要註冊一個與其對應的確認和補償(撤銷操做)"。它分爲三個操做:

  • Try階段:主要是對業務系統作檢測及資源預留。

  • Confirm階段:確認執行業務操做。

  • Cancel階段:取消執行業務操做。

  2PC一般用來解決兩個數據庫之間的分佈式事務問題,比較侷限。如今企業採用的是各式各樣的SOA服務,更須要解決兩個服務之間的分佈式事務問題。

  爲了解決SOA系統中的分佈式事務問題,支付寶提出了TCC。TCC是Try、Confirm、Cancel三個單詞的縮寫,實際上是一個應用層面的2PC協議,Confirm對應2PC中的事務提交操做,Cancel對應2PC中的事務回滾操做,如圖10-6所示。

 

 (1)準備階段:調用方調用全部服務方提供的Try接口,該階段各調用方作資源檢查和資源鎖定,爲接下來的階段2作準備。

 (2)提交階段:若是全部服務方都返回YES,則進入提交階段,調用方調用各服務方的Confirm接口,各服務方進行事務提交。若是有一個服務方在階段1返回NO或者超時了,則調用方調用各服務方的Cancel接口,如圖10-7所示。

 

  這裏有一個關鍵問題:TCC既然也借鑑2PC的思路,那麼它是如何解決2PC的問題的呢?也就是說,在階段2,調用方發生宕機,或者某個服務超時了,如何處理呢? 

  答案是:不斷重試!無論是Confirm失敗了,仍是Cancel失敗了,都不斷重試。這就要求Confirm和Cancel都必須是冪等操做。注意,這裏的重試是由TCC的框架來執行的,而不是讓業務方本身去作。

  下面以一個轉帳的事件爲例,來講明TCC的過程。假設有三個帳號A、B、C,經過SOA提供的轉帳服務操做。A、B同時分別要向C轉30元、50元,最後C的帳號+80元,A、B各減30元、50元。

  階段1:分別對帳號A、B、C執行Try操做,A、B、C三個帳號在三個不一樣的SOA服務裏面,也就是分別調用三個服務的Try接口。具體來講,就是帳號A鎖定30元,帳號B鎖定50元,檢查帳號C的合法性,好比帳號C是否違法被凍結,帳號C是否已註銷。

  因此,在這個場景裏面,對應的「扣錢」的Try操做就是「鎖定」,對應的「加錢」的Try操做就是檢查帳號合法性,爲的是保證接下來的階段2扣錢可扣、加錢可加! 

  階段2:A、B、C的Try操做都成功,執行Confirm操做,即分別調用三個SOA服務的Confirm接口。A、B扣錢,C加錢。若是任意一個失敗,則不斷重試,直到成功爲止。

  從案例能夠看出,Try操做主要是爲了「保證業務操做的前置條件都獲得知足」,而後在Confirm階段,由於前置條件都知足了,因此能夠不斷重試保證成功。

 

  TCC事務的處理流程與2PC兩階段提交相似,不過2PC一般都是在跨庫的DB層面,而TCC本質上就是一個應用層面的2PC,須要經過業務邏輯來實現。這種分佈式事務的實現方式的優點在於,可讓應用本身定義數據庫操做的粒度,使得下降鎖衝突、提升吞吐量成爲可能

 

  而不足之處則在於對應用的侵入性很是強,業務邏輯的每一個分支都須要實現try、confirm、cancel三個操做。此外,其實現難度也比較大,須要按照網絡狀態、系統故障等不一樣的失敗緣由實現不一樣的回滾策略。爲了知足一致性的要求,confirm和cancel接口還必須實現冪等。

 

TCC的具體原理圖如👇:

5. 事務狀態表+調用方重試+接收方冪等

  一樣以轉帳爲例,介紹一種相似於TCC的方法。TCC的方法經過TCC框架內部來作,下面介紹的方法是業務方本身實現的。

  調用方維護一張事務狀態表(或者說事務日誌、日誌流水),在每次調用以前,落盤一條事務流水,生成一個全局的事務ID。事務狀態表的表結構如表1所示。 

  

  初始是狀態1,每調用成功1個服務則更新1次狀態,最後全部系統調用成功,狀態更新到狀態4,狀態二、3是中間狀態。固然,也能夠不保存中間狀態,只設置兩個狀態:Begin和End。事務開始以前的狀態是Begin,所有結束以後的狀態是End。若是某個事務一直停留在Begin狀態,則說明該事務沒有執行完畢。

  而後有一個後臺任務,掃描狀態表,在過了某段時間後(假設1次事務執行成功一般最多花費30s),狀態沒有變爲最終的狀態4,說明這條事務沒有執行成功。因而從新調用系統A、B、C。保證這條流水的最終狀態是狀態4(或End狀態)。固然,系統A、B、C根據全局的事務ID作冪等操做,因此即便重複調用也沒有關係。

補充說明:

 (1)若是後臺任務重試屢次仍然不能成功,要爲狀態表加一個Error狀態,經過人工介入干預。

 (2)對於調用方的同步調用,若是部分紅功,此時給客戶端返回什麼呢?

  答案是不肯定,或者說暫時未知。只能告訴用戶該筆錢轉帳超時,請稍後再來確認。

 (3)對於同步調用,調用方調用A或B失敗的時候,能夠重試三次。若是重試三次還不成功,則放棄操做,再交由後臺任務後續處理。

 

6 對帳 

  把上一節的方案擴展一下,豈止事務有狀態,系統中的各類數據對象都有狀態,或者說都有各自完整的生命週期,同時數據與數據之間存在着關聯關係。咱們能夠很好地利用這種完整的生命週期和數據之間的關聯關係,來實現系統的一致性,這就是「對帳」。

  在前面,咱們把注意力都放在了「過程」中,而在「對帳」的思路中,將把注意力轉移到「結果」中。什麼意思呢?

  在前面的方案中,不管最終一致性,仍是TCC、事務狀態表,都是爲了保證「過程的原子性」,也就是多個系統操做(或系統調用),要麼所有成功,要麼所有失敗。

  但全部的「過程」都必然產生「結果」,過程是咱們所說的「事務」,結果就是業務數據。一個過程若是部分執行成功、部分執行失敗,則意味着結果是不完整的。從結果也能夠反推出過程出了問題,從而對數據進行修補,這就是「對帳」的思路!

下面舉幾個對帳的例子。

  案例1:電商網站的訂單履約系統。一張訂單從「已支付」,到「下發給倉庫」,到「出倉完成」。假定從「已支付」到「下發給倉庫」最多用1個小時;從「下發給倉庫」到「出倉完成」最多用8個小時。意味着只要發現1個訂單的狀態過了1個小時以後還處於「已支付」狀態,就認爲訂單下發沒有成功,須要從新下發,也就是「重試」。一樣,只要發現訂單過了8個小時還未出倉,這時可能會發出報警,倉庫的做業系統是否出了問題……諸如此類。

  這個案例跟事務的狀態很相似:一旦發現系統中的某個數據對象過了一個限定時間生命週期仍然沒有走完,仍然處在某個中間狀態,就說明系統不一致了,要進行某種補償操做(好比重試或報警)。

  更復雜一點:訂單有狀態,庫存系統的庫存也有狀態,優惠系統的優惠券也有狀態,根據業務規則,這些狀態之間進行比對,就能發現系統某個地方不一致,作相應的補償。

  案例2:微博的關注關係。須要存兩張表,一張是關注表,一張是粉絲表,這兩張表各自都是分庫分表的。假設A關注了B,須要先以A爲主鍵進行分庫,存入關注表;再以B爲主鍵進行分庫,存入粉絲表。也就是說,一次業務操做,要向兩個數據庫中寫入兩條數據,如何保證原子性?

  案例3:電商的訂單系統也是分庫分表的。訂單一般有兩個經常使用的查詢維度,一個是買家,一個是賣家。若是按買家分庫,按賣家查詢就很差作;若是按賣家分庫,按買家查詢就很差作。這種一般會把訂單數據冗餘一份,按買家進行分庫分表存一份,按賣家再分庫分表存一份。和案例2存在一樣的問題:一個訂單要向兩個數據庫中寫入兩條數據,如何保證原子性?

  若是把案例二、案例3的問題看做爲一個分佈式事務的話,能夠用最終一致性、TCC、事務狀態表去實現,但這些方法都過重,一個簡單的方法是「對帳」。

  由於兩個庫的數據是冗餘的,能夠先保證一個庫的數據是準確的,以該庫爲基準校對另一個庫。

對帳又分爲全量對帳和增量對帳:

 (1)全量對帳。好比天天晚上運做一個定時任務,比對兩個數據庫。

 (2)增量對帳。能夠是一個定時任務,基於數據庫的更新時間;也能夠基於消息中間件,每一次業務操做都拋出一個消息到消息中間件,而後由一個消費者消費這條消息,對兩個數據庫中的數據進行比對(固然,消息可能丟失,沒法百分之百地保證,仍是須要全量對帳來兜底)。

  總之,對帳的關鍵是要找出「數據背後的數學規律」。有些規律比較直接,誰都能看出來,好比案例二、案例3的冗餘數據庫;有些規律隱含一些,好比案例1的訂單履約的狀態。找到了規律就能夠基於規律進行數據的比對,發現問題,而後補償。

 

7. 妥協方案:弱一致性+基於狀態的補償

 能夠發現:

  「最終一致性」是一種異步的方法,數據有必定延遲;

  TCC是一種同步方法,但TCC須要兩個階段,性能損耗較大;

  事務狀態表也是一種同步方法,但每次要記事務流水,要更新事務狀態,很煩瑣,性能也有損耗;

  「對帳」也是一個過後過程。

 若是須要一個同步的方案,既要讓系統之間保持一致性,又要有很高的性能,支持高併發,應該怎麼處理呢?

  如圖10-8所示,電商網站的下單至少須要兩個操做:建立訂單和扣庫存。訂單系統有訂單的數據庫和服務,庫存系統有庫存的數據庫和服務。先建立訂單,後扣庫存,可能會建立訂單成功,扣庫存失敗;反過來,先扣庫存,後建立訂單,可能會扣庫存成功,建立訂單失敗。如何保證建立訂單 + 扣庫存兩個操做的原子性,同時還要能抵抗線上的高併發流量?

 

  若是用最終一致性方案,由於是異步操做,若是庫存扣減不及時會致使超賣,所以最終一致性的方案不可行;若是用TCC方案,則意味着一個用戶請求要調用兩次(Try和Confirm)訂單服務、兩次(Try和Confirm)庫存服務,性能又達不到要求。若是用事務狀態表,要寫事務狀態,也存在性能問題。 

  既要知足高併發,又要達到一致性,魚和熊掌不能兼得。能夠利用業務的特性,採用一種弱一致的方案。

  對於該需求,有一個關鍵特性:對於電商的購物來說,容許少賣,但不能超賣。好比有100件東西,賣給99我的,有1件沒有賣出去,這是能夠接受的;但若是賣給了101我的,其中1我的拿不到貨,平臺違約,這就不能接受。而該處就利用了這個特性,具體作法以下。

 

 方案1:先扣庫存,後建立訂單。

  如表2所示,有三種狀況:

  (1)扣庫存成功,提交訂單成功,返回成功。

  (2)扣庫存成功,提交訂單失敗,返回失敗,調用方重試(此處可能會多扣庫存)。

  (3)扣庫存失敗,再也不提交訂單,返回失敗,調用方重試(此處可能會多扣庫存)。 

 

 方案2:先建立訂單,後扣庫存。

  如表3所示,也有三種狀況:

  (1)提交訂單成功,扣庫存成功,返回成功。

  (2)提交訂單成功,扣庫存失敗,返回失敗,調用方重試(此處可能會多扣庫存)。

  (3)提交訂單失敗,再也不扣庫存,調用方重試。

 

  不管方案1,仍是方案2,只要最終保證庫存能夠多扣,不能少扣便可。 

  可是,庫存多扣了,數據不一致,怎麼補償呢?

  庫存每扣一次,都會生成一條流水記錄。這條記錄的初始狀態是「佔用」,等訂單支付成功後,會把狀態改爲「釋放」。

  對於那些過了很長時間一直是佔用,而不釋放的庫存,要麼是由於前面多扣形成的,要麼是由於用戶下了單但沒有支付。 

  經過比對,獲得庫存系統的「佔用又沒有釋放的庫存流水」與訂單系統的未支付的訂單,就能夠回收這些庫存,同時把對應的訂單取消。相似12306網站,過必定時間不支付,訂單會取消,將庫存釋放。

 

8. 妥協方案:重試+回滾+報警+人工修復 

  上文介紹了基於訂單的狀態 +庫存流水的狀態作補償(或者說叫對帳)。若是業務很複雜,狀態的維護也很複雜,就能夠採用下面這種更加妥協而簡單的方法。

  按方案1,先扣庫存,後建立訂單。不作狀態補償,爲庫存系統提供一個回滾接口。建立訂單若是失敗了,先重試。若是重試還不成功,則回滾庫存的扣減。如回滾也失敗,則發報警,進行人工干預修復。

  總之,根據業務邏輯,經過三次重試或回滾的方法,最大限度地保證一致。實在不一致,就發報警,讓人工干預。只要日誌流水記錄得完整,人工確定能夠修復!一般只要業務邏輯自己沒問題,重試、回滾以後還失敗的機率會比較低,因此這種辦法雖然醜陋,但很實用。

 

9. 總結

   本章總結了實踐中比較可靠的七種方法:兩種最終一致性的方案,兩種妥協辦法,兩種基於狀態 + 重試 + 冪等的方法(TCC,狀態機+重試+冪等),還有一種對帳方法。

  在實現層面,妥協和對帳的辦法最容易,最終一致性次之,TCC最複雜。

原文:https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/88968532

相關文章
相關標籤/搜索