分佈式系統事務一致性

一 分佈式系統特色

現今互聯網界,分佈式系統和微服務架構盛行。業界著名的CAP理論也告訴咱們,在設計和實現一個分佈式系統時,須要將數據一致性、系統可用性和分區容忍性放在一塊兒考慮。html

一、CAP理論git

在分佈式系統中,一致性(Consistency)、可用性(Availability)和分區容忍性(Partition Tolerance)3 個要素最多隻能同時知足兩個,不可兼得。其中,分區容忍性又是不可或缺的。github

  • 一致性:分佈式環境下多個節點的數據是否強一致。
  • 可用性:分佈式服務能一直保證可用狀態。當用戶發出一個請求後,服務能在有限時間內返回結果。
  • 分區容忍性:特指對網絡分區的容忍性。

舉例:Cassandra、Dynamo 等,默認優先選擇AP,弱化C;HBase、MongoDB 等,默認優先選擇CP,弱化A。算法

二、BASE 理論shell

核心思想:數據庫

  • 基本可用(Basically Available):指分佈式系統在出現故障時,容許損失部分的可用性來保證核心可用。
  • 軟狀態(Soft State):指容許分佈式系統存在中間狀態,該中間狀態不會影響到系統的總體可用性。
  • 最終一致性(Eventual Consistency):指分佈式系統中的全部副本數據通過必定時間後,最終可以達到一致的狀態。

二 一致性模型

數據的一致性模型能夠分紅如下 3 類:apache

  1. 強一致性:數據更新成功後,任意時刻全部副本中的數據都是一致的,通常採用同步的方式實現。
  2. 弱一致性:數據更新成功後,系統不承諾當即能夠讀到最新寫入的值,也不承諾具體多久以後能夠讀到。
  3. 最終一致性:弱一致性的一種形式,數據更新成功後,系統不承諾當即能夠返回最新寫入的值,可是保證最終會返回上一次更新操做的值。

分佈式系統數據的強一致性、弱一致性和最終一致性能夠經過Quorum NRW算法分析。網絡

三 分佈式事務

分佈式事務的目的是保障分佈式存儲中數據一致性,而跨庫事務會遇到各類不可控制的問題,如個別節點宕機,像單機事務同樣的ACID是沒法奢望的。架構

一、Two/Three Phase Commit併發

2PC,中文叫兩階段提交。在分佈式系統中,每一個節點雖然能夠知曉本身的操做時成功或者失敗,卻沒法知道其餘節點的操做的成功或失敗。當一個事務跨越多個節點時,爲了保持事務的ACID特性,須要引入一個做爲協調者的組件來統一掌控全部節點(稱做參與者)的操做結果並最終指示這些節點是否要把操做結果進行真正的提交。 兩階段提交的算法以下:

第一階段:

  1. 協調者會問全部的參與者結點,是否能夠執行提交操做。
  2. 各個參與者開始事務執行的準備工做:如:爲資源上鎖,預留資源。
  3. 參與者響應協調者,若是事務的準備工做成功,則迴應「能夠提交」,不然迴應「拒絕提交」。

第二階段:

  • 若是全部的參與者都回應「能夠提交」,那麼,協調者向全部的參與者發送「正式提交」的命令。參與者完成正式提交,並釋放全部資源,而後迴應「完成」,協調者收集各結點的「完成」迴應後結束這個Global Transaction。
  • 若是有一個參與者迴應「拒絕提交」,那麼,協調者向全部的參與者發送「回滾操做」,並釋放全部資源,而後迴應「回滾完成」,協調者收集各結點的「回滾」迴應後,取消這個Global Transaction。

兩段提交最大的問題就是第3)項,若是第一階段完成後,參與者在第二階沒有收到決策,那麼數據結點會進入「不知所措」的狀態,這個狀態會block住整個事務。也就是說,協調者Coordinator對於事務的完成很是重要,Coordinator的可用性是個關鍵。

因些,咱們引入三段提交,三段提交在Wikipedia上的描述以下,他把二段提交的第一個段break成了兩段:詢問,而後再鎖資源。最後真正提交。三段提交的核心理念是:在詢問的時候並不鎖定資源,除非全部人都贊成了,纔開始鎖資源。但三階段提交也存在一些缺陷,要完全從協議層面避免數據不一致,能夠採用Paxos或者Raft 算法

