一致性模型
有時候,在跟一些同學討論 TiKV 事務模型的時候,我都提到了 Linearizability,也提到了 Snapshot Isolation,以及需要手動 lock 來保證 Serializable Snapshot Isolation,很多時候,當我嘴裡面蹦出來這些名詞的時候,有些同學就一臉懵逼了。所以我覺得有必要仔細來解釋一下,順帶讓我自己將所有的 isolation 以及 consistency 這些情況都歸納總結一遍,讓自己也理解透徹一點。
幸運的是,業內已經有很多人做了這個事情,譬如在 ofollow,noindex">Highly Available Transactions: Virtues and Limitations 這篇論文裡面,作者就總結了不同模型是否能滿足 Highly Available Transactions(HATs)。

途中,紅色圓圈裡面的模型屬於 Unavailable,藍色的屬於 Sticky Available,其餘的就是 Highly Available。這裡解釋下相關的含義:
- Unavailable: 當出現網路隔離等問題的時候,為了保證資料的一致性,不提供服務。熟悉 CAP 理論的同學應該清楚,這就是典型的 CP 系統了。
- Sticky Available: 即使一些節點出現問題,在一些還沒出現故障的節點,仍然保證可用,但需要保證 client 的操作是一致的。
- Highly Available: 就是網路全掛掉,在沒有出現問題的節點上面,仍然可用。
Unavailable 比較容易理解,這裡在討論下 Sticky 和 Highly,對於 Highly Available 來說,如果一個 server 掛掉了,client 可以去連線任意的其他 server,如果這時候仍然能獲取到結果,那麼就是 Highly Available 的。但對於 Sticky 來說,還需要保證 client 操作的一致性,譬如 client 現在 server 1 上面進行了很多操作,這時候 server 1 掛掉了,client 切換到 server 2,但在 server 2 上面看不到 client 之前的操作結果,那麼這個系統就不是 Sticky 的。所有能在 Highly Available 系統上面保證的事情一定也能在 Sticky Available 系統上面保證,但反過來就不一定了。
Jepsen 在 官網 上面有一個簡化但更好看一點的圖

