PostgreSQL為開發者提供了一組豐富的工具來管理對數據的並發訪問。在內部,數據一致性通過使用一種多版本模型(多版本並發控制,MVCC)來維護。這就意味着每個 SQL 語句看到的都只是一小段時間之前的數據快照(一個數據庫版本),而不管底層數據的當前狀態。這樣可以保護語句不會看到可能由其他在相同數據行上執行更新的並發事務造成的不一致數據,為每一個數據庫會話提供事務隔離。MVCC避免了傳統的數據庫系統的鎖定方法,將鎖爭奪最小化來允許多用戶環境中的合理性能。
使用MVCC並發控制模型而不是鎖定的主要優點是在MVCC中,對查詢(讀)數據的鎖請求與寫數據的鎖請求不沖突,所以讀不會阻塞寫,而寫也從不阻塞讀。甚至在通過使用革新的可序列化快照隔離(SSI)級別提供最嚴格的事務隔離級別時,PostgreSQL也維持這個保證。
今天我們就從基礎開始聊聊PostgreSQL內部的這一特性,聊聊MVCC和PostgreSQL中的事務特性。
這里,我們先用一個表格展示PostgreSQL中的事務等級,在看到后面章節時可以翻回來看看。
1.基礎知識
1.1事務ID
當一個事務開始時,PostgreSQL中的事務管理系統會為該事務分配一個唯一標識符,即事務ID(txid).PostgreSQL中的txid被定義為一個32位的無符號整數,也就是說,它能記錄大約42億個事務。通常txid對我們是透明的,但是我們可以利用PostgreSQL內部的函數來獲取當前事務的txid:
postgres=# BEGIN;
BEGIN
postgres=# SELECT txid_current();
txid_current
--------------
233
(1 row)
在所有的txid中,有幾個值是有特殊含義的:
txid = 0 表示 Invalid txid,通常作為判斷txid的有效性使用;
txid = 1 表示 Bootstrap txid,目前情況下,只在intidb的時候,初始化數據庫的時候使用
txid = 2 表示 Frozen txid,一般是在Vacuum時使用(在后面會提到)。
記住這樣一個假設:txid小的事務所修改的元組對txid大的事務來說,是可見的,反之則不可見。
1.2元組(tuple)結構
關於元組的結構能說的東西很多,我在這篇文章中也談到了一些。不過我們這里不談那么深。關於元組(tuple)原則上可以分成普通的元組和TOAST元組。這里為簡化起見,我們就只談論簡單的元組。
簡單元組的結構圖如下:
在上圖中我們需要了解的和事務相關的結構是HeapTupleHeaderData結構,這個也就是一條元組的“頭”部分。
有幾個字段需要我們了解下:
-
t_xmin中保存的是插入這條元組的事務的txid
-
t_xmax中保存的是更新或者刪除這條元組的事務的txid。如果這條元組並沒有沒刪除或者更新,那么t_xmax字段被設置為0,即該字段INVALID
-
t_cid中保存的是插入這條元組的命令的id。在一個事務中可能會有多個命令,事務中的這些命令會依次被編號(從0開始遞增)。對於如下的事務: BEGIN;INSERT ;INSERT END。那么第一個INSERT的t_cid為0,第二個INSERT的t_cid為1.依次類推。
-
t_ctid中保存元組的標識符(即tid)。它指向該元組本身或者該元組的新“版本”。因為PostgreSQL對記錄的修改不會直接修改tuple中的用戶數據,而是重新生成一個tuple,舊的tuple通過t_ctid指向新的tuple。如果一條記錄被修改多次,那么該記錄會存在多個“版本”。各版本之間通過t_ctid串聯,形成一個版本鏈。通過這個版本鏈,我們就可以找到最新的版本了。實際的t_ctid是一個二元組(x,y).其中x(從0開始編號)代表元組所在的page,y(從1開始編號)表示在該page的第幾個位置上。
1.3元組(tuple)的INSERT/DELETE/UPDATE操作
上面說到PostgreSQL中對記錄的修改不是直接修改tuple結構,而是重新生成一個tuple,舊的tuple通過t_ctid指向新的tuple。那么這里我們依次解釋INSERT/DELETE/UPDATE是如何實現的。
我們先上一個簡圖,這里面的page header,line pointer這些這里談論無關的結構我就不畫出來了。我們重點關注tuple在page中的位置關系。
1.3.1 INSERT操作
INSERT操作最簡單,直接將一條元組放進對應的page里面即可。如下:
這里我們假設
-
1.該表在執行插入前為空
-
2.txid為99的事務插入了該元組==> Tuple_1。
於是在Tuple_1中我們可以看到:
-
t_xmin被設置為了99,也就是事務的txid,99.
-
t_xmax被設置為了0,因為該元組還沒有被更新或者刪除
-
t_cid被設置為了0,很顯然,它是被事務的第一條命令插入的
-
t_ctid被設置為了(0,1),這是這個表的第一個元組,因此它被放置在第0 page的第1個位置上。
如何從數據庫外部來驗證呢?PostgreSQL提供了一個插件:pageinspect
我們可以像下面那樣去查看數據庫頁面上的信息:
postgres=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
postgres=# CREATE TABLE tbl (data text);
CREATE TABLE
postgres=# INSERT INTO tbl VALUES('A');
INSERT 0 1
postgres=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid
FROM heap_page_items(get_raw_page('tbl', 0));
tuple | t_xmin | t_xmax | t_cid | t_ctid
-------+--------+--------+-------+--------
1 | 99 | 0 | 0 | (0,1)
(1 row)
1.3.2 DELETE操作
上面說過了,PostgreSQL中的刪除操作只是在邏輯上“刪除了”元組,實際上該元組依然留存在數據庫的存儲頁面中,只是對該元組進行了一些處理,使得其在查詢中變得“不可見”。
假設我們在txid為111的事務中將上面插入的元組進行刪除時,其結果如下:
我們可以看到,Tuple_1元組的t_xmax字段被修改了,被改成了111,即刪除Tuple_1的事務的txid。
當txid為111的事務被提交時,Tuple_1就成了無效的元組了,成為“dead tuple”。
從圖上可以看到,這個Tuple_1依然殘留在數據庫的頁面上。隨着數據庫的運行,可以預見,這種“dead tuple”會越來越多。他們,會在運行VACUUM命令時被清理掉,有關VACUUM不是我們這次討論的重點,這里略去。
1.3.3 UPDATE操作
相對於前面兩個操作,UPDATE顯得稍微復雜一點。根據我們上面所說,PostgreSQL對記錄的修改不會直接修改tuple中的用戶數據。PostgreSQL對UPDATE的處理是先刪除舊數據,后增加新數據。“翻譯”過來就是將要更新的舊tuple標記為DELETE,然后再插入一條新的tuple。
同樣地,我們假設在txid為99的事務中插入一條記錄,並且在txid為100的事務中被先后更新2次。過程如下:
根據我們提到的"先刪除舊數據,后增加新數據"的原則,當執行txid為100的事務中的第一條UPDATE命令時,操作分為兩步:
-
刪除舊元組Tuple_1:設置Tuple_1的t_xmax值為當前事務的txid;
-
新增新元組Tuple_2:設置Tuple_2的t_xmin,t_xmax,t_cid,t_ctid;設置Tuple_1的t_ctid指向Tuple_2
即:
對於Tuple_1:
t_xmax = 100;
t_ctid : (0,1) => (0,2)
對於Tuple_2:
t_xmin = 100;
t_xmax = 0;
t_cid = 0;
t_ctid = (0,2)
同理,當執行txid為100的事務中的第二條UPDATE命令也是如此:
對於Tuple_2:
t_xmax = 100;
t_ctid : (0,2) => (0,3)
對於Tuple_3:
t_xmin = 100;
t_xmax = 0;
t_cid = 1;
t_ctid = (0,3)
當txid為100的事務被提交時,Tuple_1和Tuple_2就成了“dead tuple”,反之,如果該事務被abort,則Tuple_2和Tuple_3就成了“dead tuple”。
說了以上這些,大家應該對這些操作在數據庫內部的實現有一個粗略的理解。同時,大家可能對這個ctid很好奇。這是個標記tuple所在位置的標記。那么問題來了,tuple插入的位置是如何選定的?完全是按照順序存放么?如果是那樣,我覺得可能直接記錄tuple在頁面內部的偏移就好了。問題就在於這個位置的選定是有"技術"的,PostgreSQL內部有一個稱為FSM的機制,即Free Space Map。通過它發現一個表的各個頁面的空余空間,從而決定放在那里比較好。具體的不多說,有興趣的可以通過PostgreSQL的另一個插件pg_freespacemap來探究一下。
postgres=# CREATE EXTENSION pg_freespacemap;
CREATE EXTENSION
postgres=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio"
FROM pg_freespace('accounts');
blkno | avail | freespace ratio
-------+-------+-----------------
0 | 7904 | 96.00
1 | 7520 | 91.00
2 | 7136 | 87.00
3 | 7136 | 87.00
4 | 7136 | 87.00
5 | 7136 | 87.00
....
有了上面的這些基礎,我們就可以繼續討論一些較為深入的內容了。
2.MVCC基礎
2.1 事務提交日志(commit log)
PostgreSQL采用的日志主要有XLOG和CLOG,即事務日志和事務提交日志。XLOG是一般的日志記錄,即通常意義上所認識的日志記錄,它記錄了事務對數據更新的過程和事務的最終狀態。CLOG在一般的數據庫教材上是沒有提及的,其實CLOG是XLOG的一種輔助形式,記錄了事務的最終狀態。因為每一條XLOGH志記錄相對較大,如果需要通過日志判斷一個事務的狀態,那么使用CLOG比使用XLOG要髙效得多。同時CLOG占用的空間也非常有限,因此它被存儲在共享內存中,可以快速地讀取。
下面我們就來看看clog的工作原理及其維護過程。
首先,在CLOG中,PostgreSQL定義了四個事務狀態:
IN_PROGRESS,
COMMITTED,
ABORTED,
SUB_COMMITTED.
除了最后一個狀態,其它的狀態都是“人如其名”,不多說。而SUB_COMMITTED針對的是子事務,這里先不討論太多。
由於存儲在共享內存中,CLOG是以8KB大小的page存儲的。在CLOG的頁面內部事務狀態是以類似"數組"的形式存儲的。其中"數組"的下標就是事務的ID,"數組"中的值即為對應下標的事務的當前狀態。由於CLOG只記錄事務的4個狀態,因此,只需要2個bit就可以表示,即一個字節可以表示4個事務狀態。在一個CLOG page(8KB)中,可以表示8K*8b/2b=32K=2^15條CLOG記錄。
下面上例子:
在T1時刻:txid為200的事務提交了,那么在CLOG中txid為200的事務的狀態從IN_PROGRESS裝換為COMMITTED。
在T2時刻:txid為201的事務abort了,那么在CLOG中txid為201的事務的狀態從IN_PROGRESS裝換為ABORTED。
整個CLOG就這樣一直追溯着事務系統中所有事務的狀態。如果有新的事務則直接在"數組"后面添加。當當前頁面(8KB大小)已經存滿時,則增加一個新的page進行記錄。
當PostgreSQL數據庫shutdown或者當進行checkpoint時,CLOG中的數據會被寫回到pg_clog目錄下。打開pg_clog目錄,我們會發現一些命名為0000,0001之類的文件。這些即為CLOG文件。這些文件的最大的size為256KB。也就是說,一個文件最多可以存儲256KB/8kB=32個CLOG。如果當前有8個CLOG,那么0000文件就可以存下來。而如果當前有35個CLOG,那么就需要0000,0001兩個文件來存儲,以此類推。
而當PostgreSQL數據庫啟動的時候,pg_clog文件夾下的CLOG記錄又會被讀到共享內存中。
2.2 事務快照(Transaction Snapshot)
終於要說到事務快照了。
事務快照我認為是一個很形象的詞,很容易從字面上理解它的意味。所謂"快照",就是拿着照相機"咔嚓"一聲記錄下當前瞬間的信息,在按下快門后所發生的變化我們統統無法察覺,即invisible。
同樣地,事務快照就是當某一個事務執行期間,記錄的某一個時間點上哪些事務是active,所謂active,就是說這個事務要么在執行中(in progress),要么還沒有開始。
PostgreSQL有內置的函數可以獲取當前的數據快照:txid_current_snapshot()函數
通過該函數可以獲取當前的快照,但是快照信息解讀起來需要費一點腦筋。
postgres=# SELECT txid_current_snapshot();
txid_current_snapshot
-----------------------
100:104:100,102
(1 row)
上面的數據快照是這樣一個數字序列構成: xmin:xmax:xip_list
下面我來一一介紹各個字段的意義。
- xmin
最早的active的事務的txid,即txid值最小的active事務;所有txid小於該值的事務,如果1.狀態為commited則為visible,2.狀態aborted則為dead,
- xmax
第一個還未分配的txid,所有txid大於該值的事務在快照生成時尚未開始,即不可見
- xip_list
快照生成時所有active的事務的txid。
對照上面的解釋,我們來兩個有代表性的例子吧:
- 對於(a):
txid小於100的事務都不是active的;
txid大於等於100的事務都是active的。
- 對於(b)
txid小於100的事務都不是active的;
txid大於等於104的事務都是active的;
txid為100和102都是active的,101和103都不是active的。
那么我們疑惑了,我們花這么多篇幅去判斷一個事務是不是active有什么意義呢?
下面來了。
在PostgreSQL中,當一個事務的隔離級別是READ COMMITTED時,在該事務中的每個命令執行之前都會重新獲取一次snapshot,而如果一個事務的隔離級別是REPEATABLE READ或者SERIALIZABLE時,該事務只在第一條命令執行時獲取snapshot。
記住以上的差別,正是以上的差別引起tuple的可見性的差別,從而實現不同的隔離級別。
還是來一張圖來解釋上面的話吧。
我們假設上面的三個事務依次執行,其中Transaction_A 和Transaction_B的隔離級別都是READ COMMITTED,Transaction_C的隔離級別是REPEATABLE READ。我們分時間段T1~T5來解釋:
T1
Transaction_A開始並執行第一條命令,此時獲取txid和snapshot。事務系統給Transaction_A分配txid為200,並獲取當前快照為 200:200:
T2
Transaction_B開始並執行第一條命令,此時獲取txid和snapshot。事務系統給Transaction_B分配txid為201,並獲取當前快照為 200:200:,因為Transaction_A正在執行中,所以Transaction_B無法看到Transaction_A中的修改。
T3
Transaction_C開始並執行第一條命令,此時獲取txid和snapshot。事務系統給Transaction_C分配txid為202,並獲取當前快照為 200:200:,因為Transaction_A正在執行中,所以Transaction_C無法看到Transaction_A和Transaction_B中的修改。
T4
Transaction_A進行了commit。事務管理系統刪除了Transaction_A的信息。
T5
Transaction_B和Transaction_C分別執行它們的SELECT命令。
此時,Transaction_B獲取一個新的snapshot(因為它的隔離級別是READ COMMITTED),該snapshot為 201:201:。因為Transaction_A的已經提交。Transaction_A對Transaction_B可見。
同時,由於Transaction_C的隔離級別是REPEATABLE READ,它仍然使用第一條命令執行時獲得的snapshot 200:200 ,因此Transaction_A和Transaction_B仍然對Transaction_C不可見。
2.3元組可見性規則
所謂"元組可見性(Tuple Visibility)"的規則就是利用:
1.tuple中的t_xmin和t_xmax字段;
2.clog
3.當前的snapshot
來判斷一個tuple對當前事務中的執行語句是可見還是不可見。所謂可見與不可見就是說當前命令在處理時是否要處理該tuple。
還是為了簡便起見,我們回避了子事務和有關t_ctid的問題。只討論最簡單的情形。
我們選取十條規則並將他們分為三類進行說明。
1. t_xmin的狀態為ABORTED
我們知道t_xmin是一個tuple被INSERT時的事務txid。如果該事務的狀態為ABORTED,說明該事務被取消,那么理所當然該事務所INSERT的tuple自然是無效並且是不可見的。所以Rule1即為:
Rule 1: If Status(Tuple.t_xmin) = ABORTED ⇒ Tuple is Invisible
2. t_xmin的狀態為IN_PROGRESS
如果一個tuple的t_xmin的狀態為IN_PROGRESS,那么很大可能它是不可見的。
因為:
如果這個tuple是其它事務(非當前事務)所插入的,那么這個tuple顯然是不可見的,因為這個tuple還未提交(postgreSQL不支持讀未提交)。
Rule 2: If Status(t_xmin) = IN_PROGRESS && t_xmin ≠ current_txid ⇒ Tuple is Invisible
如果這個tuple是當前事務提交的,並tuple的t_xmax值不是0,即該tuple是由當前事務插入,但是被當前事務UPDATE或者DELETE過了,因此,顯然是不可見的。
Rule 3: If Status(t_xmin) = IN_PROGRESS && t_xmin = current_txid && t_xmax ≠ INVAILD ⇒ Tuple is Invisible
反之,如果這個tuple是當前事務提交的,並tuple的t_xmax值是0,說明這個tuple是由當前事務插入並且並沒有被修改過,所以,它是可見的。
Rule 4: If Status(t_xmin) = IN_PROGRESS && t_xmin = current_txid && t_xmax = INVAILD ⇒ Tuple is Visible
3. t_xmin的狀態為COMMITTED
和上面的相反,如果一個tuple的t_xmin的狀態為COMMITTED,那么很大可能它是可見的。
先把規則列出來,后面再解釋。
Rule 5: If Status(t_xmin) = COMMITTED && Snapshot(t_xmin) = active ⇒ Tuple is Invisible
Rule 6: If Status(t_xmin) = COMMITTED && Snapshot(t_xmin) ≠ active && (t_xmax = INVALID || Status(t_xmax) = ABORTED) ⇒ Tuple is Visible
Rule 7: If Status(t_xmin) = COMMITTED && Status(t_xmax) = IN_PROGRESS && t_xmax = current_txid ⇒ Tuple is Invisible
Rule 8: If Status(t_xmin) = COMMITTED && Status(t_xmax) = IN_PROGRESS && t_xmax ≠ current_txid ⇒ Tuple is Visible
Rule 9: If Status(t_xmin) = COMMITTED && Status(t_xmax) = COMMITTED && Snapshot(t_xmax) = active ⇒ Tuple is Visible
Rule 10: If Status(t_xmin) = COMMITTED && Status(t_xmax) = COMMITTED && Snapshot(t_xmax) ≠ active ⇒ Tuple is Invisible
Rule5是比較顯然的,對於一個tuple,插入它的事務已經提交(COMMITED),並且該事務在當前的snapshot下是active的,說明該事務對當前事務中的命令來說是 in progress 或者 not yet started(忘記的話,看下2.2節的內容),故該事務插入的tuple對在當前為不可見;
Rule6,顯然,該tuple沒有被修改或者修改它的事務被abort了 = > 該tuple沒有被修改;插入該tuple的事務x在當前snapshot中是inactive(inactive說明事務x對於當前要執行的SQL命令來說要么被提交了,要么被abort了),所以可見;
Rule7,如果tuple被當前事務UPDATE或者DELETE了,自然這個tuple對於我們來說是舊版本了,不可見;
Rule8,和Rule7對比,這個tuple是被別的事務x修改(UPDATE或者DELETE)了,而且事務x沒有被提交(postgreSQL不支持讀未提交),所以修改后的元組對我們不可見,我們能看到的還是當前這個元組,所以當前tuple可見;
Rule9,雖然修改這個tuple的事務x已經提交了,但是事務x在當前snapshot中是active的,即對當前事務中的命令來說是 in progress 或者 not yet started(第二次用到這個假設了),所以事務x的修改對當前命令不可見,所以我們看到了還是這個tuple;
Rule10,和上一條相反,修改這個tuple的事務x已經提交了,並且事務x在當前snapshot中是inactive(inactive說明事務x對於當前要執行的SQL命令來說要么被提交了,要么被abort了)的,所以對當前事務中的命令來說,這個事務x已經提交,所以這個tuple對當前事務中的命令而言,已經是被修改過了,即是舊版本的了,所以即為不可見。
3.PostgreSQL中的MVCC
又交代了一些基礎知識,下面正式進入MVCC。
3.1 元組可見性檢測
這一節我們來談論PostgreSQL中的"元組可見性"的檢測。
所謂"元組可見性"的檢測就是利用元組可見性規則來判斷一個tuple對當前事務中的執行語句是可見還是不可見。我們知道在PostgreSQL中是tuple是多版本,那么對於一個事務中的命令來說,它需要找到對應事務中當前命令應該看到的那個版本的tuple進行處理。
通過"元組可見性"的檢測不僅僅可以幫助找到正確"版本"的tuple,同時還可以用來解決 ANSI SQL-92 標准中定義的異常:
臟讀;
不可重復讀;
幻讀
即,可以實現不同的事務隔離級別。
還是上例子吧:
簡化起見,txid=200的事務的隔離級別為READ COMMITTED,txid=201的事務的隔離級別我們分READ COMMITTED或者REPEATABLE READ兩種情況討論。
上圖中命令的執行順序如下:
T1 :txid=200的事務開始
T2 :txid=201的事務開始
T3 :txid=200和txid=201的事務分別執行SELECT命令
T4 :txid=200的事務執行UPDATE命令
T5 :txid=200和txid=201的事務分別執行SELECT命令
T6 :txid=200的事務commit
T7 :txid=201的事務執行SELECT命令
下面我們就來看看PostgreSQL是如何執行"元組可見性"檢測的。
T3 :
在T3時刻,當前表中只有Tuple_1,根據Rule6該tuple對所有事務可見;
T5 :
在T5時刻的情況有所不同,我們對兩個事務分開討論。
對於txid = 200的事務,此刻,我們可知Tuple_1是不可見的(根據Rule7),Tuple_2可見(根據Rule4);
因此,此時SELECT語句的返回結果為:
postgres=# -- txid 200
postgres=# SELECT * FROM tbl;
name
------
Hyde
(1 row)
對於txid = 201的事務,此刻,我們可知Tuple_1是不可見的(根據Rule8),Tuple_2同樣不可見(根據Rule2);
因此,此時SELECT語句的返回結果為:
postgres=# -- txid 201
postgres=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
我們可以看到,這里txid = 201的事務不會讀取txid = 200的事務的未提交的更新,也就是回避了臟讀問題。在PostgreSQL所有的事務隔離級別中都不會造成臟讀。
T7 :
在T7時刻,只有txid = 201的事務還在運行,txid = 200的事務已經提交。現在我們分兩種情況來討論txid = 201的事務的行為。
-
1)txid = 201的事務的隔離級別為READ COMMITTED
此時由於txid = 200的事務已經提交,因此,此時重新獲取的snapshot為 201:201: 。因此,我們可以知道Tuple_1是不可見的(根據Rule10),Tuple_2是可見的(根據Rule6),
因此,在READ COMMITTED級別下,SELECT語句的返回結果為:
postgres=# -- txid 201 (READ COMMITTED)
postgres=# SELECT * FROM tbl;
name
------
Hyde
(1 row)
我們可以看到,該事務在隔離級別為READ COMMITTED時,前后兩次相同的SELECT獲取的結果不同,也就是不可重復讀。
-
2)txid = 201的事務的隔離級別為REPEATABLE READ
此時雖然txid = 200的事務已經提交,但是我們知道在REPEATABLE READ/SERIALIZABLE時,事務只在執行第一條命令時獲取一次snapshot,因此,此時snapshot仍保持不變為 200:200: 。因此,我們可以知道Tuple_1是不可見的(根據Rule9),Tuple_2是可見的(根據Rule5),
因此,在READ COMMITTED級別下,SELECT語句的返回結果為:
postgres=# -- txid 201 (READ COMMITTED)
postgres=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
我們可以看到,事務在隔離級別為REPEATABLE READ時,前后兩次相同的SELECT獲取的結果不變,即回避了不可重復讀。
到這里,我們已經解決了解決了臟讀和不可重復讀的問題,那么還有一個幻讀。然而幻讀在PostgreSQL的事務在隔離級別為REPEATABLE READ時存在么?
我們知道,幻讀的定義是:一個事務重新執行一個返回符合一個搜索條件的行集合的查詢, 發現滿足條件的行集合因為另一個最近提交的事務而發生了改變。
顯然,在PostgreSQL中,由於快照隔離機制,我們繼續上面的分析就能發現:在REPEATABLE READ/SERIALIZABLE隔離級別時消除了幻讀(篇幅限制,我就不寫了),即在從REPEATABLE READ開始就回避了幻讀的問題,這和其它數據庫上不太一樣,PostgreSQL中提供的隔離級別更加嚴格。
關於這個系列,還剩下"如何避免Lost Update"和"SSI(Serializable Snapshot Isolation)"這兩個問題沒討論,篇幅太長了,並不是寫論文,容樓主喝口茶,留在下篇再說吧~