目前兩階段提交、三階段提交存在以下的侷限性,並不適合在微服務架構體系下使用:

  • 全部的操做必須是事務性資源(好比數據庫、消息隊列、EJB組件等),存在使用侷限性(微服務架構下多數使用HTTP協議),比較適合傳統的單體應用;

  • 因爲是強一致性,資源須要在事務內部等待,性能影響較大,吞吐率不高,不適合高併發與高性能的業務場景;

二、Try Confirm Cancel(TCC)

一個完整的TCC業務由一個主業務服務和若干個從業務服務組成,主業務服務發起並完成整個業務活動,TCC模式要求從服務提供三個接口:Try、Confirm、Cancel。

  1. Try:完成全部業務檢查,預留必須業務資源。
  2. Confirm:真正執行業務,不做任何業務檢查;只使用Try階段預留的業務資源;Confirm操做知足冪等性。

  3. Cancel:釋放Try階段預留的業務資源;Cancel操做知足冪等性。

整個TCC業務分紅兩個階段完成:

第一階段:主業務服務分別調用全部從業務的try操做,並在活動管理器中登記全部從業務服務。當全部從業務服務的try操做都調用成功或者某個從業務服務的try操做失敗,進入第二階段。

第二階段:活動管理器根據第一階段的執行結果來執行confirm或cancel操做。若是第一階段全部try操做都成功,則活動管理器調用全部從業務活動的confirm操做。不然調用全部從業務服務的cancel操做。

與2PC比較:

  • 位於業務服務層而非資源層。
  • 沒有單獨的準備(prepare)階段,Try操做兼備資源操做與準備能力。
  • Try操做能夠靈活選擇業務資源的鎖定粒度。
  • 開發成本較高。

缺點:

  • Canfirm和Cancel的冪等性很難保證。
  • 這種方式缺點比較多,一般在複雜場景下是不推薦使用的,除非是很是簡單的場景,很是容易提供回滾Cancel,並且依賴的服務也很是少的狀況。
  • 這種實現方式會形成代碼量龐大,耦合性高。並且很是有侷限性,由於有不少的業務是沒法很簡單的實現回滾的,若是串行的服務不少,回滾的成本實在過高。

三、異步確保最終一致性

核心思想:

eBay 的架構師Dan Pritchett,曾在一篇解釋BASE 原理的論文《 Base:An Acid Alternative》中提到一個eBay 分佈式系統一致性問題的解決方案。它的核心思想是將須要分佈式處理的任務經過消息或者日誌的方式來異步執行,消息或日誌能夠存到本地文件、數據庫或消息隊列,再經過業務規則進行失敗重試,它要求各服務的接口是冪等的。

本地消息表

其基本的設計思想是將遠程分佈式事務拆分紅一系列的本地事務。若是不考慮性能及設計優雅,藉助關係型數據庫中的表便可實現。

舉個經典的跨行轉帳的例子來描述。

第一步僞代碼以下,扣款100,經過本地事務保證了憑證消息插入到消息表中:

begin transaction:
  update User set account = account - 100 where userId = 'A'
  insert into message(msgId, userId, amount, status) values('123','A', 100, 1)
commit transaction

第二步,通知對方銀行帳戶上加100了。那問題來了,如何通知到對方呢?

一般採用兩種方式:

  1. 採用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件。
  2. 採用定時輪詢掃描的方式,去檢查消息表的數據。

兩種方式其實各有利弊,僅僅依靠MQ,可能會出現通知失敗的問題。而過於頻繁的定時輪詢,效率也不是最佳的(90%是無用功)。因此,咱們通常會把兩種方式結合起來使用。

解決了通知的問題,又有新的問題了。萬一這消息有重複被消費,往用戶賬號上多加了錢,那豈不是後果很嚴重?其實咱們能夠消息消費方也經過一個「消費狀態表」來記錄消費狀態。在執行「加款」操做以前,檢測下該消息(提供標識)是否已經消費過,消費完成後,經過本地事務控制來更新這個「消費狀態表」。這樣子就避免重複消費的問題:

get msgId = '123';
check if mgsId is in message_applied(msgId);
if not applied:
    begin transaction:
        update User set account = account + 100 where userId = 'B'
        insert into message_applied(msgId) values('123')
    commit transaction

上訴的方式是一種很是經典的實現,基本避免了分佈式事務,實現了「最終一致性」。可是,關係型數據庫的吞吐量和性能方面存在瓶頸,頻繁的讀寫消息會給數據庫形成壓力。因此,在真正的高併發場景下,該方案也會有瓶頸和限制的。

