淺析數據一致性


什么是數據一致性?

  在數據有多分副本的情況下,如果網絡、服務器或者軟件出現故障,會導致部分副本寫入成功,部分副本寫入失敗。這就造成各個副本之間的數據不一致,數據內容沖突。 實踐中,導致數據不一致的情況有很多種,表現樣式也多種多樣,比如數據更新返回操作失敗,事實上數據在存儲服務器已經更新成功。


CAP定理

  CAP定理是2000年,由 Eric Brewer 提出來的。Brewer認為在分布式的環境下設計和部署系統時,有3個核心的需求,以一種特殊的關系存在。這里的分布式系統說的是在物理上分布的系統,比如我們常見的web系統。 
  這3個核心的需求是:Consistency,Availability和Partition Tolerance,賦予了該理論另外一個名字 - CAP。 
  Consistency:一致性,這個和數據庫ACID的一致性類似,但這里關注的所有數據節點上的數據一致性和正確性,而數據庫的ACID關注的是在在一個事務內,對數據的一些約束。系統在執行過某項操作后仍然處於一致的狀態。在分布式系統中,更新操作執行成功后所有的用戶都應該讀取到最新值。 
  Availability:可用性,每一個操作總是能夠在一定時間內返回結果。需要注意“一定時間”和“返回結果”。“一定時間”是指,系統結果必須在給定時間內返回。“返回結果”是指系統返回操作成功或失敗的結果。 
  Partition Tolerance:分區容忍性,是否可以對數據進行分區。這是考慮到性能和可伸縮性。 
  CAP定理認為,一個提供數據服務的存儲系統無法同事滿足數據一致性、數據可用性、分區容忍性。 
  為什么不能完全保證這個三點了,個人覺得主要是因為一旦進行分區了,就說明了必須節點之間必須進行通信,涉及到通信,就無法確保在有限的時間內完成指定的行文,如果要求兩個操作之間要完整的進行,因為涉及到通信,肯定存在某一個時刻只完成一部分的業務操作,在通信完成的這一段時間內,數據就是不一致性的。如果要求保證一致性,那么就必須在通信完成這一段時間內保護數據,使得任何訪問這些數據的操作不可用。 
  如果想保證一致性和可用性,那么數據就不能夠分區。一個簡單的理解就是所有的數據就必須存放在一個數據庫里面,不能進行數據庫拆分。這個對於大數據量,高並發的互聯網應用來說,是不可接受的。 
  在大型網站應用中,數據規模總是快速擴張的,因此可伸縮性即分區容忍性必不可少,規模變大以后,機器數量也會變得龐大,這是網絡和服務器故障會頻繁出現,要想保證應用可用,就必須保證分布式處理系統的高可用性。所以在大型網站中,通常會選擇強化分布式存儲系統的可用性(A)和伸縮性(P),在某種程度上放棄一致性(C)。一般來說,數據不一致通常出現在系統高並發寫操作或者集群狀態不穩(故障恢復、集群擴容等)的情況下,應用系統需要對分布式數據處理系統的數據不一致性有所了解並進行某種意義上的補償和糾錯,以避免出現應用系統數據不正確。


數據一致性模型

  一些分布式系統通過復制數據來提高系統的可靠性和容錯性,並且將數據的不同的副本存放在不同的機器,由於維護數據副本的一致性代價高,因此許多系統采用弱一致性來提高性能,一些不同的一致性模型也相繼被提出。

  1. 強一致性: 要求無論更新操作實在哪一個副本執行,之后所有的讀操作都要能獲得最新的數據。
  2. 弱一致性:用戶讀到某一操作對系統特定數據的更新需要一段時間,我們稱這段時間為“不一致性窗口”。
  3. 最終一致性:是弱一致性的一種特例,保證用戶最終能夠讀取到某操作對系統特定數據的更新。

數據一致性實現技術