下面,我會按照 Jepsen 裡面的圖,對不同的 model 進行解釋一下。至於為啥選擇 Jepsen 裡面的例子,一個是因為 Jepsen 現在是一款主流的測試不同分散式系統一致性的工具,它的測試用例就是測試的是上圖提到的模型,我們自然也會關心這些模型。另外一個就是這個模型已經覆蓋了大多數場景了,理解了這些,大部分都能遊刃有餘處理了。
如果大家仔細觀察,可以發現,從根節點 Strict Serializable,其實是有兩個分支的,一個對應的就是資料庫裡面的 Isolation(ACID 裡面的 I),另一個其實對應的是分散式系統的 Consistency(CAP 裡面的 C),在 HATs 裡面,叫做 Session Guarantees。
Isolation
要對 Isolation 有個快速的理解,其實只需要看 A Critique of ANSI SQL Isolation Levels 這篇論文就足夠了,裡面詳細的介紹了資料庫實現中遇到的各種各樣的 isolation 問題,以及不同的 isolation level 到底能不能解決。
在論文裡面,作者詳細的列舉了多種異常現象,這裡大概介紹一下。
P0 - Dirty Write
Dirty Write 就是一個事務,覆蓋了另一個之前還未提交事務寫入的值。假設現在我們有兩個事務,一個事務寫入 x = y = 1,而另一個事務寫入 x = y = 2,那麼最終的結果,我們是希望看到 x 和 y 要不全等於 1,要不全等於 2。但在 Dirty Write 情況下面,可能會出現如下情況:
+------+-------+-------+-------+-------+ | T1| Wx(1) ||| Wy(1) | +------+-------+-------+-------+-------+ | T2|| Wx(2) | Wy(2) || +------+-------+-------+-------+-------+ | x(0) | 1| 2| 2| 2| +------+-------+-------+-------+-------+ | y(0) | 0| 0| 2| 1| +------+-------+-------+-------+-------+
可以看到,最終的值是 x = 2 而 y = 1,已經破壞了資料的一致性了。
P1 - Dirty Read
Dirty Read 出現在一個事務讀取到了另一個還未提交事務的修改資料。假設現在我們有一個兩個賬戶,x 和 y,各自有 50 塊錢,x 需要給 y 轉 40 元錢,那麼無論怎樣,x + y = 100 這個約束是不能打破的,但在 Dirty Read 下面,可能出現:
+-------+--------+--------+--------+--------+ | T1| Wx(10) ||| Wy(90) | +-------+--------+--------+--------+--------+ | T2|| Rx(10) | Ry(50) || +-------+--------+--------+--------+--------+ | x(50) | 10| 10| 10| 10| +-------+--------+--------+--------+--------+ | y(50) | 50| 50| 50| 90| +-------+--------+--------+--------+--------+
在事務 T2,讀取到的 x + y = 60,已經打破了約束條件了。
P2 - Fuzzy Read
Fuzzy Read 也叫做 Non-Repeatable Read,也就是一個還在執行的事務讀取到了另一個事務的更新操作,仍然是上面的轉賬例子:
+-------+--------+--------+--------+--------+ | T1| Rx(50) ||| Ry(90) | +-------+--------+--------+--------+--------+ | T2|| Wx(10) | Wy(90) || +-------+--------+--------+--------+--------+ | x(50) | 50| 10| 10| 10| +-------+--------+--------+--------+--------+ | y(50) | 50| 50| 90| 90| +-------+--------+--------+--------+--------+
在 T1 還在執行的過程中,T2 已經完成了轉賬,但 T1 這時候能讀到最新的值,也就是 x + y = 140 了,破壞了約束條件。
P3 - Phantom
Phantom 通常發生在一個事務首先進行了一次按照某個條件的 read 操作,譬如 SQL 裡面的 SELECT WHERE P
,然後在這個事務還沒結束的時候,另外的事務寫入了一個新的滿足這個條件的資料,這時候這個新寫入的資料就是 Phantom 的了。
+----------------+-----------+--------------+--------------+--------------+ | T1| {a, b, c} ||| R(4)| +----------------+-----------+--------------+--------------+--------------+ | T2|| W(d)| W(4)|| +----------------+-----------+--------------+--------------+--------------+ | Employees| {a, b, c} | {a, b, c, d} | {a, b, c, d} | {a, b, c, d} | +----------------+-----------+--------------+--------------+--------------+ | Employee Count | 3| 3| 4| 4| +----------------+-----------+--------------+--------------+--------------+
假設現在 T1 按照某個條件讀取到了所有僱員 a,b,c,這時候 count 是 3,然後 T2 插入了一個新的僱員 d,同時更新了 count 為 4,但這時候 T1 在讀取 count 的時候會得到 4,已經跟之前讀取到的 a,b,c 衝突了。
P4 - Lost Update
我們有時候也會遇到一種 Lost Update 的問題,如下
+--------+-----+---------+---------+ | T1||| Wx(110) | +--------+-----+---------+---------+ | T2|| Wx(120) || +--------+-----+---------+---------+ | x(100) | 100 | 120| 110| +--------+-----+---------+---------+
在上面的例子中,我們沒有任何 dirty write,因為 T2 在 T1 更新之前已經提交成功,也沒有任何 dirty read,因為我們在 write 之後沒有任何 read 操作,但是,當整個事務結束之後,T2 的更新其實丟失了。
P4C - Cursor Lost Update
Cursor Lost Update 是上面 Lost Update 的一個變種,跟 SQL 的 cursor 相關。在下面的例子中,RC(x) 表明在 cursor 下面 read x,而 WC(x) 則表明在 cursor 下面寫入 x。
+--------+----------+---------+----------+ | T1| RCx(100) || Wx(110) | +--------+----------+---------+----------+ | T2|| Wx(75) || +--------+----------+---------+----------+ | x(100) | 100| 75| 110| +--------+----------+---------+----------+
如果我們允許 T2 在 T1 RC 和 WC 之間寫入資料,那麼 T2 的更新也會丟失。
A5A - Read Skew
Read Skew 發生在兩個或者多個有完整性約束的資料上面,還是傳統的轉賬例子,需要保證 x + y = 100,那麼 T1 就會看到不一致的資料了。
+-------+--------+--------+--------+--------+ | T1| Rx(50) ||| Ry(75) | +-------+--------+--------+--------+--------+ | T2|| Wx(25) | Wy(75) || +-------+--------+--------+--------+--------+ | x(50) | 50| 25| 25| 25| +-------+--------+--------+--------+--------+ | y(50) | 50| 50| 75| 75| +-------+--------+--------+--------+--------+
A5B - Write Skew
Write Skew 跟 Read Skew 比較類似,假設 x + y <= 100,T1 和 T2 在執行的時候都發現滿足約束,然後 T1 更新了 y,而 T2 更新了 x,然後最終結果打破了約束,如下:
+-------+--------+--------+--------+--------+ | T1| Rx(30) | Ry(10) | Wy(60) || +-------+--------+--------+--------+--------+ | T2| Rx(30) | Ry(10) || Wx(50) | +-------+--------+--------+--------+--------+ | x(30) | 30| 30| 30| 50| +-------+--------+--------+--------+--------+ | y(10) | 10| 10| 60| 60| +-------+--------+--------+--------+--------+
Isolation Levels
上面我們介紹了不同的異常情況,下面的表格說明了,在不同的隔離級別下面,那些異常情況可能發生:
P0 | P1 | P4C | P4 | P2 | P3 | A5A | A5B | |
---|---|---|---|---|---|---|---|---|
Read Uncommitted | NP | P | P | P | P | P | P | P |
Read Committed | NP | NP | P | P | P | P | P | P |
Cursor Stability | NP | NP | NP | SP | SP | P | P | SP |
Repeatable Read | NP | NP | NP | NP | NP | P | NP | NP |
Snapshot | NP | NP | NP | NP | NP | SP | NP | P |
Serializable | NP | NP | NP | NP | NP | NP | NP | NP |
- NP - Not Possible,在該隔離級別下面不可能發生
- SP - Sometimes Possible,在該隔離級別下面有時候可能發生
- P - Possible,在該隔離級別下面會發生
鑑於網上已經對不同的 Isolation Level,尤其是 MySQL 的解釋的太多了,這裡就簡單的解釋一下。
- Read Uncommitted - 能讀到另外事務未提交的修改。
- Read Committed - 能讀到另外事務已經提交的修改。
- Cursor Stability - 使用 cursor 在事務裡面引用特定的資料,當一個事務用 cursor 來讀取某個資料的時候,這個資料不可能被其他事務更改,除非 cursor 被釋放,或者事務提交。
- Monotonic Atomic View - 這個級別是 read committed 的增強,提供了一個原子性的約束,當一個在 T1 裡面的 write 被另外事務 T2 觀察到的時候,T1 裡面所有的修改都會被 T2 給觀察到。
- Repeatable Read - 可重複讀,也就是對於某一個數據,即使另外的事務有修改,也會讀取到一樣的值。
- Snapshot - 每個事務都會在各自獨立,一致的 snapshot 上面對資料庫進行操作。所有修改只有在提交的時候才會對外可見。如果 T1 修改了某個資料,在提交之前另外的事務 T2 修改並提交了,那麼 T1 會回滾。
- Serializable - 事務按照一定順序執行。
另外需要注意,上面提到的 isolation level 都不保證實時約束,如果一個程序 A 完成了一次寫入 w,然後另外的程序 B 開始了一次讀取 r,r 並不能保證觀察到 w 的結果。另外,在不同事務之間,這些 isolation level 也不保證不同程序的順序。一個程序可能在一次事務裡面看到一次寫入 w,但可能在後面的事務上面沒看到同樣的 w。事實上,一個程序甚至可能看不到在這個程序上面之前的寫入,如果這些寫入都是發生在不同的事務裡面。有時候,他們還可能會對事務進行排序,譬如將 write-only 的事務放到所有的 read 事務的後面。
要解決這些問題,我們需要引入順序約束,這也就是下面 Session Guarantee 要乾的事情。
Session Guarantee
在 HATs 論文裡面,相關的概念叫做 Session Guarantee,主要是用來保證在一個 session 裡面的實時性約束以及客戶端的操作順序。
Writes Follow Reads
如果某個程序讀到了一次寫入 w1 寫入的值 v,然後進行了一次新的寫入 w2,那麼 w2 寫入的值將會在 w1 之後可見。
Monotonic Reads
如果一個程序開始了一次讀取 r1,然後在開始另一次讀取 r2,那麼 r2 不可能看到 r1 之前資料。
Monotonic Writes
如果一個程序先進行了一次寫入 w1,然後在進行了一次寫入 w2,那麼所有其他的程序都會觀察到 w1 在 w2 之前發生。
Read Your Writes
如果一個程序先進行了一次寫入 w,然後後面執行了一次讀取 r,那麼 r 一定會看到 w 的改動。
PRAM
PRAM 就是 Pipeline Random Access Memory,對於單個程序的寫操作都被觀察到是順序的,但不同的程序寫會觀察到不同的順序。譬如下面這個操作是滿足 PRAM 的,但不滿足後面說的 Causal。
+----+------+------+------+------+------+ | P1 | W(1) ||||| +----+------+------+------+------+------+ | P2 || R(1) | W(2) ||| +----+------+------+------+------+------+ | P3 |||| R(2) | R(1) | +----+------+------+------+------+------+ | P4 |||| R(1) | R(2) | +----+------+------+------+------+------+
Causal
Causal 確定了有因果關係的操作在所有程序間的一致順序。譬如下面這個
+----+------+------+------+------+------+------+ | P1 | W(1) |||||| +----+------+------+------+------+------+------+ | P2 || W(2) ||||| +----+------+------+------+------+------+------+ | P3 ||| R(2) || R(1) || +----+------+------+------+------+------+------+ | P4 |||| R(1) || R(2) | +----+------+------+------+------+------+------+
對於 P3 和 P4 來說,無論是先讀到 2,還是先讀到 1, 都是沒問題的,因為 P1 和 P2 裡面的 write 操作並沒有因果性,是並行的。但是下面這個
+----+------+------+------+------+------+------+ | P1 | W(1) |||||| +----+------+------+------+------+------+------+ | P2 || R(1) | W(2) |||| +----+------+------+------+------+------+------+ | P3 |||| R(2) | R(1) || +----+------+------+------+------+------+------+ | P4 |||| R(1) || R(2) | +----+------+------+------+------+------+------+
就不滿足 Cansal 的一致性要求了,因為對於 P2 來說,在 Write 2 之前,進行了一次 Read 1 的操作,已經確定了 Write 1 會在 Write 2 之前發生,也就是確定了因果關係,所以 P3 打破了這個關係。
Sequential
Sequential 會保證操作按照一定順序發生,並且這個順序會在不同的程序上面都是一致的。一個程序會比另外的程序超前,或者落後,譬如這個程序可能讀到了已經是陳舊的資料,但是,如果一個程序 A 從程序 B 讀到了某個狀態,那麼它就不可能在讀到 B 之前的狀態了。
譬如下面的操作就是滿足 Sequential 的
+----+------+------+------+------+------+------+ | P1 | W(1) |||||| +----+------+------+------+------+------+------+ | P2 || W(2) ||||| +----+------+------+------+------+------+------+ | P3 ||| R(1) || R(2) || +----+------+------+------+------+------+------+ | P4 |||| R(2) || R(2) | +----+------+------+------+------+------+------+
對於 P3 來說,它仍然能讀到之前的 stale 狀態 1。但下面的就不對了:
+----+------+------+------+------+------+------+ | P1 | W(1) |||||| +----+------+------+------+------+------+------+ | P2 || W(2) ||||| +----+------+------+------+------+------+------+ | P3 ||| R(2) || R(1) || +----+------+------+------+------+------+------+ | P4 |||| R(2) || R(2) | +----+------+------+------+------+------+------+
對於 P3 來說,它已經讀到了最新的狀態 2,就不可能在讀到之前的狀態 1 了。
Linearizable
Linearizability 要求所有的操作都是按照一定的順序原子的發生,而這個順序可以認為就是跟操作發生的時間一致的。也就是說,如果一個操作 A 在 B 開始之前就結束了,那麼 B 只可能在 A 之後才能產生作用。
譬如下面的操作:
+----+------+------+------+------+------+ | P1 | W(1) ||||| +----+------+------+------+------+------+ | P2 || W(2) |||| +----+------+------+------+------+------+ | P3 ||| R(2) | R(2) || +----+------+------+------+------+------+ | P4 |||| R(2) | R(2) | +----+------+------+------+------+------+
對於 P3 和 P4 來說,因為之前已經有新的寫入,所以他們只能讀到 2,不可能讀到 1。
Strict Serializable
終於來到了 Strict Serializable,大家可以看到,它結合了 serializable 以及 linearizable,也就是說,它會讓所有操作按照實時的順序依次操作,也就是所有的程序會觀察到完全一致的順序,這也是最強的一致性模型了。
TiKV
好了,最後再來聊聊 TiKV,TiKV 是一個支援分散式事務的 key-value database。對於某個事務,TiKV 會通過 PD 這個服務在事務開始的時候分配一個 start timestamp,以及事務提交的時候分配一個 commit timestamp。因為我們的授時是通過 PD 這個單點服務進行的,所以時間是一定能保證單調遞增的,也就是說,我們所有的操作都能跟保證實時有序,也就是滿足 Linearizable。
TiKV 採用的是常用的 MVCC 模型,也就是每個 key-value 實際儲存的時候,會在 key 上面帶一個 timestamp,我們就可以用 timestamp 來生成整個資料庫的 snapshot 了,所以 TiKV 是 snapshot isolation 的。既然是 snapshot isolation,那麼就會遇到 write skew 問題,所以 TiKV 額外提供了 serializable snapshot isolation,使用者需要顯示的對要操作的資料進行 lock 操作。
但現在 TiKV 並不支援對 range 加 lock,所以不能完全的防止 phantom,譬如假設最多允許 8 個任務,現在已經有 7 個任務了,我們還可以新增一個任務,但這時候另外一個事務也做了同樣的事情,但新增的是不同的任務,這時候就會變成 9 個任務,另外的事務在 scan 的時候就會發現打破了約束。這個也就是 A Critique of ANSI SQL Isolation Levels 裡面提到的 sometimes possible。
所以,TiKV 是 snapshot isolation + linearizable。雖然 TiKV 也可以支援 Read Committed,但通常不建議在生產環境中使用,因為 TiKV 的 Read Committed 跟傳統的還不太一樣,可能會出現能讀到一個事務提交到某個節點的資料,但這時候在另外的節點還讀不到這個事務提交的資料,畢竟在分散式系統下面,不同節點的事務提交也是有網路延遲的,不可能同時執行。
小結
在分散式系統裡面,一致性是非常重要的一個概念,理解了它,在自己設計分散式系統的時候,就能充分的考慮到底系統應該提供怎樣的一致性模型。譬如對於 TP 資料庫來說,就需要有一個比較 strong 的一致性模型,而對於一些不重要的系統,譬如 cache 這些,就可以使用一些比較 weak 的模型。對 TiKV 來說,我們在 Percolator 基礎上面,也一直在致力於分散式事務的優化,如果你對這方面感興趣,歡迎聯絡我 pingcap.com" target="_blank" rel="nofollow,noindex">[email protected] 。