分佈式事務中的一致性和隔離性你真的懂了嗎

前言

在分佈式(數據庫)系統中,咱們常常會聽到一些「高大上」卻又比較「迷惑」的詞彙,好比,ACID和CAP中的"C"是不是同一含義、Snapshot Isolation(SI)和Serializable Snapshot Isolation(SSI)區別是什麼、Serializable和Linearizable是一個意思嗎、Consistency和Consensus呢?若是你沒法清晰的回答這些問題,那麼但願本文會對你有所幫助。算法

ACID與CAP的Consistency

ACID和CAP中的C是都是Consistency的縮寫,可是他們的含義倒是大相徑庭的。ACID包含Atomicity(原子性)、Consistency(一致性)、Durability(持久性)、Isolation(隔離性)四個方面,其中Atomicity(原子性)、Durability(持久性)和 Isolation(隔離性)都是存儲引擎提供的能力保障,可是Consistency(一致性)卻不是存儲引擎提供的,相反,它是業務層面的一種邏輯約束,以轉帳這個最爲經典的例子而言,A有100元RMB,B有0元RMB,如今A要轉給B 50元RMB,那麼轉帳先後,A和B的總錢數必須仍是100元RMB,顯然,這只是業務層規定的邏輯約束而已。在《DDIA》一書中,對ACID也有着以下的描述:數據庫

CAP是Consistency(一致性)、Availability(可用性)和Partition tolerance(分區容錯性)的簡稱,這裏的Consistency(一致性)其實更符合咱們對一致性更直觀的理解,它表示一種對「數據新鮮度」的保證,即對「什麼時候」能讀到「正確」的數據的保證。這裏的Consistency就表明Linearizability Consistency(線性一致性),另外,咱們常見一致性模型還有Sequential Consistency(順序一致性)和Casual Consistency(因果一致性)等,這些在後文中會着重介紹。《DDIA》一書對CAP也有一些介紹,在此貼一下做爲輔助說明:

Consistency和Consensus

Consistency和Consensus比較迷惑的不是它們的含義,更多的是他們看上去很「類似」,其實,只要把它們翻譯成中文,也就好理解了。Consistency就是「一致性」的意思了(前文已經屢次提到),而Consensus是「共識」的意思。工程中,Consistency表示你「什麼時候」能讀到「正確」的數據,常見的有Linearizability Consistency(線性一致性)、Sequential Consistency(順序一致性)和Casual Consistency(因果一致性)等。而Consensus多指一種「共識」算法,即多方參與共同決定一件事情,好比Basic Paxos算法就能夠用來在一羣分佈式節點中決定一個值(諸如選主操做)。固然Consistency和Consensus也不是徹底沒有關係的,不少Consistency模型中也會用到Consensus算法來完成。編程

Serializable和Linearizable

Serializable和Linearizable能夠說是最容易混淆的兩個概念,Serializable字面翻譯爲「序列化」,Linearizable字面翻譯爲「線性化」,乍一看差很少,但事實上它們是徹底不一樣的兩個方面。爲了直觀的看出它們的關係,引用一張出自jepsen官網的圖:多線程

從上圖能夠看到,Serializable是一種事務隔離級別(併發的事務之間),讓全部的事務從自身角度看上去,好像全部的事務都以某種次序順序執行同樣(不必定知足全局時間順序)。而Linearizable,前文已經屢次提到,它是一種一致性模型(線性一致性),表示對某個對象執行寫以後能夠馬上讀到這個最新值(讀寫順序與全局時間一致)。所以能夠看到,Serializable和Linearizable是徹底不相關的概念。《DDIA》一書中對這兩個概念也有較爲直白的對比。

所以, 所謂的Strict Serializable(強一致)模型,就是同時知足Serializable和Linearizable,即全部事務按串行化隔離級別執行(注:不必定是真的串行化執行,而是最終效果和某種串行執行效果相同)、並能馬上讀出「正確」(注:正確表示能馬上讀到一個對象最近寫入的數據)數據的一致性模型,而Serializable Snapshot Isolation(SSI)只知足了Serializable卻不具有Linearizable的語義,即它只知足了事務按某種順序串行執行,卻沒法知足能馬上讀到「正確」的數據。既然Linearizable表示一種Consistency模型,Serializable表示事務的一種Isolation級別,那麼下文就分別從「一致性」和「隔離性」兩個方面分別進行闡述。