Quorum系統NRW策略

  這個協議有三個關鍵字N、R、W。

  • N代表數據所具有的副本數。
  • R表示完成讀操作所需要讀取的最小副本數,即一次讀操作所需要參與的最小節點數目。
  • W表示完成寫操作所需要寫入的最小副本數,即一次寫操作所需要參與的最小節點數目。

  該策略中,只需要保證R+W>N,就可以保證強一致性。 
  例如:N=3,W=2,R=2,那么表示系統中數據有3個不同的副本,當進行寫操作時,需要等待至少有2個副本完成了該寫操作系統才會返回執行成功的狀態,對於讀操作,系統有同樣的特性。由於R + W > N,因此該系統是可以保證強一致性的。 
  R + W> N會產生類似Quorum的效果。該模型中的讀(寫)延遲由最慢的R(W)副本決定,有時為了獲得較高的性能和較小的延遲,R和W的和可能小於N,這時系統不能保證讀操作能獲取最新的數據。 
  如果R + W > N,那么分布式系統就會提供強一致性的保證,因為讀取數據的節點和被同步寫入的節點是有重疊的。在關系型數據管理系統中,如果N=2,可以設置為W=2,R=1,這是比較強的一致性約束,寫操作的性能比較低,因為系統需要2個節點上的數據都完成更新后才將確認結果返回給用戶。 
  如果R + W ≤ N,這時讀取和寫入操作是不重疊的,系統只能保證最終一致性,而副本達到一致的時間則依賴於系統異步更新的實現方式,不一致性的時間段也就等於從更新開始到所有的節點都異步完成更新之間的時間。 
R和W的設置直接影響系統的性能、擴展性與一致性。如果W設置為1,則一個副本完成更改就可以返回給用戶,然后通過異步的機制更新剩余的N-W的副本;如果R設置為1,只要有一個副本被讀取就可以完成讀操作,R和W的值如較小會影響一致性,較大則會影響性能,因此對這兩個值的設置需要權衡。

下面為不同設置的幾種特殊情況: 
1. 當W=1,R=N時,系統對寫操作有較高的要求,但讀操作會比較慢,若N個節點中有節點發生故障,那么讀操作將不能完成。 
2. 當R=1,W=N時,系統對讀操作有較高性能、高可用,但寫操作性能較低,用於需要大量讀操作的系統,若N個節點中有節點發生故障,那么些操作將不能完成。 
3. 當R=Q,W=Q(Q=N/2+1)時,系統在讀寫性能之間取得平衡,兼顧了性能和可用性。

