分佈式系統(微服務架構)的一致性和冪等性問題相關概念解析

前言數據庫

什麼是分佈式系統?關於這點其實並無明確且統一的定義。在我看來,只要一個系統知足如下幾點就能夠稱之爲分佈式系統服務器

  • 系統由物理上不一樣分佈的多個機器節點組成
  • 系統的多個節點經過網絡進行通訊,協調彼此之間的工做。
  • 系統做爲總體統一對外提供服務,其分佈式細節對客戶端透明。

要想更好的理解分佈式系統,並正確使用甚至構建分佈式系統,須要理解其中的兩個關鍵概念——分佈式系統的數據一致性和分佈式系統的冪等性。網絡

1. 分佈式系統的數據一致性架構

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。併發

對於分佈式系統,數據可能存在於不一樣的物理節點上,節點之間只能經過網絡進行通訊來協調彼此之間的狀態,而網絡通訊須要時間而且其自己並不十分可靠,於是如何保持數據一致性成爲了分佈式系統的難題。對於不一樣的分佈式系統,其一致性語義以及面對的一致性難題可能略有差異分佈式

1.1 分佈式存儲系統中的一致性問題函數

在分佈式存儲系統中,爲了保持系統的高可用,同時增長讀操做的併發性,同一份數據會有多份副本,不一樣的副本存儲於不一樣的節點上,以下圖所示微服務

 

在併發環境下,由於存在多個客戶端同時讀取同一數據在不一樣節點上的副本,於是如何維護數據的一致性視圖就很是重要,即對於使用該分佈式系統的客戶端而言,對於多副本數據的讀寫其表現應該和單份數據同樣,一般系統是經過數據複製的方式來達到這一點的,源碼分析

  • 客戶端將節點1中的副本A修改成10,系統將經過網絡通訊的方式將節點2和節點3中的副本A也更新爲10。然而網絡通訊是須要時間的,假設在系統還未將節點1中的A值同步到節點2和節點3,此時另外一個客戶端訪問了節點2和節點3,這個時候系統怎麼辦?
  • 甚至,考慮更極端的場景,節點之間的網絡被斷開,不一樣節點沒法感知到彼此的存在,固然也就沒法保持多副本數據的同一視圖,那麼這個時候系統又該怎麼辦?

1.2 微服務應用的分佈式一致性問題性能

微服務架構下,原有的單體應用按功能被拆分紅一個個微服務應用,每一個微服務應用被部署在不一樣的機器節點上,只完成原有單體應用的某一部分功能,操做屬於該業務功能的數據庫或表。彼此以前經過網絡通訊的方式協調彼此之間的工做,做爲總體共同對外提供服務,於是一個業務功能的實現,可能會涉及到多個微服務的調用,操做物理上不一樣的多個數據庫或表。好比對於下單並支付這個業務功能而言,須要調用下單微服務和支付微服務來共同完成。

 

對於下單並支付這一業務功能,應用先調用訂單微服務,在訂單數據庫中添加一條訂單記錄,成功後再調用支付微服務添加相應的支付記錄,只有這兩個微服務都調用成功,該業務功能纔算執行成功。這個過程可能存在如下的問題:

  • 訂單微服務調用成功,訂單記錄已落地,可是支付微服務因爲各類緣由遲遲得不到響應,此時用戶經過訂單號查詢只能查到訂單記錄而查不到支付記錄,這對於已經成功付款的用戶而言確定是沒法接受的,這種狀況該怎麼辦?
  • 訂單微服務調用成功,訂單記錄已落地,可是支付微服務調用失敗,此時訂單記錄和支付記錄所對應的業務狀態不一致,這時候系統該怎麼辦?

1.3 對於一致性的正確理解

