最終一致性:BASE論文筆記

最終一致性:BASE論文筆記

簡述

Base論文是ebay的架構師於2008年提交的一篇論文。主要用來闡述在分佈式架構設計下,基於BASE的設計思想和方案。所謂BASE就是basically available(基本的可用性),soft state(軟狀態,所謂的軟狀態,指的是暫時的不一致,後文會詳細展開),eventually consistent(最終一致性)。在分佈式領域,有著名的CAP理論,也就是一致性,可用性,分區容錯性便可靠性,這三者沒法同時得到。而base理論就是在犧牲部分一致性的基礎上,來達到可用性的大幅提高的一種方案理念。
歡迎加入技術交流羣186233599討論交流,也歡迎關注技術公衆號:風火說。java

分區容錯性

這是BASE裏相對簡單好操做的一個地方。好比咱們須要存儲用戶數據,經過部署多個物理實例,將用戶數據均勻的分散在其上。即便其中的一個服務器發生宕機,也不會影響到其餘的數據。容忍了局部失敗而提供了總體上必定的容錯性。
容錯的問題經過將數據部署多個節點來保證。那就帶來下面的問題,數據的一致性如何保證。傳統的業務解決方式就是2PC。依賴於數據庫提供的XA事務來實現分佈式下數據一致性。sql

傳統的數據庫事務方式在分佈式領域的問題

考慮一個這樣的場景,A給B進行轉帳。2個用戶在不一樣的銀行,他們的數據庫部署在不一樣的物理節點。而轉帳是一個事務操做。傳統意義上有所謂的分佈式事務,也就是2PC這種協議來保證分佈式狀況下的事務。可是2PC協議的開銷很大,不利於在大規模的狀況下的性能表現。而且XA只是數據庫層面的協議,若是應用自己是分佈式的,還須要額外的落地支持。在實現上也不簡單。數據庫

BASE方式來解決

這個轉帳例子有兩個數據庫,若是A成功了,B失敗了,此時就須要回滾A。若是咱們不回滾,而是重試直到B成功也是一種可行的方案。對於同一個數據庫實例,在一個鏈接中可使用事務操做不一樣的表。基於以上的兩點,咱們給A增長一個消息表,用來存儲須要別的庫異步配合的執行消息。在這個例子中,用於存儲向B發送的消息。那麼咱們的操做以下服務器

  1. 在A中開啓事務,對用戶執行扣錢sql,而且往消息表中新增一個消息,消息的內容是要求B執行加錢的操做。
  2. 提交事務。若是事務失敗則回滾,該業務失敗。此時B徹底無感知。
  3. 若是事務提交成功,則向B發出調用,執行增長錢的操做。若是B回覆成功,將消息表中的該消息刪除。
  4. 若是B回覆失敗,則發起重試,或者依靠定時任務不斷重試,直到成功。
  5. 成功後刪除消息表中的該消息。

咱們來分析下上面的作法,首先經過A中的事務保證了在A上轉帳的操做落地完成而且有記錄能夠查詢(消息表中的就是未完成的記錄)。在事務提交成功後再執行和B相關的操做,返回成功才刪除消息表,這樣就保證操做最後老是能夠成功(由於B返回了成功消息)。能夠看到,在A事務成功到B調用成功這之間,數據在兩個數據庫上存在不一致的狀況,這也是BASE理論中犧牲的強一致性的地方,可是經過這樣的作法,數據在兩個系統中最終是能夠達到一致的,也便是所謂的最終一致性。經過犧牲強一致性,提升了系統的吞吐。而且這個不一致的時間窗口實際上對於通常的用戶是無感知的。可能就是在幾十毫秒到兩三秒之間,用戶是能夠容許也是理解這樣的延遲的。
那麼上面的方案是否就已經能夠解決問題了呢,答案是不。網絡

冪等

在上面的流程能夠看到,在A事務成功之後要調用B的接口,若是調用失敗是須要重複調用直到成功的。問題在於,因爲須要網絡傳輸調用結果,有可能B調用其實是成功了,可是網絡中斷致使A沒法收到消息。那麼A就會認爲是調用失敗,從而再次發起調用。那麼B就將一個加錢的動做執行了2次。此時兩邊的數據處於不一致的狀態,而且沒法修復。爲了解決這個問題,咱們引入冪等約束。所謂冪等操做也就是說對於該操做,一次或屢次的調用產生的結果是相同的。上面的問題就是因爲調用B加錢的操做不是冪等,而A在理論上必然存在重複調用的狀況(由於網絡是不可靠的),進而致使數據不一致錯誤。那麼怎麼讓B的加錢操做是冪等的呢?A中存在一個消息表,用於存放須要執行操做的消息,那麼給B中也增長一個更新表。對於A中的每個消息都存在一個全局惟一的id。那麼調用B的加錢操做的流程修改成以下架構

  1. B開啓事務。檢查更新表中是否存在消息id。若是存在消息id,直接忽略該操做。而且返回A成功消息。
  2. 若是不存在消息id,執行用戶的加錢操做,而且往更新表中插入該消息。提交事務。提交事務成功返回A成功消息,提交失敗則返回失敗消息。

