多版本併發控制(MVCC)在分佈式系統中的應用

from: http://coolshell.cn/articles/6790.html

from: http://baike.baidu.com/view/1887040.htm


問題

最近項目中遇到了一個分佈式系統的併發控制問題。該問題可以抽象爲:某分佈式系統由一個數據中心D和若干業務處理中心L1,L2 … Ln組成;D本質上是一個key-value存儲,它對外提供基於HTTP協議的CRUD操作接口。L的業務邏輯可以抽象爲下面3個步驟:

  1. read: 根據keySet {k1, … kn}從D獲取keyValueSet {k1:v1, … kn:vn}
  2. do: 根據keyValueSet進行業務處理,得到需要更新的數據集keyValueSet’ {k1′:v1′, … km’:vm’} (:讀取的keySet和更新的keySet’可能不同)
  3. update: 把keyValueSet’更新到D (:D保證在一次調用更新多個key的原子性)

在沒有事務支持的情況下,多個L進行併發處理可能會導致數據一致性問題。比如,考慮L1和L2的如下執行順序:

  1. L1從D讀取key:123對應的值100
  2. L2從D讀取key:123對應的100
  3. L1將key:123更新爲100 + 1
  4. L2將key:123更新爲100 + 2

如果L1和L2串行執行,key:123對應的值將爲103,但上面併發執行中L1的執行效果完全被L2所覆蓋,實際key:123所對應的值變成了102。

解決方案1:基於鎖的事務

爲了讓L的處理具有可串行化特性(Serializability),一種最直接的解決方案就是考慮爲D加上基於鎖的簡單事務。讓L在進行業務處理前先鎖定D,完成以後釋放鎖。另外,爲了防止持有鎖的L由於某種原因長時間未提交事務,D還需要具有超時機制,當L嘗試提交一個已超時的事務時會得到一個錯誤響應。

本方案的優點是實現簡單,缺點是鎖定了整個數據集,粒度太大;時間上包含了L的整個處理時間,跨度太長。雖然我們可以考慮把鎖定粒度降低到數據項級別,按key進行鎖定,但這又會帶來其他的問題。由於更新的keySet’可能是事先不確定的,所以可能無法在開始事務時鎖定所有的key;如果分階段來鎖定需要的key,又可能出現死鎖(Deadlock)問題。另外,按key鎖定在有鎖爭用的情況下並不能解決鎖定時間太長的問題。所以,按key鎖定仍然存在重要的不足之處。

解決方案2:多版本併發控制

爲了實現可串行化,同時避免鎖機制存在的各種問題,我們可以採用基於多版本併發控制(Multiversion concurrency control,MVCC)思想的無鎖事務機制。人們一般把基於鎖的併發控制機制稱成爲悲觀機制,而把MVCC機制稱爲樂觀機制。這是因爲鎖機制是一種預防性的,讀會阻塞寫,寫也會阻塞讀,當鎖定粒度較大,時間較長時併發性能就不會太好;而MVCC是一種後驗性的,讀不阻塞寫,寫也不阻塞讀,等到提交的時候才檢驗是否有衝突,由於沒有鎖,所以讀寫不會相互阻塞,從而大大提升了併發性能。我們可以借用源代碼版本控制來理解MVCC,每個人都可以自由地閱讀和修改本地的代碼,相互之間不會阻塞,只在提交的時候版本控制器會檢查衝突,並提示merge。目前,Oracle、PostgreSQL和MySQL都已支持基於MVCC的併發機制,但具體實現各有不同。

MVCC的一種簡單實現是基於CAS(Compare-and-swap)思想的有條件更新(Conditional Update)。普通的update參數只包含了一個keyValueSet’,Conditional Update在此基礎上加上了一組更新條件conditionSet { … data[keyx]=valuex, … },即只有在D滿足更新條件的情況下才將數據更新爲keyValueSet’;否則,返回錯誤信息。這樣,L就形成了如下圖所示的Try/Conditional Update/(Try again)的處理模式:

雖然對單個L來講不能保證每次都成功更新,但從整個系統來看,總是有任務能夠順利進行。這種方案利用Conditional Update避免了大粒度和長時間的鎖定,當各個業務之間資源爭用不大的情況下,併發性能很好。不過,由於Conditional Update需要更多的參數,如果condition中value的長度很長,那麼每次網絡傳送的數據量就會比較大,從而導致性能下降。特別是當需要更新的keyValueSet’很小,而condition很大時,就顯得非常不經濟。

爲了避免condition太大所帶來的性能問題,可以爲每條數據項增加一個int型的版本號字段,由D維護該版本號,每次數據有更新就增加版本號;L在進行Conditional Update時,通過版本號取代具體的值。