一致性

Consistency並非分佈式數據庫中新增的概念,相反,Consistency存在於計算機中的各個角落。好比,多核CPU的Cache之間存在Consistency問題,併發編程中多線程之間也存在內存Consistency問題。本文就將以常見的多線程編程爲例,着重介紹Linearizability Consistency(線性一致性)、Sequential Consistency(順序一致性)和Casual Consistency(因果一致性)之間的概念和區別,不用擔憂,這些概念和分佈式中的Consistency模型是徹底一致的。閉包

Linearizability Consistency(線性一致性)

Linearizability Consistency(線性一致性)的要求兩個:併發

  • 任何一次讀都能讀到某個數據的最近一次寫的數據。dom

  • 系統中的全部進程,看到的操做順序,都和全局時鐘下的順序一致。分佈式

顯然這兩個條件都對全局時鐘有很是高的要求。比它要求更弱一些的,就是Sequential Consistency(順序一致性)。性能

Sequential Consistency(順序一致性)

Sequential Consistency(順序一致性),也一樣有兩個條件,其一與前面Linearizability Consistency(線性一致性)的要求同樣,也是能夠立刻讀到最近寫入的數據,然而它的第二個條件就弱化了不少,它容許系統中的全部進程造成本身合理的統一的一致性,不須要與全局時鐘下的順序都一致。這裏的第二個條件的要點在於:操作系統

  • 系統的全部進程的順序一致,並且是合理的,就是說任何一個進程中,這個進程對同一個變量的讀寫順序要保持,而後你們造成一致。

  • 不須要與全局時鐘下的順序一致。

可見, Sequential Consistency(順序一致性)在順序要求上並無那麼嚴格,它只要求系統中的全部進程達成本身認爲的一致就能夠了,即錯的話一塊兒錯,對的話一塊兒對,同時不違反程序的順序便可,並不須要個全局順序保持一致。

引用《分佈式計算-原理、算法與系統》一張圖進一步說明Linearizability Consistency(線性一致性)和Sequential Consistency(順序一致性)之間的區別:

  • 圖a是知足Sequential Consistency(順序一致性),可是不知足Linearizability Consistency(線性一致性)。緣由在於,從全局時鐘的觀點來看,P2進程對變量X的讀操做在P1進程對變量X的寫操做以後,然而讀出來的倒是舊的數據。可是這個圖倒是知足Sequential Consistency(順序一致性)的,由於兩個進程P1,P2的一致性並無衝突。從這兩個進程的角度來看,順序應該是這樣的:Write(y,2) , Read(x,0) , Write(x,4), Read(y,2),每一個進程內部的讀寫順序都是合理的,可是顯然這個順序與全局時鐘下看到的順序並不同。

  • 圖b知足Linearizability Consistency(線性一致性),由於每一個讀操做都讀到了該變量的最新寫的結果,同時兩個進程看到的操做順序與全局時鐘的順序同樣,都是Write(y,2) , Read(x,4) , Write(x,4), Read(y,2)。

  • 圖c不知足Sequential Consistency(順序一致性),固然也就不知足Linearizability Consistency(線性一致性)。由於從進程P1的角度看,它對變量Y的讀操做返回告終果0。那麼就是說,P1進程的對變量Y的讀操做在P2進程對變量Y的寫操做以前,這意味着它認爲的順序是這樣的:write(x,4) , Read(y,0) , Write(y,2), Read(x,0),顯然這個順序又是不能被知足的,由於最後一個對變量x的讀操做讀出來也是舊的數據。所以這個順序是有衝突的,不知足順序一致性。

Casual Consistency(因果一致性)

Casual Consistency(因果一致性)在一致性的要求上,又比Sequential Consistency(順序一致性)下降了:它僅要求有因果關係的操做順序獲得保證,非因果關係的操做順序則無所謂。因果相關的要求是這樣的:

  • 本地順序:本進程中,事件執行的順序即爲本地因果順序。

  • 異地順序:若是讀操做返回的是寫操做的值,那麼該寫操做在順序上必定在讀操做以前。

  • 閉包傳遞:和時鐘向量裏面定義的同樣,若是a->b,b->c,那麼確定也有a->c。

