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是如何實現的spa

//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默認爲0code

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的結果ci

//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)能夠看到以前的版本信息,不一樣的事務擁有各自的數據空間,其操做不會對對方產生干擾,保證了事務的隔離性。it

提交事務,查看最終結果以下:io

//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命令操做來清理過時的數據。

相關文章
相關標籤/搜索