另一個問題是上面的解決方案假設了D是可以支持Conditional Update的;那麼,如果D是一個不支持Conditional Update的第三方的key-value存儲怎麼辦呢?這時,我們可以在L和D之間增加一個P作爲代理,所有的CRUD操作都必須經過P,讓P來進行條件檢查,而實際的數據操作放在D。這種方式實現了條件檢查和數據操作的分離,但同時降低了性能,需要在P中增加cache,提升性能。由於P是D的唯一客戶端;所以,P的cache管理是非常簡單的,不必像多客戶端情形擔心緩存的失效。不過,實際上,據我所知redis和Amazon SimpleDB都已經有了Conditional Update的支持。

悲觀鎖和MVCC對比

上面介紹了悲觀鎖和MVCC的基本原理,但是對於它們分別適用於什麼場合,不同的場合下兩種機制優劣具體表現在什麼地方還不是很清楚。這裏我就對一些典型的應用場景進行簡單的分析。需要注意的是下面的分析不針對分佈式,悲觀鎖和MVCC兩種機制在分佈式系統、單數據庫系統、甚至到內存變量各個層次都存在。

### 場景1:對讀的響應速度要求高

有一類系統更新特別頻繁,並且對讀的響應速度要求很高,如股票交易系統。在悲觀鎖機制下,寫會阻塞讀,那麼當有寫操作時,讀操作的響應速度就會受到影響;而MVCC不存在讀寫鎖,讀操作是不受任何阻塞的,所以讀的響應速度會更快更穩定。

### 場景2:讀遠多於寫

對於許多系統來講,讀操作的比例往往遠大於寫操作,特別是某些海量併發讀的系統。在悲觀鎖機制下,當有寫操作佔用鎖,就會有大量的讀操作被阻塞,影響併發性能;而MVCC可以保持比較高且穩定的讀併發能力。

### 場景3:寫操作衝突頻繁

如果系統中寫操作的比例很高,且衝突頻繁,這時就需要仔細評估。假設兩個有衝突的業務L1和L2,它們在單獨執行是分別耗時t1,t2。在悲觀鎖機制下,它們的總時間大約等於串行執行的時間:

T = t1 + t2

而在MVCC下,假設L1在L2之前更新,L2需要retry一次,它們的總時間大約等於L2執行兩次的時間(這裏假設L2的兩次執行耗時相等,更好的情況是,如果第1次能緩存下部分有效結果,第二次執行L2耗時是可能減小的):

T’ = 2 * t2

這時關鍵是要評估retry的代價,如果retry的代價很低,比如,對某個計數器遞增,又或者第二次執行可以比第一次快很多,這時採用MVCC機制就比較適合。反之,如果retry的代價很大,比如,報表統計運算需要算幾小時甚至一天那就應該採用鎖機制避免retry。

從上面的分析,我們可以簡單的得出這樣的結論:對讀的響應速度和併發性要求比較高的場景適合MVCC;而retry代價越大的場景越適合悲觀鎖機制。

總結

本文介紹了一種基於多版本併發控制(MVCC)思想的Conditional Update解決分佈式系統併發控制問題的方法。和基於悲觀鎖的方法相比,該方法避免了大粒度和長時間的鎖定,能更好地適應對讀的響應速度和併發性要求高的場景。




MVCC

目錄


