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


