分佈式存儲系統的一致性問題,主要在於如何維持多副本的一致性視圖上,即如何使多份數據對外表現的和一份數據同樣。而微服務架構下的分佈式應用系統,其一致性問題主要在於如何使不一樣微服務的數據對同一業務狀態的描述保持一致,好比對於下單並支付這一業務操做而言,下單和支付要麼同時成功,要麼應該同時失敗,而不該該一個成功一個失敗,而且在這個過程當中,某部分已經成功或失敗的數據是否應該對客戶端可見。在聯繫一下本地事務ACID中的一致性,咱們可能會產生必定的混亂:它們講的一致性是一個東西嗎?先說下個人我的理解:無論是ACID的一致性仍是不一樣分佈式系統中的一致性,它們本質上講的是一件事:數據的一致性,在於正確的反應現實世界,對發生於現實世界的事情的正確描述。這就要求,一致性的數據至少要知足如下兩個條件:

  • 1.符合系統自己具備的約束條件,好比數據庫中的數據要遵循主碼,外碼,check約束。
  • 2.與特定業務有關的全部數據,它們對業務執行狀態的描述應該保持一致。好比從A帳戶轉帳100元到B帳戶這一業務操做,無論A帳戶和B帳戶是否在一個數據庫,也無論這一業務操做是否執行成功,兩個帳戶的總金額應該保持不變;若是有關帳戶金額的數據存儲在分佈式系統的多個不一樣的副本,則這些副本的數據應該同樣。

從這個意義上,無論是單機數據庫仍是分佈式存儲系統仍是微服務架構下的分佈式應用,對一致性的追求本質上是同樣的:在知足系統自己約束的前提下,對於發生的業務操做及其執行狀態的一致性描述。只不過因爲分佈式系統數據的分佈式存儲以及網絡通訊情況的複雜,使得分佈式系統要保持數據一致性相比單機應用要考慮更多複雜的因素,實現也要困難的多。不少文章把它們作了嚴格的區分,我的以爲很沒有必要,也不利於對於一致性的正確理解,從哲學的角度看,是割裂了事物共性和個性之間的聯繫。

2.分佈式一致性模型

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

就好像單機數據庫中爲事務的隔離性設置了不一樣的級別,分佈式系統中對數據的一致性級別也有分類。總的來講能夠分爲強一致性和弱一致性兩大類,弱一致性中又能夠繼續細分爲最終一致性,因果一致性,會話一致性,單調讀一致性和單調寫一致性等多種,不過弱一致性中只有最終一致性比較重要,其餘的能夠暫時忽略。

 

  • 強一致性
  • 以帶多副本的分佈式存儲系統爲例,全部鏈接到分佈式系統的客戶端看到的某一數據的值都是同樣的。當某個客戶端修改了這個值,後續的全部客戶端都能讀取到這個更新的值,而且全部的更新操做都在這個新的值的基礎上進行,直到這個值被再次修改,以下圖所示,在A修改X前全部客戶端都能讀取到X的值爲1,在A將X修改成2以後,全部客戶端都能讀取到這個更新後的值。

 

  •  
  • 最終一致性
  • 全部不能知足強一致性要求的都稱爲弱一致性,而最終一致性是其中比較強的一種。在最終一致性模型下,當數據項X被修改後,客戶端並不必定能立刻看到這個更新後的值(有些可能讀取到了新值,有些讀取到的可能仍是舊值),可是在一段時間後,全部客戶端都能讀取到這個更新後的值並進行相關操做。最終一致性模型下,分佈式數據最終能達到一致,可是須要通過一段時間,這段時間稱爲不一致窗口。
  • 以下圖所示,在A將X修改成2後,在不一致窗口內只有B能讀取到X=2,其餘客戶端讀取到的依舊是X=1。可是在不一致窗口後,全部客戶端都能讀取到X=2。

 

  •  

3. 追求強一致性的約束——CAP定理

嚴格意義上來說,真正的一致性模型只有一種——強一致性,這也是一種理想化的模型。它爲分佈式數據維護了徹底一致的視圖,使得一旦修改了數據後,全部客戶端可以立刻看到這個更新後的值並基於這個新值進行後續的操做,使得咱們操做分佈式數據和操做本地數據同樣。在分佈式系統中要實現一致性須要考慮其餘因素,好比可用性和分區容忍性,而這些因素相互有制約,這種制約關係在CAP定理中被很好的進行了描述。