引用《分佈式計算-原理、算法與系統》一書中的圖來進一步說明 Casual Consistency(因果一致性)和 Sequential Consistency(順序一致性)之間的區別:

  • 圖a知足 Sequential Consistency(順序一致性),所以也知足Casual Consistency(因果一致性),由於從這個系統中的四個進程的角度看,它們都有相同的順序也有相同的因果關係。

  • 圖b知足Casual Consistency(因果一致性)可是不知足 Sequential Consistency(順序一致性)。首先P1和P2的寫是沒有因果關係的,從P3看來,Read(x,7) 表示P2的 Write(x,7)必定在P3的Read(x,7)以前, P3的Read(x,2)表示P1的Write(x,2)必定在P3的Read(x,2)以前,又由於P3中Read(x,7) 在Read(x,2)以前(本地因果順序),所以,從P3角度看P1和P2的執行順序應該是:Write(x,7)、Write(x,2)、Write(x,4)。一樣的分析方法,能夠得出從P4角度看P1和P2的執行順序應該是:Write(x,2)、Write(x,4)、Write(x,7)。因爲P3和P4看到的執行順序不一致,所以這不知足Sequential Consistency(順序一致性)要求。

  • 圖c展現了比Casual Consistency(因果一致性)更弱的一種一致性模型: PRAM(Pipelined Random Access Memory)管道式存儲器,是Lipton和Sandberg於1988年在學術報告」PRAM: A scalable shared memory」中提出。如前所述, Sequential Consistency(順序一致性)要求全部進程看到的程序執行順序必須一致,而Casual Consistency(因果一致性)下降了一致性要求,它要求有因果關係的操做在全部進程上看到必須一致,而PRAM Consistency進一步下降一致性要求。先看PRAM定義:「…Writesdone by a single process are received by all other processes in the order inwhich they were issued, but writes from different processes may be seen in adifferent order by different processes.」 意即在PRAM中,不一樣進程能夠看到不一樣的執行順序,但在某一進程上的多個寫操做,在全部進程上看到的順序必須一致,而不一樣進程上的寫操做在不一樣進程上看起來其執行順序則能夠不一致。圖c展現的例子而言,從P3角度看到的P1和P2操做順序爲:Write(x,2)、Write(x,4)、Read(x,4)、Write(x,7),這是知足Casual Consistency(因果一致性)的。從P4角度看爲:Write(x,2)、Read(x,4)、Write(x,7)、Write(x,4),這顯然不知足Casual Consistency(因果一致性)的要求。

從jepsen官網的那張圖能夠看到,Casual Consistency(因果一致性)和PRAM下面還包含了Writes Follow Reads、Monotonic Reads、Monotonic Writes、Read Your Writes等一致性模型,這些都比較簡單,鑑於篇幅緣由本文再也不贅述。

隔離性

前文提到了,Serializable是一種事務隔離級別(併發的事務之間),是ACID中的Isoloation的意思。可是最開始ACID的Isolation只有4中隔離級別,隨着技術的演進,出現了不少當初的標準沒有定義的新的隔離級別,諸如snapshot Isoloation等。下表詳細歸納了6種隔離級別,每種隔離級別強度層層遞進,但也存在或引入某些新的問題,以snapshot Isoloation爲例,它能解決repeatable read存在的幻讀問題,可是卻存在write skew的問題。

下面這張圖更明顯的體現了各隔離級別的強度關係。

下面就不一樣隔離異常分別說明。

在這以前先定義幾個概念:

  • 長期鎖:到事務結束就釋放的鎖
  • 短時間鎖:對相關數據操做完成就釋放的鎖

這裏提到的寫鎖和排他鎖能夠互換,讀鎖和共享鎖能夠互換,長期鎖也被稱爲二階段鎖,就是事務某個時候鎖上了算一個階段,最後一塊兒釋放算一個階段。

P0 dirty write (髒寫)

現象:最開始的階段是一切皆有可能發生,沒有任何鎖,因此碰到的第一個問題是髒寫。當一個事務覆蓋寫了另外一個正在運行的事務寫入的值時就會發生髒寫。好比下面的例子,事務一致性的約束性條件是x必須等於y, 一開始x和y初始值都爲0,以後事務T1 準備寫入 x=y=1 而且事務 T2 準備寫入 x=y=2,可是因爲T1和T2都沒有對數據加鎖,所以致使互相發生覆蓋寫(髒寫),致使最終都成功commit以後,x==2,y==1,違反了約束性條件。

