現今互聯網界,分佈式系統和微服務架構盛行。業界著名的CAP理論也告訴咱們,在設計和實現一個分佈式系統時,須要將數據一致性、系統可用性和分區容忍性放在一塊兒考慮。html
一、CAP理論git
在分佈式系統中,一致性(Consistency)、可用性(Availability)和分區容忍性(Partition Tolerance)3 個要素最多隻能同時知足兩個,不可兼得。其中,分區容忍性又是不可或缺的。github
舉例:Cassandra、Dynamo 等,默認優先選擇AP,弱化C;HBase、MongoDB 等,默認優先選擇CP,弱化A。算法
二、BASE 理論shell
核心思想:數據庫
數據的一致性模型能夠分紅如下 3 類:apache
分佈式系統數據的強一致性、弱一致性和最終一致性能夠經過Quorum NRW算法分析。網絡
分佈式事務的目的是保障分佈式存儲中數據一致性,而跨庫事務會遇到各類不可控制的問題,如個別節點宕機,像單機事務同樣的ACID是沒法奢望的。架構
一、Two/Three Phase Commit併發
2PC,中文叫兩階段提交。在分佈式系統中,每一個節點雖然能夠知曉本身的操做時成功或者失敗,卻沒法知道其餘節點的操做的成功或失敗。當一個事務跨越多個節點時,爲了保持事務的ACID特性,須要引入一個做爲協調者的組件來統一掌控全部節點(稱做參與者)的操做結果並最終指示這些節點是否要把操做結果進行真正的提交。 兩階段提交的算法以下:
第一階段:
第二階段:
兩段提交最大的問題就是第3)項,若是第一階段完成後,參與者在第二階沒有收到決策,那麼數據結點會進入「不知所措」的狀態,這個狀態會block住整個事務。也就是說,協調者Coordinator對於事務的完成很是重要,Coordinator的可用性是個關鍵。
因些,咱們引入三段提交,三段提交在Wikipedia上的描述以下,他把二段提交的第一個段break成了兩段:詢問,而後再鎖資源。最後真正提交。三段提交的核心理念是:在詢問的時候並不鎖定資源,除非全部人都贊成了,纔開始鎖資源。但三階段提交也存在一些缺陷,要完全從協議層面避免數據不一致,能夠採用Paxos或者Raft 算法。
目前兩階段提交、三階段提交存在以下的侷限性,並不適合在微服務架構體系下使用:
全部的操做必須是事務性資源(好比數據庫、消息隊列、EJB組件等),存在使用侷限性(微服務架構下多數使用HTTP協議),比較適合傳統的單體應用;
因爲是強一致性,資源須要在事務內部等待,性能影響較大,吞吐率不高,不適合高併發與高性能的業務場景;
二、Try Confirm Cancel(TCC)
一個完整的TCC業務由一個主業務服務和若干個從業務服務組成,主業務服務發起並完成整個業務活動,TCC模式要求從服務提供三個接口:Try、Confirm、Cancel。
Confirm:真正執行業務,不做任何業務檢查;只使用Try階段預留的業務資源;Confirm操做知足冪等性。
Cancel:釋放Try階段預留的業務資源;Cancel操做知足冪等性。
整個TCC業務分紅兩個階段完成:
第一階段:主業務服務分別調用全部從業務的try操做,並在活動管理器中登記全部從業務服務。當全部從業務服務的try操做都調用成功或者某個從業務服務的try操做失敗,進入第二階段。
第二階段:活動管理器根據第一階段的執行結果來執行confirm或cancel操做。若是第一階段全部try操做都成功,則活動管理器調用全部從業務活動的confirm操做。不然調用全部從業務服務的cancel操做。
與2PC比較:
缺點:
三、異步確保最終一致性
核心思想:
本地消息表
其基本的設計思想是將遠程分佈式事務拆分紅一系列的本地事務。若是不考慮性能及設計優雅,藉助關係型數據庫中的表便可實現。
舉個經典的跨行轉帳的例子來描述。
第一步僞代碼以下,扣款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了。那問題來了,如何通知到對方呢?
一般採用兩種方式:
兩種方式其實各有利弊,僅僅依靠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投遞消息的操做就必定能成功。這樣一致性彷佛很難保證。
咱們來分析下可能的狀況:
從上面分析的幾種狀況來看,貌似問題都不大的。那麼咱們來分析下消費者端面臨的問題:
如何保證消息與業務操做一致,不丟失?
主流的MQ產品都具備持久化消息的功能。若是消費者宕機或者消費失敗,均可以執行重試機制的(有些MQ能夠自定義重試次數)。
如何避免消息被重複消費形成的問題?
這種方式比較常見,性能和吞吐量是優於使用關係型數據庫消息表的方案。若是MQ自身和業務都具備高可用性,理論上是能夠知足大部分的業務場景的。不過在沒有充分測試的狀況下,不建議在交易業務中直接使用。
MQ(事務消息)
舉個例子,Bob向Smith轉帳,那咱們究竟是先發送消息,仍是先執行扣款操做?
好像均可能會出問題。若是先發消息,扣款操做失敗,那麼Smith的帳戶裏面會多出一筆錢。反過來,若是先執行扣款操做,後發送消息,那有可能扣款成功了可是消息沒發出去,Smith收不到錢。除了上面介紹的經過異常捕獲和回滾的方式外,還有沒有其餘的思路呢?
下面以阿里巴巴的RocketMQ中間件爲例,分析下其設計和實現思路。
RocketMQ第一階段發送Prepared消息時,會拿到消息的地址,第二階段執行本地事物,第三階段經過第一階段拿到的地址去訪問消息,並修改狀態。細心的讀者可能又發現問題了,若是確認消息發送失敗了怎麼辦?RocketMQ會按期掃描消息集羣中的事物消息,這時候發現了Prepared消息,它會向消息發送者確認,Bob的錢究竟是減了仍是沒減呢?若是減了是回滾仍是繼續發送確認消息呢?RocketMQ會根據發送端設置的策略來決定是回滾仍是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。以下圖:
各大知名的電商平臺和互聯網公司,幾乎都是採用相似的設計思路來實現「最終一致性」的。這種方式適合的業務場景普遍,並且比較可靠。不過這種方式技術實現的難度比較大。目前主流的開源MQ(ActiveMQ、RabbitMQ、Kafka)均未實現對事務消息的支持,因此需二次開發,可參考RocketMQ的事務消息(transactional message)。
總結:
閱讀了很多這方面的文章,在此基礎上,總結一下分佈式事務一致性的解決方案。分佈式系統的事務一致性自己就是一個技術難題,目前沒有一種很簡單很完美的方案可以應對全部場景。分佈式系統的一個難點就是由於「網絡通訊的不可靠」,只能經過「確認機制」、「重試機制」、「補償機制」等各方面來解決一些問題。在綜合考慮可用性、性能、實現複雜度等各方面的狀況上,比較好的選擇是「異步確保最終一致性」,只是具體實現方式上有一些差別。
參考:
理性撕逼!分佈式事務:不過是在一致性、吞吐量和複雜度之間,作一個選擇
程立:《大規模SOA系統中的分佈事務處事》