兩階段提交算法

  在兩階段提交協議中,系統一般包含兩類機器(或節點):一類為協調者(coordinator),通常一個系統中只有一個;另一類為事務參與者(participants,cohorts或workers),一般包含多個,在數據存儲系統中可以理解為數據副本的個數。兩階段提交協議由兩個階段組成,在正常的執行下,這兩個階段的執行過程如下所述:

  • 階段1:請求階段(commit-request phase,或稱表決階段,voting phase)。 
    在請求階段,協調者將通知事務參與者准備提交或取消事務,然后進入表決過程。在表決過程中,參與者將告知協調者自己的決策:同意(事務參與者本地作業執行成功)或取消(本地作業執行故障)。
  • 階段2:提交階段(commit phase)。 
    在該階段,協調者將基於第一個階段的投票結果進行決策:提交或取消。當且僅當所有的參與者同意提交事務協調者才通知所有的參與者提交事務,否則協調者將通知所有的參與者取消事務。參與者在接收到協調者發來的消息后將執行響應的操作。

  舉個例子:A組織B、C和D三個人去爬長城:如果所有人都同意去爬長城,那么活動將舉行;如果有一人不同意去爬長城,那么活動將取消。用2PC算法解決該問題的過程如下:

  1. 首先A將成為該活動的協調者,B、C和D將成為該活動的參與者。
  2. 階段1:A發郵件給B、C和D,提出下周三去爬山,問是否同意。那么此時A需要等待B、C和D的郵件。B、C和D分別查看自己的日程安排表。B、C發現自己在當日沒有活動安排,則發郵件告訴A它們同意下周三去爬長城。由於某種原因,D白天沒有查看郵件。那么此時A、B和C均需要等待。到晚上的時候,D發現了A的郵件,然后查看日程安排,發現周三當天已經有別的安排,那么D回復A說活動取消吧。
  3. 階段2:此時A收到了所有活動參與者的郵件,並且A發現D下周三不能去爬山。那么A將發郵件通知B、C和D,下周三爬長城活動取消。此時B、C回復A“太可惜了”,D回復A“不好意思”。至此該事務終止。

  兩階段提交算法在分布式系統結合,可實現單用戶對文件(對象)多個副本的修改,多副本數據的同步。其結合的原理如下:

  1. 客戶端(協調者)向所有的數據副本的存儲主機(參與者)發送:修改具體的文件名、偏移量、數據和長度信息,請求修改數據,該消息是1階段的請求消息。
  2. 存儲主機接收到請求后,備份修改前的數據以備回滾,修改文件數據后,向客戶端回應修改成功的消息。如果存儲主機由於某些原因(磁盤損壞、空間不足等)不能修改數據,回應修改失敗的消息。
  3. 客戶端接收發送出去的每一個消息回應,如果存儲主機全部回應都修改成功,向每存儲主機發送確認修改的提交消息;如果存在存儲主機回應修改失敗,或者超時未回應,客戶端向所有存儲主機發送取消修改的提交消息。該消息是2階段的提交消息。
  4. 存儲主機接收到客戶端的提交消息,如果是確認修改,則直接回應該提交OK消息;如果是取消修改,則將修改數據還原為修改前,然后回應取消修改OK的消息。
  5. 客戶端接收全部存儲主機的回應,整個操作成功。

  在該過程中可能存在通信失敗,例如網絡中斷、主機宕機等諸多的原因,對於未在算法中定義的其它異常,都認為是提交失敗,都需要回滾,這是該算法基於確定的通信回復實現的,在參與者的確定回復(無論是回復失敗還是回復成功)之上執行邏輯處理,符合確定性的條件當然能夠獲得確定性的結果哲學原理。 
  缺點:單個A是個嚴重問題:沒有熱備機制,A節點宕機了或者鏈接它的網絡壞了會阻塞該事務;吞吐量不行,沒有充分發動更多A的力量,一旦某個A第一階段投了贊成票就得在它上面加獨占鎖,其他事務不得接入,直到當前事務提交or回滾。

分布式鎖服務

  分布式鎖是對數據被外界修改持保守態度,在整個數據處理過程中將數據處於鎖定狀態,在用戶修改數據的同時,其它用戶不允許修改。 
  采用分布式鎖服務實現數據一致性,是在操作目標之前先獲取操作許可,然后再執行操作,如果其他用戶同時嘗試操作該目標將被阻止,直到前一個用戶釋放許可后,其他用戶才能夠操作目標。分析這個過程,如果只有一個用戶操作目標,沒有多個用戶並發沖突,也申請了操作許可,造成了由於申請操作許可所帶來的資源使用消耗,浪費網絡通信和增加了延時。 
  采用分布式鎖實現多副本內容修改的一致性問題, 選擇控制內容顆粒度實現申請鎖服務。例如我們要保證一個文件的多個副本修改一致, 可以對整個文件修改設置一把鎖,修改時申請鎖,修改這個文件的多個副本,確保多個副本修改的一致,修改完成后釋放鎖;也可以對文件分段,或者是文件中的單個字節設置鎖, 實現更細顆粒度的鎖操作,減少沖突。 
  常用的鎖實現算法有Lamport bakery algorithm (俗稱面包店算法), 還有Paxos算法以及樂觀鎖。下面對其原理做簡單概述。