MVCC
  Multi-Version Concurrency Control 多版本併發控制  大多數的MySQL事務型存儲引擎,如InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制。事實上,他們都和另外一種用來增加併發性的被稱爲“多版本併發控制(MVCC)”的機制來一直使用。MVCC不只使用在MySQL中,Oracle,PostgreSQL以及其他一些數據庫系統也同樣使用它。   你可將MVCC看成行級別鎖的一種妥協,它在許多情況下避免了使用鎖,同時可以提供更小的開銷。根據實現的不同,它可以允許非阻塞式讀,在寫操作進行時只鎖定必要的記錄。   MVCC會保存某個時間點上的數據快照。這意味闃事務可以看到一個一致的數據視圖,不管他們需要跑多久。這同時也意味着不同的事務在同一個時間點看到的同一個表的數據可能是不同的。如果你從來沒有過種體驗的話,可能理解起來比較抽象,但是隨着慢慢地熟悉這種理解將會很容易。   各個存儲引擎對於MVCC的實現各不相同。這些不同中的一些包括樂觀和悲觀併發控制。我們將通過一個簡化的InnoDB版本的行爲來展示MVCC工作的一個側面。   InnoDB:通過爲每一行記錄添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。但是InnoDB並不存儲這些事件發生時的實際時間,相反它只存儲這些事件發生時的系統版本號。這是一個隨着事務的創建而不斷增長的數字。每個事務在事務開始時會記錄它自己的系統版本號。每個查詢必須去檢查每行數據的版本號與事務的版本號是否相同。讓我們來看看當隔離級別是REPEATABLE READ時這種策略是如何應用到特定的操作的:   SELECT InnoDB必須每行數據來保證它符合兩個條件:   1、InnoDB必須找到一個行的版本,它至少要和事務的版本一樣老(也即它的版本號不大於事務的版本號)。這保證了不管是事務開始之前,或者事務創建時,或者修改了這行數據的時候,這行數據是存在的。   2、這行數據的刪除版本必須是未定義的或者比事務版本要大。這可以保證在事務開始之前這行數據沒有被刪除。   符合這兩個條件的行可能會被當作查詢結果而返回。   INSERT:InnoDB爲這個新行記錄當前的系統版本號。   DELETE:InnoDB將當前的系統版本號設置爲這一行的刪除ID。   UPDATE:InnoDB會寫一個這行數據的新拷貝,這個拷貝的版本爲當前的系統版本號。它同時也會將這個版本號寫到舊行的刪除版本里。   這種額外的記錄所帶來的結果就是對於大多數查詢來說根本就不需要獲得一個鎖。他們只是簡單地以最快的速度來讀取數據,確保只選擇符合條件的行。這個方案的缺點在於存儲引擎必須爲每一行存儲更多的數據,做更多的檢查工作,處理更多的善後操作。   MVCC只工作在REPEATABLE READ和READ COMMITED隔離級別下。READ UNCOMMITED不是MVCC兼容的,因爲查詢不能找到適合他們事務版本的行版本;它們每次都只能讀到最新的版本。SERIABLABLE也不與MVCC兼容,因爲讀操作會鎖定他們返回的每一行數據[1]

說明

  通過使用MVCC(Multi-Version Concurrency Control)算法自動提供併發控制。MVCC維持一個數據的多個版本使讀寫操作沒有衝突。也就是說數據元素X上的每一個寫操作產生X的一個新版本,GBase 8m爲X的每一個讀操作選擇一個版本。由於消除了數據庫中數據元素讀和寫操作的衝突,GBase 8m得到優化,具有更好的性能。特別是對於數據庫讀和寫兩種方法,他們不用等待其他同時進行的相同數據寫和讀的完成。在併發事務中,數據庫寫只等待正在對同一行數據進行更新的寫,這是現有的行鎖定方法的弱點。同時MVCC回收不需要的和長時間不用的內存,防止內存空間的浪費。MVCC優化了數據庫併發系統,使系統在有大量併發用戶時得到最高的性能,並且可以不用關閉服務器就直接進行熱備份。

比鎖定的優勢

  使用MVCC多版本併發控制比鎖定模型的主要優點是在MVCC裏, 對檢索(讀)數據的鎖要求與寫數據的鎖要求不衝突, 所以讀不會阻塞寫,而寫也從不阻塞讀。  在數據庫裏也有表和行級別的鎖定機制, 用於給那些無法輕鬆接受 MVCC 行爲的應用。 不過,恰當地使用 MVCC 總會提供比鎖更好地性能。

GBase8的特性

  在 GBase 中的查詢功能通過 MVCC 提供的一致性非鎖讀(在下文我們簡稱爲一致性讀),就是提供通過數據庫在一個時間點上的快照來實現信息的查詢。查詢只是對那些在這個時間點之前提交的事務所做的變更,而並不關注在時間點之後的變更或未提交的事務。當然,若是該事務自身進行的變更,對於查詢是可見的。  GBase 的默認級別是 READ COMMITTED ,在該隔離級別下事務中的查詢語句,使用當前時間戳進行一致性讀,每次查詢的時間戳是不相同的。  但對 REPEATABLE READ 隔離級別,在同一個事務中的所有一致性讀,使用的時間戳均是第一個查詢的時間戳,這樣讀取的也就是由該事務第一次讀建立起來的數據快照。用戶只有通過提交當前事務,併發出一個新的查詢纔會得到新的數據快照。  一致性讀是 GBase 在 READ COMMITTED 和 REPEATABLE READ 隔離級別下,處理 SELECT 語句中使用的默認模式。一致性讀在它讀的數據上不設置任何鎖,因此在一致性讀某個表的同時,其它用戶均可以修改這個表。  注意在 DROP TABLE 和 ALTER TABLE 運作時,一致性讀無效 。一致性讀在 DROP TABLE 上無效是因爲 GBase 不能使用已經 drop 的表,該表已經刪除。一致性讀在 ALTER TABLE 上無效是因爲 GBase 會在事務內,重新創建一個新表並從舊錶向新表插入記錄。這樣當用戶再次執行一致性讀時,在新表中將看不到任何行,因爲在新表中的數據都在第一次一致性讀的快照之外。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章