CAP是"Consistency","Availabilty","Partition Tolerance"的簡稱,分別表明了:強一致性,可用性和分區容忍性,它們的含義分別以下:

  • 強一致性:在分佈式系統同一份數據有多副本的狀況下,對於數據的操做效果和只有單份數據同樣。
  • 可用性:客戶端在任什麼時候刻對數據的讀/寫操做都應該保證在時限內完成。
  • 分區容忍性:當分佈式系統出現網絡分區,不一樣分區間的機器沒法進行網絡通訊時,系統仍然可以繼續工做。

CAP定理的內容:對於一個分佈式系統,沒法同時實現強一致性,可用性和分區容忍性,即CAP三要素不可兼得。

3.1 如何理解CAP三要素不可兼得

因爲網絡的不可靠性,網絡分區的狀況不可避免的會發生,當出現網絡分區時,不一樣分區的機器沒法進行通訊。分佈式系統必須可以在出現網絡分區的狀況下繼續工做,於是對於分佈式系統而言,P即分區容忍性是必需要具有的要素,那麼問題就轉化爲了,在系統知足分區容忍性的前提下,爲何強一致性和可用性不可兼得。

假設數據項A的三個副本分別存儲在不一樣的物理節點,在某一時刻,系統狀態以下圖所示

 

當客戶端將節點1上的A修改成2後,系統出現了網絡分區,其中節點1和節點2在一個網絡分區中,而節點3在另外一個分區中

 

當有客戶端嘗試讀取節點3上的A值時,系統將面臨兩難困境

  • 系統等待節點3從節點1同步A的值,待數據一致後再返回客戶端響應,可是由於節點3和節點1不在一個分區中,雙方沒法進行通訊,致使系統沒法在限定時間內給客戶端返回讀取結果,這明顯不符合可用性的要求。
  • 系統當即返回一個A=1的舊值給客戶端,因爲A的值在不一樣節點上不同,致使一致性的條件被破壞。

於是,對於知足分區容錯性的系統而言,強一致性和可用性的要求難以同時被知足。其實這是很容易理解的,即便沒有網絡分區,由於不一樣節點上的數據須要通過網絡通訊來保持一致性,這個過程自己就比較花時間,當須要在給定很短的時限內基於客戶端響應時,對於一致性的保證天然就比較弱。

3.2 如何正確理解CAP定理

  • 對於分佈式系統而言CAP三要素不可兼得,但並不意味着在任什麼時候刻都必須從中作出取捨,或者在構建分佈式系統之初就選擇其中兩個而放棄另外一個,這種見解具備片面性。
  • 因爲網絡分區出現的可能性很是小,系統在正常運行的狀況下仍是應該兼顧AC二者,在進入網絡分區模式後才須要對P進行保證,從A和C中選擇犧牲一個。
  • A和C並非一個硬幣的兩面,只能選擇其中一個;A和C應該當作天平,系統能夠選擇向哪邊傾斜,但另外一邊也應該必定程度的保留。
  • 對於A和C之間的選擇,不該該粗粒度的整個系統級別進行選取,而應該針對系統中的不一樣子系統,針對性的採起不一樣的取捨策略。

4. 一致性的妥協——最終一致性和Base原則

由CAP定理可知,在分佈式系統中過於追求數據的強一致性將致使可用性必定程度被犧牲,這意味着系統將不能很好的響應用戶的請求,這會必定程度影響用戶體驗。於是對於大部分佈式系統而言,應當在保證系統高可用的前提下去追求數據的一致性,BASE原則正是對這一思想的描述。

  • BA(Basically Available)
  • 基本可用:系統在絕大部分時間應處於可用狀態,容許出現故障損失部分可用性,但保證核心可用。
  • S(Soft State)
  • 軟狀態:數據狀態不要求在任什麼時候刻都保持一致,容許存在中間狀態,而該狀態不影響系統可用性。對於多副本的存儲系統而言,就是容許副本之間的同步存在延時,而且在這個過程當中系統依舊能夠響應客戶端請求。
  • E(Eventual Consistency)
  • 最終一致性:儘管軟狀態不要求分佈式數據在任什麼時候刻都保持一致,但通過必定時間後,這些數據最終能達到一致性狀態。