MQ(非事務消息)

一般狀況下,在使用非事務消息支持的MQ產品時,咱們很難將業務操做與對MQ的操做放在一個本地事務域中管理。仍是以上述提到的「跨行轉帳」爲例,咱們很難保證在扣款完成以後對MQ投遞消息的操做就必定能成功。這樣一致性彷佛很難保證。

咱們來分析下可能的狀況:

  1. 操做數據庫成功,向MQ中投遞消息也成功,皆大歡喜。
  2. 操做數據庫失敗,不會向MQ中投遞消息了。
  3. 操做數據庫成功,可是向MQ中投遞消息時失敗,向外拋出了異常,剛剛執行的更新數據庫的操做將被回滾。

從上面分析的幾種狀況來看,貌似問題都不大的。那麼咱們來分析下消費者端面臨的問題:

  1. 消息出列後,消費者對應的業務操做要執行成功。若是業務執行失敗,消息不能失效或者丟失。須要保證消息與業務操做一致。
  2. 儘可能避免消息重複消費。若是重複消費,也不能所以影響業務結果。

如何保證消息與業務操做一致,不丟失?

主流的MQ產品都具備持久化消息的功能。若是消費者宕機或者消費失敗,均可以執行重試機制的(有些MQ能夠自定義重試次數)。

如何避免消息被重複消費形成的問題?

  1. 保證消費者調用業務的服務接口的冪等性。
  2. 經過消費日誌或者相似狀態表來記錄消費狀態,便於判斷(建議在業務上自行實現,而不依賴MQ產品提供該特性)。

這種方式比較常見,性能和吞吐量是優於使用關係型數據庫消息表的方案。若是MQ自身和業務都具備高可用性,理論上是能夠知足大部分的業務場景的。不過在沒有充分測試的狀況下,不建議在交易業務中直接使用。

MQ(事務消息)

舉個例子,Bob向Smith轉帳,那咱們究竟是先發送消息,仍是先執行扣款操做?

好像均可能會出問題。若是先發消息,扣款操做失敗,那麼Smith的帳戶裏面會多出一筆錢。反過來,若是先執行扣款操做,後發送消息,那有可能扣款成功了可是消息沒發出去,Smith收不到錢。除了上面介紹的經過異常捕獲和回滾的方式外,還有沒有其餘的思路呢?

下面以阿里巴巴的RocketMQ中間件爲例,分析下其設計和實現思路。

RocketMQ第一階段發送Prepared消息時,會拿到消息的地址,第二階段執行本地事物,第三階段經過第一階段拿到的地址去訪問消息,並修改狀態。細心的讀者可能又發現問題了,若是確認消息發送失敗了怎麼辦?RocketMQ會按期掃描消息集羣中的事物消息,這時候發現了Prepared消息,它會向消息發送者確認,Bob的錢究竟是減了仍是沒減呢?若是減了是回滾仍是繼續發送確認消息呢?RocketMQ會根據發送端設置的策略來決定是回滾仍是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。以下圖:

各大知名的電商平臺和互聯網公司,幾乎都是採用相似的設計思路來實現「最終一致性」的。這種方式適合的業務場景普遍,並且比較可靠。不過這種方式技術實現的難度比較大。目前主流的開源MQ(ActiveMQ、RabbitMQ、Kafka)均未實現對事務消息的支持,因此需二次開發,可參考RocketMQ的事務消息(transactional message)。

總結:

閱讀了很多這方面的文章,在此基礎上,總結一下分佈式事務一致性的解決方案。分佈式系統的事務一致性自己就是一個技術難題,目前沒有一種很簡單很完美的方案可以應對全部場景。分佈式系統的一個難點就是由於「網絡通訊的不可靠」,只能經過「確認機制」、「重試機制」、「補償機制」等各方面來解決一些問題。在綜合考慮可用性、性能、實現複雜度等各方面的狀況上,比較好的選擇是「異步確保最終一致性」,只是具體實現方式上有一些差別。

 

參考:

分佈式系統的事務處理

分佈式系統事務一致性解決方案

理性撕逼!分佈式事務:不過是在一致性、吞吐量和複雜度之間,作一個選擇

知乎:經常使用的分佈式事務解決方案介紹有多少種?

一次給女友轉帳引起我對分佈式事務的思考

用消息隊列和消息應用狀態表來消除分佈式事務

程立:《大規模SOA系統中的分佈事務處事》

相關文章
相關標籤/搜索