前言
在分散式(資料庫)系統中,我們經常會聽到一些“高大上”卻又比較“迷惑”的詞彙,比如,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(線性一致性)的要求兩個:
-
任何一次讀都能讀到某個資料的最近一次寫的資料。
-
系統中的所有程式,看到的操作順序,都和全域性時鐘下的順序一致。
顯然這兩個條件都對全域性時鐘有非常高的要求。比它要求更弱一些的,就是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)隔離的論文可以參考。
總結
至此,本文對開始提出的幾個概念和疑惑都做了介紹和解答,其實,分散式(資料庫)系統中還有很多類似的概念,比如計算層常用的邏輯執行計劃與物理執行計劃、語法樹與抽象語法樹等,其中,有很多並不是概念上的迷惑,而是由於這些技術和詞彙大多由國外發明,在向國內滲透過程中,一般人很難在一開始就能完全東西背後的含義。就好比,外國人第一次看到“小心地滑”,很難搞清楚兩種含義的區別。