BASE理論的核心思想是:把分佈式系統的可用性放在首位,放棄CAP中對數據強一致性的追求,只要系統能保證數據最終一致。

4.1 CAP,BASE以及ACID的關係

CAP描述了對於一個分佈式系統而言重要的三要素:數據一致性,可用性,分區容錯性之間的制約關係,當你選擇了其中的兩個時,就不得不對剩下的一個作必定程度的犧牲。BASE和ACID均可以看作是對CAP三要素進行取捨後的某種特殊狀況

  • BASE強調可用性和分區容錯性,放棄強一致性,這是大部分分佈式系統的選擇,好比NoSQL系統,微服務架構下的分佈式系統
  • ACID是單機數據的事務特性,由於不是分佈式系統無需考慮分區容錯,故而是選擇了可用性和強一致性後的結果。
  • 它們之間的關係以下所示

 

  •  

5. 分佈式系統的冪等性

冪等的概念來自於抽象代數,好比對於一元函數來講,知足如下條件

 

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

便可稱爲知足冪等性。在計算機科學中,一個操做若是屢次執行產生的影響與一次執行的影響相同,這樣的操做即符合冪等性。在分佈式系統中,服務消費方調用服務提供方的接口,屢次調用的結果應該與一次調用的結果同樣,這正是分佈式環境下冪等性的語義。爲何冪等性對分佈式系統而言如此重要?由於在分佈式環境下,服務的調用通常採用http協議或者rpc的方式,即雙方須要經過網絡進行通訊,而由於網絡故障或者消息超時的存在,可能服務消費方已經成功調用了服務提供方的服務接口,可是消費方並無收到來自對方的成功響應,致使消費方覺得服務調用失敗從而再次進行調用,也就是說網絡的不可靠性致使了服務接口被屢次調用的可能。分佈式系統必須保證在這種狀況下,即便接口被屢次調用,它對系統產生的影響應該與該接口只被調用一次的結果同樣。

6.微服務架構的分佈式一致性和冪等性問題

6.1 微服務架構下的分佈式一致性問題

微服務架構下,處理一個業務請求可能須要調用多個微服務進行處理,之前面的下單並支付場景爲例,完成該業務請求須要前後調用訂單微服務的下單接口和支付微服務的支付接口,只有這兩個接口都調用成功,該業務操做纔算執行成功。那麼微服務架構中是如何保證同屬於一個業務單元的多個操做的原子性以及保證分佈式數據一致性的?——答案是分佈式事務。

 
分佈式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不一樣的分佈式系統的不一樣節點之上

而且根據遵循的一致性原則不一樣,能夠分爲剛性分佈式事務和柔性分佈式事務兩大類。

  • 遵循ACID原則的剛性事務
  • 剛性事務追求數據的強一致性,好比基於兩階段提交和三階段提交的分佈式事務就屬於剛性事務,經過分佈式事務,客戶端能夠看到描述業務執行狀態的多個數據的一致性視圖,好比下單並支付這個業務操做,客戶端要麼可以同時查詢到下單和支付成功的信息,要麼可以同時查詢到下單和支付失敗的信息,其餘不一致的狀況對於客戶端而言都是不可見的。好比下單成功,支付還在處理;下單成功,支付失敗,下單記錄正在回滾。也就是說,當訂單數據和支付數據不一致時,對於客戶端的訪問請求應該予以拒絕。

 

  •  

這固然致使了系統可用性的下降,加上剛性事務實現時會致使同步阻塞的問題,鎖定資源等問題,會極大的影響系統的吞吐量和設計彈性,因此實際上微服務架構不太會採用剛性事務。

  • 遵循BASE原則的柔性事務
  • 柔性事務只對數據的最終一致性進行保證,容許系統存在必定時間的數據不一致,好比訂單記錄已經被更新可是支付記錄還沒落地時,又好比訂單記錄更新成功可是支付失敗訂單記錄回滾的過程。

 

  •  