1. Lamport面包店算法

  是解決多個線程並發訪問一個共享的單用戶資源的互斥問題的算法。 由Leslie Lamport(英語:Leslie Lamport)發明。 
  這個算法也可以稱為時間戳策略,或者叫做Lamport邏輯時鍾。 
  這里先陳述一下這個邏輯時鍾的內容: 
  我們用分布式系統中的事件的先后關系,用“->”符號來表示,例如:若事件a發生在事件b之前,那么a->b. 
  該關系需要滿足下列三個條件:

  1. 如果a和b是同一進程中的事件,a在b之前發生,則a->b
  2. 如果事件a是消息發送方,b是接收方,則a->b
  3. 對於事件a、b、c,如果有a->b,b->c,則有a->c

  注意,對於任何一個事件a,a -> a都是不成立的,也就是說,關系->是反自反的。有了上面的定義,我們也可以定義出“並發”(concurrent)的概念了:

對於事件a、b,如果a -> b,b -> a兩個都不成立,那么a和b就是並發的。

  直觀上,上面的->關系非常好理解,即“xxx在xxx之前發生”。也就是說,一個系統在輸入I1下,如果有a->b,那么對於這個系統的同一個輸入I1,無論重復運行多少次,a也始終發生在b之前;如果在輸入I1下a和b是並發的,則表示在同一個輸入I1下的不同運行中,a可能在b之前,也可能在b之后,也可能恰好同時發生。也就是,並發並不是指一定同時發生,而是表示一種不確定性。->和並發的概念,就是我們理解一個系統時最基礎的概念之一了。 
  有了上面的概念,我們可以給系統引入時鍾了。這里的時鍾就是lamport邏輯時鍾。一個時鍾,本質上是一個事件到實數(假設時間是連續的)的函數。這個函數將每個事件映射到一個數字,代表這個事件發生的時間。形式一點來說,對於每個進程Pi,都有一個時鍾Ci,這個時鍾將該進程中的事件a映射到Ci(a)。而整個系統的時鍾C=< C0, C1, …, Cn>,對於一個事件b,假設b屬於進程Pj,那么C(b) =Cj(b)。

  這里插一句,從這個定義也可以看到大師對分布式系統的理解。分布式系統中不存在一個“全局”的實體。在該系統中,每個進程都是一個相對獨立的實體,它們有自己的本地信息(本地Knowledge)。而整個系統的信息則是各個進程的信息的一個聚合。 
  有了時鍾的一個“本質定義”還不夠,我們需要考慮,什么樣的時鍾是一個有意義的,或者說正確的時鍾。其實,有了前文的->關系的定義,正確的時鍾應滿足的條件已經十分明顯了: 
  時鍾條件:對於任意兩個事件a,b,如果a -> b,那么C(a) < C(b)。 
  注意,反過來講這個條件可不成立。如果我們要求反過來也成立,即“如果a -> b為假,那么C(a) < C(b)也為假”,那就等於要求並發事件必須同時發生,這顯然是不合理的。 
  結合前文->關系的定義,我們可以把上面的條件細化成如下兩條:

  1. 如果a和b是進程Pi中的兩個事件,並且在Pi中,a在b之前發生,那么Ci(a) < Ci(b);
  2. 如果a是Pi發送消息m,b是Pj接收消息m,那么Ci(a) < Cj(b);

  上面就定義了合理的邏輯時鍾。顯然,一個系統可以有無數個合理的邏輯時鍾。實現邏輯時鍾也相對簡單,只要遵守兩條實現規則就可以了:

  1. 每個進程Pi在自己的任何兩個連續的事件之間增加Ci值;
  2. 如果事件a是Pi發送消息m,那么在m中應該帶上時間戳Tm=Ci(a);如果b是進程Pj接收到消息m,那么,進程Pj應該設置Cj為大於max(Tm,Cj(b))。

  有了上面邏輯時鍾的定義,我們現在可以為一個系統中所有的事件排一個全序,就是使用事件發生時的邏輯時鍾讀數進行排序,讀數小的在先。當然,此時可能會存在兩個事件同時發生的情況。如果要去除這種情況,方法也非常簡單:如果a在進程Pi中,b在進程Pj中,Ci(a) = Cj(b)且i < j,那么a在b之前。形式化一點,我們可以把系統事件E上的全序關系“=>”定義為: 
  假設a是Pi中的事件,b是Pj中的事件,那么:a => b當且僅當以下兩個條件之一成立:

  1. Ci(a) < Cj(b);
  2. Ci(a) = Cj(b) 且 i < j;

  Lamport把上面這些數理邏輯時鍾的概念以非常直觀地類比為顧客去面包店采購。面包店只能接待一位顧客的采購。已知有n位顧客要進入面包店采購,安排他們按照次序在前台登記一個簽到號碼。該簽到號碼逐次加1。根據簽到號碼的由小到大的順序依次入店購貨。完成購買的顧客在前台把其簽到號碼歸0. 如果完成購買的顧客要再次進店購買,就必須重新排隊。 
  這個類比中的顧客就相當於線程,而入店購貨就是進入臨界區獨占訪問該共享資源。由於計算機實現的特點,存在兩個線程獲得相同的簽到號碼的情況,這是因為兩個線程幾乎同時申請排隊的簽到號碼,讀取已經發出去的簽到號碼情況,這兩個線程讀到的數據是完全一樣的,然后各自在讀到的數據上找到最大值,再加1作為自己的排隊簽到號碼。為此,該算法規定如果兩個線程的排隊簽到號碼相等,則線程id號較小的具有優先權。 
  把該算法原理與分布式系統相結合,即可實現分步鎖。 
  注意這個系統中需要引入時鍾同步,博主的意見是可以采用SNTP實現時鍾同步(非權威,僅供參考)。