解決:對 x 和 y 持有長期寫鎖(直到commit以後才釋放鎖),這樣後續的T2就沒法覆蓋寫x,T1沒法覆蓋寫y了。防止髒寫之後會出現新的現象,髒讀。

P1 dirty read (髒讀 read uncommited)

現象: 當一個事務讀取另外一個仍處於運行中的事務寫入的值時(未提交),就會發生髒讀。好比下面的例子,事務一致性的約束性條件是x+y=100, 一開始x和y初始值都爲50,事務T1準備將x改寫爲10(加長期寫鎖),此時事務T2讀x爲10(沒有加任何鎖),讀y爲50,對事務T2而言,x+y=60不知足約束性條件。

解決: 在基於鎖的實現中,使用短時間讀鎖和長期寫鎖,長期寫鎖能夠防止事務 T2 讀到x讀數據,短時間讀鎖可讓後續的T1 能夠繼續寫y。解決髒讀問題,又面臨的問題是不可重複讀。

P2 non-repeatable read (不可重複讀)

現象:在使用短時間讀鎖和長期寫鎖實現read commited以後就可能存在這種異常。好比下面的例子,事務一致性的約束性條件是x+y=100, 一開始x和y初始值都爲50,T1讀x爲50(對x加短時間讀鎖),以後事務T2對x和y都作了修改(對x和y加長期寫鎖),而後成功提交。 以後T1讀y爲90,此時對T1而言,x+y=140不知足約束條件,所以出現不可重複讀。(注:由於此時T2已經commit,所以這不屬於髒讀read uncommited,注意和上面的例子作區分。同時還要注意,不少人認爲只有對同一個對象讀屢次出現值不同纔算不可重複讀,其實這是狹隘的理解,其實能夠屢次讀不一樣對象,只要屢次讀會改變一致性約束就算不可重複讀)

解決:在基於鎖的實現中,使用長期讀鎖和長期寫鎖,也就是事務 T2 的 x 要等事務 T1 提交以後才能寫入。解決了不可重複讀之後,還會碰到幻讀的狀況。

P3 phantom (幻讀)

現象:幻讀發生在正在執行的事務 T1 有斷言讀 (如select where) 時,另一個事務 T2 執行了和斷言集合有交集的插入操做。好比 T1 在 T2 插入d以前讀到了員工總數是 3,可是 T2 執行的時候有交集,插入了新的數據d,這個時候員工總數是 4,可是 T1 若是再讀取的話,就會發現員工總數變成了 4,而不是最初的 3,這就是幻讀。

解決:解決幻讀的方式是使用長期(斷言型)讀鎖和寫鎖。也就是不容許在這個範圍內進行插入操做。解決了幻讀之後的事務就徹底可串行化了(不必定是真正的串行,而是等同於某種串行執行效果),這樣的事務併發度是最弱的。

P4 update lost (更新丟失)

更新丟失這個現象不是比幻讀更約束的現象,這個是在防止髒讀(實現read commited)之後可能會出現的現象。

現象:事務 T2 提交的寫被其餘事務覆蓋,首先,這不是髒寫,由於 T2 已經提交,其次沒有髒讀,由於在寫以後沒有讀操做,這樣的現象稱爲更新丟失。

解決:升級到可重複讀就能夠了。

P4C cursor update lost (遊標更新丟失)

現象:Cursor Lost Update 是上面 Lost Update 的一個變種,跟 SQL 的 cursor 相關。在下面的例子中,RC(x) 代表在 cursor 下面 read x,而 WC(x) 則代表在 cursor 下面寫入 x。若是容許 T2 在 T1 RC 和 WC 之間寫入數據,那麼 T2 的更新也會丟失。

解決:在遊標移動或者釋放以前,都不釋放鎖,這個是到達可重複讀以前的一個插曲。這個也是在實現read commited後會發生的事情。

A5A read skew (讀偏)

偏能夠理解爲不一致,這個是發生在多個數據之間有一個總的約束的時候(邏輯上業務層面的約束)。

現象:讀偏也是在實現read commited後可能出現的現象。 假設如今的約束是x+y=100,事務T1先讀x爲50(T1對x加短時間讀鎖),而後事務T2將x改寫爲25(T2對x加長期寫鎖),將y改寫爲75(T2對x加長期寫鎖),對事務T2而言,x+y=100是知足約束的,因此它能夠成功提交。T2提交成功以後(全部的鎖也都釋放了),此時T1還在運行,T1讀y爲75,x+y>100,不知足約束。

解決:使用快照隔離 (Snapshot Isolation),快照隔離是基於 MVCC 的。當一個 T 事務開始的時候,T 會得到一個抽象的時間戳(版本),當對數據 x 進行讀取的時候,並非直接看到最新寫入的數據,而是在 T 開始前的全部執行事務中最後一個對 X 標記的版本(若是 T 修改過 x,那麼看到的是本身的版本)。也就是說 T 是基於當前的數據庫最近一個鏡像進行操做的,而 T 開始執行時得到的版本就是這個快照的憑證。這樣能保證全部的讀都是基於一個一致的狀態獲取的。

SI 解決衝突的方法通常是 「First-Commiter-Wins」, 也就是說,若是兩個併發的事務修改了同一個數據,先寫的事務會成功,然後寫的事務會發現版本和本來的不一致而退出事務(abort)。

以這裏的例子來講,T1 的 y 只會讀到本身開始時候的版本,也就是 50,而不是 75,這樣讀偏就解決了。可是快照隔離仍是不能解決另外一個問題,就是寫偏。這是咱們要面臨的新問題。

A5B write skew (寫偏)

現象:這個和讀偏相似,只不過,它是一種業務邏輯層面上的不一致。事務T1和T2一開始都有本身的版本,T1讀x爲30,並將y從10改成60,從T1自身角度看並無違反約束(x+y<=100)。一樣,T2中先讀y爲10,並將x從30改成50,從T2角度看也並無違反約束(x+y<=100)。最後,在T1和T2提交時都是能夠提交成功的(任意順序),由於它們沒有修改相同的數據,在數據層面不存在衝突。可是此時x+y已經大於100了,違反了業務邏輯層面的約束。

解決:目前Snapshot Isolation(SI)的算法有不少,參考 cockroachDB 使用的論文的話,能夠說,經過對版本依賴構成有向圖,解決成環問題,以此達到Serializable Snapshot Isolation(SSI)的級別。

好比上面的例子,若是 T1 在 y 讀了以後寫了一個版本的 y 就構成一個先讀後寫的 rw(y) 依賴,相似的 T2 對 T1 構成了一個先讀後寫的 rw(x) 依賴。還有兩種無害的依賴是先寫後讀 (wr) 和先寫後寫 (ww)。論文中闡述了,形成寫偏的條件是成環,而且環中有兩個連續的 rw 依賴。也就是下面這種形式。

這個問題的關鍵是,檢查成環這件事情,就跟操做系統檢查死鎖同樣,消耗太大了,性能上不能接受。因此這個實現的妥協是,把檢查放寬,讓一些無害的條件也被認定爲有害,經過重試來恢復執行,寧肯錯是一百,也毫不放過一個。

這個條件是隻要有兩個連續的 rw 依賴就會放棄提交,即便沒有成環。這個檢查發生在讀的時候若是發現讀的版本和本身開始以前的版本不一致就會找到依賴的事務,構建一條入邊,另外一個事務構建一條出邊,若是某個事務入邊出邊都有 rw 邊,這個節點就會被做爲嫌疑人。固然還有其餘關於Serializable Snapshot Isolation(SSI)隔離的論文能夠參考。

總結

至此,本文對開始提出的幾個概念和疑惑都作了介紹和解答,其實,分佈式(數據庫)系統中還有不少相似的概念,好比計算層經常使用的邏輯執行計劃與物理執行計劃、語法樹與抽象語法樹等,其中,有不少並非概念上的迷惑,而是因爲這些技術和詞彙大多由國外發明,在向國內滲透過程當中,通常人很難在一開始就能徹底東西背後的含義。就比如,外國人第一次看到「當心地滑」,很難搞清楚兩種含義的區別。

相關文章
相關標籤/搜索