使用這樣的邏輯,則B的加錢操做就成爲了一個冪等操做,能夠承受屢次調用。異步

簡單的冪等

上面方案的冪等依靠本地的更新表記錄了全部的消息id進行比對進而防止屢次的重複調用。這樣須要一個更新表而且要存儲全部的消息,比較重一些。若是咱們給於消息一個不斷遞增的序號,而且b的數據表中新增一個序號字段。b只要執行消息前會比對消息的序號和自身數據的序號。若是消息序號大於自身序號才能夠執行。也就是執行以下的僞代碼分佈式

begin transaction
update b set money=message.money+b.money,version=message.version where b.id = message.userid and b.version > message.version
commit
end transaction

經過這樣的方式,就不須要啓用一個單獨的更新表。而後對於B的業務表有侵入性的修改。性能

中間總結

在有了上面的例子,如今咱們來稍微總結。經過將一致性要求從強一致性下降到最終一致性,咱們能夠避免2PC這樣的高成本協議,而且讓業務具備更強的伸縮性。將上面具體的方案抽象下,其中的思路仍是比較清晰的。架構設計

  1. 將一個分佈式的事務拆分紅多個本地事務,引入消息概念。
  2. 將本地數據更新和消息的新增綁定爲一個事務,做爲總體進行提交。
  3. 在在一個本地事務成功的狀況下,進行下一個遠端的事務操做。而且要求該遠端事務操做具有冪等性,能夠承受重複的屢次調用而不會致使數據錯誤。
  4. 消息能夠被本地被屢次嘗試或者在異步組件中嘗試直到消息送達而且操做成功。最終讓全部系統的數據達到一致。

上面的思路重點就是在異步消息這個概念的引入。而冪等的保證方式能夠有兩種,一種是被調用方,也就是接口提供方,自身保存一份執行過的消息表,用於在執行操做前進行比對,避免執行重複操做。一種是將消息引入序號改變,被調用方只執行比自身序號大的消息。

TCC類型的冪等

上面的基於存儲消息方式的冪等因爲須要存儲執行過的消息會帶來額外的存儲開銷。而且執行過的消息理論上已經失去了其意義(假設調用方執行成功,後續就不會再去調用,那麼就沒有判重的需求了)。這種方式中,消息的序號是由消息的發送方來生成的,而且被調用方始終須要存儲着全部的操做歷史,歷史數據會愈來愈多,而且都是無用的歷史數據。那咱們換個思路,消息的序號是由消息的收取方來生成如何。具體的操做以下

  1. A開啓事務,執行本地業務更新,調用B的try接口傳遞業務參數。B的try接口調用成功則返回一個全局惟一的消息id
  2. A將這個消息id寫入到消息表中。提交事務。
  3. 若是事務提交成功,調用B的confirm接口。接口的參數只有消息id。代表該業務確認須要執行。B將真實執行該業務,而且將confirm執行結果返回給予A。若是成功則刪除消息表中的該消息。若是失敗則經過異步組件發生重試。
  4. 若是事務提交失敗,則調用B的cancel接口,接口參數只有消息id。代表取消該業務的執行。

在這種方式中,消息的id是由被調用方被調用try接口時產生。此時被調用方存儲該消息id。在被調用confirm或者cancel接口時,首先檢查該id是否存在,存在才執行下一步的操做。對於cancel接口而言,操做只是簡單的刪除該消息便可。而對於confirm操做,則須要開啓事務,而且在事務中執行對應的操做,而且刪除消息。最終提交事務。若是事務成功提交則返回成功消息,不然返回失敗。若是消息id不存在,有兩種可能。

  • 消息id是錯誤的
  • 該消息已經被執行了。

在內部可信的系統內,排除第一種狀況,只有第二種狀況。故而在這種狀況,confirm接口的調用也是返回成功消息。這種方式,被調用方只須要存儲必定規模的消息id,由於被成功執行的消息都會被刪除。再給全部存儲的消息一個過時時間。由後臺定時組件按期掃描刪除便可。這樣,消息的堆積大小也是可控的了。

相關文章
相關標籤/搜索