面試官:PostgreSQL的MVCC是怎樣實(shí)現(xiàn)的?
為了解決讀寫沖突,數(shù)據(jù)庫提供了多種手段,比如表級(jí)和行級(jí)鎖。而為了進(jìn)一步提升并發(fā)性能,引入了 MVCC(多版本并發(fā)控制)。今天來聊一聊 PostgreSQL 的 MVCC 是怎樣實(shí)現(xiàn)的。
1.隔離級(jí)別
1.1 四類問題
使用數(shù)據(jù)庫時(shí),事務(wù)的并發(fā)常常會(huì)遇到四類問題:
- 臟讀:事務(wù) A 讀取了事務(wù) B 未提交的修改數(shù)據(jù)。如果事務(wù) B 回滾,事務(wù) A 讀取的數(shù)據(jù)就是無效的臟數(shù)據(jù)。
 - 不可重復(fù)讀:同一事務(wù)內(nèi)多次讀取同一行數(shù)據(jù),這條數(shù)據(jù)因?yàn)楸黄渌聞?wù)修改過并且已經(jīng)提交事務(wù),導(dǎo)致多次讀取到的結(jié)果不一致。
 - 幻讀:同一事務(wù)內(nèi)多次查詢同一范圍內(nèi)的數(shù)據(jù),因其他事務(wù)插入或刪除符合條件的數(shù)據(jù),導(dǎo)致事務(wù)在后面讀取到的結(jié)果集不一樣,像產(chǎn)生了幻覺。
 - 序列化異常:成功提交一個(gè)事務(wù),結(jié)果可能跟按照不同順序提交這組事務(wù)的結(jié)果不一致。
 
1.2 隔離級(jí)別
針對(duì)臟讀、幻讀、不可重復(fù)讀的問題,數(shù)據(jù)庫引入了 4 種標(biāo)準(zhǔn)隔離級(jí)別:
- 串行化(Serializable):事務(wù)對(duì)數(shù)據(jù)讀寫都是串行化的。
 - 可重復(fù)讀(Repeatable Read):事務(wù)執(zhí)行過程中,多次讀取同一行數(shù)據(jù),讀取結(jié)果一致。
 - 讀已提交數(shù)據(jù)(Read Committed):事務(wù)執(zhí)行過程中,如果有其他事務(wù)修改了數(shù)據(jù)并且提交事務(wù),當(dāng)前事務(wù)可以讀取到最新提交的數(shù)據(jù)。
 - 讀未提交數(shù)據(jù)(Read Uncommitted):事務(wù)執(zhí)行過程中,可以讀取到其他事務(wù)未提交的數(shù)據(jù)。
 
在多數(shù)數(shù)據(jù)庫中,我們可以使用四個(gè)標(biāo)準(zhǔn)事務(wù)隔離級(jí)別中的任何一個(gè)。 但其實(shí) PostgreSQL 內(nèi)部只實(shí)現(xiàn)了三個(gè)不同的隔離級(jí)別,PostgreSQL 的 Read Uncommitted 級(jí)別跟 Read Committed 類似。這是將標(biāo)準(zhǔn)隔離級(jí)別映射到 PostgreSQL 的 MVCC 架構(gòu)的唯一合理方法。如下表格:
Isolation Level  | Dirty Read  | Nonrepeatable Read  | Phantom Read  | Serialization Anomaly  | 
Read uncommitted  | ?(pg不允許)  | ?  | ?  | ?  | 
Read committed  | ?  | ?  | ?  | ?  | 
Repeatable read  | ?  | ?  | ?(pg不允許)  | ?  | 
Serializable  | ?  | ?  | ?  | ?  | 
這個(gè)表格也可以看出,在 Repeatable read 這個(gè)隔離級(jí)別下,PostgreSQL 是不允許出現(xiàn)幻讀的。這個(gè)也可以看出 PostgreSQL 的隔離級(jí)別比 SQL 標(biāo)準(zhǔn)有更高要求的保障。
PostgreSQL 的一些數(shù)據(jù)類型和函數(shù)對(duì)事務(wù)有特殊的規(guī)則,比如 sequence 的修改對(duì)其他事務(wù)立即可見,即使修改 sequence 的事務(wù)回滾,sequence 的修改也不能回滾。
2.MVCC 原理
我們知道,在 MySQL 中,InnoDB 采用聚簇索引保存數(shù)據(jù),MVCC 則通過數(shù)據(jù)的版本鏈來實(shí)現(xiàn),版本鏈中舊版本的數(shù)據(jù)保存在 Undo Log,數(shù)據(jù)表中只保留最新版本的數(shù)據(jù)。
PostgreSQL 采用堆表存儲(chǔ)數(shù)據(jù),MVCC 實(shí)現(xiàn)機(jī)制也有所不同。當(dāng)數(shù)據(jù)被修改時(shí),舊版本的數(shù)據(jù)仍然保存在數(shù)據(jù)表中,新版本的數(shù)據(jù)作為新的行插入。通過在每行數(shù)據(jù)中額外維護(hù)版本相關(guān)字段,實(shí)現(xiàn) MVCC。
2.1 存儲(chǔ)結(jié)構(gòu)
在 PostgreSQL 的堆表結(jié)構(gòu)中,存儲(chǔ)數(shù)據(jù)的單元是 tuple(元組)。我們可以把每個(gè) tuple 比作一行數(shù)據(jù)的一個(gè)版本,結(jié)構(gòu)如下:
圖片
在用戶數(shù)據(jù)之前,會(huì)增加 8 個(gè)字段,其中 null bitmap 和 object ID 是可選,就是說會(huì)固定有 23 個(gè)字節(jié)。
- t_xmin:最小事務(wù) id,也就是插入這一行數(shù)據(jù)的事務(wù) id。
 - t_xmax:最大事務(wù) id,也就是刪除這一行數(shù)據(jù)的事務(wù) id。
 - t_cid:CommandId,用于記錄插入或刪除操作是事務(wù)中的第幾條命令。
 - t_xvac:事務(wù) id,用來記錄 VACUUM 移動(dòng)行的版本。
 - t_ctid:數(shù)據(jù)指針,指向事務(wù)對(duì)此行數(shù)據(jù)修改的當(dāng)前版本或者更新的版本。結(jié)構(gòu)為(頁號(hào), 行偏移),例如 (0,1) 表示第 0 頁的第 1 行。
 - t_infomask2:表中字段數(shù)量和各種標(biāo)志位。
 - t_infomask:各種標(biāo)志位。
 - t_hoff:記錄用戶數(shù)據(jù)的偏移量。
 
- null bitmap:記錄表中的空值,只有 HEAP_HASNULL 標(biāo)識(shí)被設(shè)置在 t_infomask,null bitmap 才會(huì)有值。如果有值,則為表中每個(gè)字段分配一個(gè) bit(1 代表非空,0 代表空),也就是說這里定義的 bit 的數(shù)量等于 t_infomask2 定義的字段數(shù)量。如果 null bitmap 沒有值,則表中所有字段都是非空。
 - object ID:兼容老版本,記錄 OID(每個(gè)表、索引、視圖、函數(shù)、類型等都會(huì)被分配一個(gè)唯一的 OID)。如果 HEAP_HASOID_OLD 標(biāo)識(shí)被設(shè)置在 t_infomask,則 object ID 就會(huì)有值。
 
2.2 更新操作
下面我們看一下 PostgreSQL 存儲(chǔ)結(jié)構(gòu)的使用。首先我們執(zhí)行一條插入 SQL:
insert into tb_test(id, name) value(1, 'jam');這條 SQL 執(zhí)行的事務(wù) xid=10,執(zhí)行這個(gè)事務(wù)后,插入一條 tuple,記為 tuple1, tuple1 的值是 [t_xmin:10, t_xmax:0, t_cid:0, t_ctid: (0,1)]
- t_xmin:10,插入這個(gè) tuple 的事務(wù) xid=10。
 - t_xmax:0,這行數(shù)據(jù)未被刪除。
 - t_cid:0,事務(wù) xid=10 這個(gè)事務(wù)的第一個(gè) SQL 命令。
 - t_ctid:設(shè)置為 (0,1),指向自身,因?yàn)檫@是該 tuple 的最新版本。
 
接著,我們執(zhí)行第二個(gè)事務(wù) xid=11,里面包含一條更新語句。
update tb_test set name = 'tom' where id = 1;這條 SQL 更新這條數(shù)據(jù)的 name 為 tom,這時(shí)需要插入一條新的 tuple,記為 tuple2,tuple2 的值是 [t_xmin:11, t_xmax:0, t_cid:0, t_ctid: (0,2)]
- t_xmin:11,插入這個(gè) tuple 的事務(wù) xid=11。
 - t_xmax:0,這行數(shù)據(jù)未被刪除。
 - t_cid:0,事務(wù) id xid=11 這個(gè)事務(wù)的第 1 個(gè) SQL 命令。
 - t_ctid:設(shè)置為 (0,2),指向自身。
 
這時(shí) tuple1 會(huì)變成 [t_xmin:10, t_xmax:11, t_cid:0, t_ctid: (0,2)]
- t_xmax:11,這個(gè) tuple 被 xid=11 這個(gè)事務(wù)邏輯刪除。
 - t_ctid:設(shè)置為 (0,2),指向 tuple2。
 
接著再執(zhí)行第三個(gè)事務(wù) xid=12,里面包含一條刪除語句:
delete from tb_test where id = 1;這條 SQL 刪除了 id=1 的這條數(shù)據(jù),這時(shí) tuple2 變成 [t_xmin:11, t_xmax:12, t_cid:0, t_ctid: (0,2)]
- t_xmin:11,插入個(gè) tuple 的事務(wù) xid=11。
 - t_xmax:12,這行數(shù)據(jù)被 xid=12 這個(gè)事務(wù)刪除。
 - t_cid:0,事務(wù) id 等于 xid=12 這個(gè)事務(wù)的第 1 個(gè) SQL 命令。
 - t_ctid:(0,2),指向自身。
 
整個(gè)過程如下圖:
圖片
2.3 可見性
當(dāng)隔離級(jí)別是 Read committed 時(shí),判斷 tuple 對(duì)一個(gè)事務(wù) x 是否可見,需要滿足下面條件:
- 創(chuàng)建這個(gè) tuple 的事務(wù)在事務(wù) x 中查詢語句執(zhí)行之前已經(jīng)提交;
 - 刪除這個(gè) tuple 的事務(wù)在事務(wù) x 中查詢語句執(zhí)行之前未提交或者已經(jīng)回滾;
 - 事務(wù) x 創(chuàng)建的 tuple,未提交也能看到。
 
當(dāng)隔離級(jí)別是 Repeatable Read 時(shí),可見性需要滿足創(chuàng)建這個(gè) tuple 的事務(wù)在事務(wù) x 開始之前已經(jīng)提交。事務(wù) x 開始后,即使這個(gè) tuple 被其他事務(wù)修改或刪除,這些變化對(duì)事務(wù) x 也是不可見的。
3.總結(jié)
PostgreSQL 基于數(shù)據(jù)庫表存儲(chǔ)歷史版本的方式實(shí)現(xiàn)了 MVCC,這種實(shí)現(xiàn)方式的優(yōu)點(diǎn)是讀取歷史版本更加方便,不用像 MySQL 那樣依賴額外的 Undo Log 存儲(chǔ)。缺點(diǎn)是在高并發(fā)的場(chǎng)景下,數(shù)據(jù)庫表會(huì)保存大量的過期 tuple(dead tuples),可能會(huì)導(dǎo)致表膨脹。
PostgreSQL 使用 VACUUM 機(jī)制清理過期的 tuple 數(shù)據(jù),步驟包括識(shí)別過期 tuple、清理過期 tuple、標(biāo)記過期 tuple 空間可重用、更新系統(tǒng)統(tǒng)計(jì)信息。















 
 
 
















 
 
 
 