PostgreSQL 的 MVCC 機制解析
導語
PostgreSQL是通過MVCC(Multi-Version Concurrency Control)來保證事務的原子性和隔離性,具體MVCC機制是怎樣實現的,下面舉些示例來做個簡單解析以加深理解。
前提
表中隱藏的系統字段
PostgreSQL的每個表中都有些系統隱藏字段,包括:
- oid: 對象標識符,生成的值是全局唯一的,表、索引、視圖都帶有oid,如果需要在用戶創建的表中使用oid字段,需要顯示指定“with oids”選項。
- ctid: 每條記錄(稱為一個tuple)在表中的物理位置標識。
- xmin: 創建一條記錄(tuple)時,記錄此值為當前事務ID。
- xmax: 創建tuple時,默認為0,刪除tuple時,記錄此值為當前事務ID。
- cmin/cmax: 標識在同一個事務中多個語句命令的序列值,從0開始,用于同一個事務中實現版本可見性判斷
MVCC機制
MVCC機制通過這些隱藏的標記字段來協同實現,下面舉幾個示例來解釋MVCC是如何實現的
- //seesion1:
- 創建表,顯示指定oid字段:
- testdb=# create table t1(id int) with oids;
- CREATE TABLE
- 插入幾條記錄
- testdb=# insert into t1 values(1);
- INSERT 17569 1
- testdb=# insert into t1 values(2);
- INSERT 17570 1
- testdb=# insert into t1 values(3);
- INSERT 17571 1
查詢當前表中的tuple信息,xmin為創建tuple時的事務ID,xmax默認為0
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+------+------+------+-------+----
- (0,1) | 80853357 | 0 | 0 | 0 | 17569 | 1
- (0,2) | 80853358 | 0 | 0 | 0 | 17570 | 2
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (3 rows)
接下來,我們更新某個tuple的字段,將tuple中id值為1更新為4,看看會發生什么
- testdb=# begin;
- BEGIN
- testdb=# select txid_current();
- txid_current
- --------------
- 80853360
- (1 row)
- testdb=# update t1 set id = 4 where id = 1;
- UPDATE 1
查看tuple詳細信息
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+------+------+------+-------+----
- (0,2) | 80853358 | 0 | 0 | 0 | 17570 | 2
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (0,4) | 80853360 | 0 | 0 | 0 | 17569 | 4
- (3 rows)
可以看到id為1的tuple(oid=17569)已經被修改了,id值被更新為4,另外ctid、xmin字段也被更新了,ctid值代表了該tuple的物理位置,xmin值是創建tuple時都已經寫入,這兩個字段都不應該被更改才對,另起一個seesion來看下(當前事務還未提交)
- //seesion2:
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+----------+------+------+-------+----
- (0,1) | 80853357 | 80853360 | 0 | 0 | 17569 | 1
- (0,2) | 80853358 | 0 | 0 | 0 | 17570 | 2
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (3 rows)
可以看到id為1的tuple(oid=17569)還存在,只是xmax值被標記為當前事務Id。 原來更新某個tuple時,會新增一個tuple,填入更新后的字段值,將原來的tuple標記為刪除(設置xmax為當前事務Id)。同理,可以看下刪除一個tuple的結果
- //seesion1:
- testdb=# delete from t1 where id = 2;
- DELETE 1
- //seesion2:
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+----------+------+------+-------+----
- (0,1) | 80853357 | 80853360 | 0 | 0 | 17569 | 1
- (0,2) | 80853358 | 80853360 | 1 | 1 | 17570 | 2
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (3 rows)
刪除某個tuple時也是將xmax標記為當前事務Id,并不做實際的物理記錄清除操作。另外cmin和cmax值遞增為1,表明了同一事務中操作的順序性。在該事務(seesion1)未提交前,其他事務(seesion2)可以看到之前的版本信息,不同的事務擁有各自的數據空間,其操作不會對對方產生干擾,保證了事務的隔離性。
提交事務,查看最終結果如下:
- //seesion1:
- testdb=# commit;
- COMMIT
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+------+------+------+-------+----
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (0,4) | 80853360 | 0 | 0 | 0 | 17569 | 4
- (2 rows)
但是,如果我們不提交事務而是回滾,結果又是如何?
- testdb=# begin ;
- BEGIN
- testdb=# update t1 set id = 5 where id = 4;
- UPDATE 1
- testdb=# rollback;
- ROLLBACK
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+----------+------+------+-------+----
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (0,4) | 80853360 | 80853361 | 0 | 0 | 17569 | 4
- (2 rows)
- xmax標記并未清除,繼續新增一條記錄:
- testdb=# insert into t1 values(5);
- INSERT 17572 1
- testdb=# select ctid, xmin, xmax, cmin, cmax, oid, id from t1;
- ctid | xmin | xmax | cmin | cmax | oid | id
- -------+----------+----------+------+------+-------+----
- (0,3) | 80853359 | 0 | 0 | 0 | 17571 | 3
- (0,4) | 80853360 | 80853361 | 0 | 0 | 17569 | 4
- (0,6) | 80853362 | 0 | 0 | 0 | 17572 | 5
- (3 rows)
發現沒有清理掉新增的tuple,消除原有tuple上的xmax標記,這是為何?處于效率的原因,如果事務回滾時也進行清除標記,可能會導致磁盤IO,降低性能。那如何判斷該tuple的是否有效呢?答案是PostgreSQL會把事務狀態記錄到clog(commit log)位圖文件中,每讀到一行時,會到該文件中查詢事務狀態,事務的狀態通過以下四種來表示:
- #define TRANSACTION_STATUS_IN_PROGRESS=0x00 正在進行中
- #define TRANSACTION_STATUS_COMMITTED=0x01 已提交
- #define TRANSACTION_STATUS_COMMITTED=0x02 已回滾
- #define TRANSACTION_STATUS_SUB_COMMITTED=0x03 子事務已提交
MVCC保證原子性和隔離性
原子性
事務的原子性(Atomicity)要求在同一事務中的所有操作要么都做,要么都不做。根據PostgreSQL的MVCC規則,插入數據時,會將當前事務ID寫入到xmin中,刪除數據時,會將事務ID寫入xmax中,更新數據相當于先刪除原來的tuple再新增一個tuple,增刪改操作都保留了事務ID,根據事務ID提交或撤銷該事務中的所有操作,從而保證了事務的原子性。
隔離性
事務的隔離性(Isolation)要求各個并行事務之間不能相互干擾,事務之間是隔離的。PostgreSQL可讀取的數據是xmin小于當前的事務ID且已經提交。對某個tuple進行更新或刪除時,其他事務讀取的就是這個tuple之前的版本。
MVCC的優勢
讀寫不會相互阻塞,寫操作并沒有堵塞其他事務的讀,在寫事務未提交前,讀取的都是之前的版本,提高了并發的訪問效率。
事務可以快速回滾,操作后的tuple都帶有當前事務ID,直接標記clog文件中對應事務的狀態就可達到回滾的目的。
MVCC帶來的問題
事務ID回卷問題
PostgreSQL也需要事務ID來確定事務的先后順序,PostgreSQL中,事務被稱為XID,獲取當前XID:
- testdb=# select txid_current();
- txid_current
- --------------
- 80853335
- (1 row)
事務ID由32bit數字表示,當事務ID用完時,就會出現新的事務ID會比老ID小,導致事務ID回卷問題(Transaction
ID Wraparound)。 PostgreSQL的事務ID規則:
- 0: InvalidXID,無效事務ID
- 1: BootstrapXID,表示系統表初使化時的事務
- 2: FrozenXID,凍結的事務ID,比任務普通的事務ID都舊。
– 大于2的事務ID都是普通的事務ID。
當***和最舊事務之差達到2^31時,就把舊事務換成FrozenXID,然后通過公式((int32)(id1 - id2)) < 0比較大小即可
垃圾數據問題
根據MVCC機制,更新和刪除的記錄都不會被實際刪除,操作頻繁的表會積累大量的過期數據,占用磁盤空間,當掃描查詢數據時,需要更多的IO,降低查詢效率。PostgreSQL的解決方法是提供vacuum命令操作來清理過期的數據。
原文鏈接:https://www.qcloud.com/community/article/528634,作者:黃輝
【本文是51CTO專欄作者“騰訊云技術社區”的原創稿件,轉載請通過51CTO聯系原作者獲取授權】