在這個不一致窗口內,系統容許客戶端對不一致的數據進行訪問,於是系統的可用性相比而言會更好,加上其擴展性良好以及吞吐量的優點,通常微服務架構下都會採用柔性事務。柔性事務有多種不一樣的實現方式,好比基於可靠事件的模式,基於補償的模式,基於Sagas長事務的模式等,具體的實現原理以及優缺點對比就放到下一篇在詳解解釋。

6.2 微服務架構下的冪等性問題

6.2.1 冪等性場景

在微服務架構下,不一樣微服務間會有大量的基於http,rpc或者mq消息的網絡通訊,接口的重複調用以及消息的重複消費可能會常常發生,好比如下這些狀況

  • 調用訂單建立接口,第一次調用超時,調用方又嘗試了一次,但其實第一次調用已經成功,只是調用方沒有及時收到響應。
  • 訂單支付完成後,須要向MQ發送一條消息,但該消息重複發送了兩條。
  • 網絡波動致使服務提供方的接口被調用了兩次。
  • 用戶在使用產品時,無心地觸發多筆交易。
  • 某些未關閉的重試機制。

微服務架構應該具備冪等性,當接口被重複調用時,消息被重複消費時,對系統的產生的影響應該和接口被調用一次,消息被消費一次時同樣。

6.2.2 CRUD操做的冪等性分析

  • 新增請求:不具有冪等性
  • 查詢請求:重複查詢不會影響系統狀態,查詢自然具有冪等性
  • 基於主鍵的更新請求
  • 要更新的值依賴於前值,不具有冪等性。好比update goods set number=number-1 where id=1
  • 要更新的值不依賴於前值,具有冪等新。好比update goods set number=newNumber where id=1
  • 刪除請求
  • 基於主鍵的物理刪除(delete)刪除具有冪等性
  • 基於主鍵的邏輯刪除(update)也具備冪等性

總結:一般只須要對新增請求和更新請求做冪等性保證。

6.2.3 如何解決冪等性問題

  • 全局惟一ID
  • 根據業務生成一個全局惟一ID,在調用接口時會傳入該ID,接口提供方會從相應的存儲系統好比Redis中去檢索這個全局ID是否存在,若是存在則說明該操做已經執行過了,將拒絕本次服務請求;不然將相應該服務請求並將全局ID存入存儲系統中,以後包含相同業務ID參數的請求將被拒絕。
  • 去重表
  • 這種方法適用於在業務中有惟一標識的插入場景。好比在支付場景中,一個訂單隻會支付一次,能夠創建一張去重表,將訂單ID做爲惟一索引。把支付而且寫入支付單據到去重表放入一個事務中,這樣當出現重複支付時,數據庫就會拋出惟一約束異常,操做就會回滾。這樣保證了訂單隻會被支付一次。
  • 多版本併發控制
  • 適合對更新請求做冪等性控制,好比要更新商品的名字,這是就能夠在更新的接口中增長一個版本號來作冪等性控制
 
boolean updateGoodsName(int id,String newName,int version);

數據庫更新的SQL語句以下

 
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
  • 狀態機控制
  • 適合在有狀態機流轉的狀況下,好比訂單的建立和付款,訂單的建立確定是在付款以前。這是能夠添加一個int類型的字段來表示訂單狀態,建立爲0,付款成功爲100,付款失敗爲99,則對訂單狀態的更新就能夠這樣表示
 
update order set status=#{status} where id=#{id} and status<#{status}
  • 插入或更新
  • 在MySQL數據庫中,若是在insert語句後面帶上ON DUPLICATE KEY UPDATE 子句,而要插入的行與表中現有記錄的唯一索引或主鍵中產生重複值,則對舊行進行更新;不然執行新紀錄的插入。
  • 咱們能夠利用該特性防止記錄的重複插入,好比good_id和category_id構成惟一索引,則重複執行屢次該SQL,數據庫中也只會有一條記錄。
 
insert into goods_category (goods_id,category_id,create_time,update_time) values(#{goodsId},#{categoryId},now(),now()) on DUPLICATE KEY UPDATE update_time=now()

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

相關文章
相關標籤/搜索