2.Paxos算法

  該算法比較熱門,類似2pc算法的升級版,在此不做贅述,可以自行搜索相關資料。(博主會在之后整理列出) 
  需要注意的是這個算法也是Leslie Lamport提出的,由此可見這位大師之牛逼! 
  Paxos算法解決的問題是一個分布式系統如何就某個值(決議)達成一致。一個典型的場景是,在一個分布式數據庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那么他們最后能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個“一致性算法”以保證每個節點看到的指令一致。一個通用的一致性算法可以應用在許多場景中,是分布式計算中的重要問題。節點通信存在兩種模型:共享內存(Shared memory)和消息傳遞(Messages passing)。Paxos算法就是一種基於消息傳遞模型的一致性算法。BigTable使用一個分布式數據鎖服務Chubby,而Chubby使用Paxos算法來保證備份的一致性。 
  不僅只用在分布式系統,凡是多個過程需要達成某種一致性的都可以用到Paxos 算法。一致性方法可以通過共享內存(需要鎖)或者消息傳遞實現,Paxos 算法采用的是后者。下面是Paxos 算法適用的幾種情況:一台機器中多個進程/線程達成數據一致;分布式文件系統或者分布式數據庫中多客戶端並發讀寫數據;分布式存儲中多個副本響應讀寫請求的一致性。

3. 采用樂觀鎖原理實現的同步

  我們舉個例子說明該算法的實現原理。如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶余額),如果采用前面的分布式鎖服務機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個並發,這樣的情況將導致怎樣的后果。 
  樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version)記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。 
  對於上面修改用戶帳戶信息的例子而言,假設數據庫中帳戶信息表中有一個 version 字段,當前值為 1 ;而當前帳戶余額字段( balance )為 $100 。

  1. 操作員 A 此時將其讀出(version=1 ),並從其帳戶余額中扣除 50100-$50 )。
  2. 在操作員 A 操作的過程中,操作員B也讀入此用戶信息( version=1 ),並從其帳戶余額中扣除 20100-$20 )。
  3. 操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣除后余額( balance=$50),提交至數據庫更新,此時由於提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新為 2 。
  4. 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=$80),但此時比對數據庫記錄版本時發現,操作員 B 提交的數據版本號為 2 ,數據庫記錄當前版本也為 2 ,不滿足 “提交版本必須大於記錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。這樣,就避免了操作員 B 用基於version=1 的舊數據修改的結果覆蓋操作員A 的操作結果的可能。

 

原文鏈接:http://blog.csdn.net/u013256816/article/details/50698167


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM