聲明:本文是《PostgreSQL實戰》讀書筆記,參考了http://www.jasongj.com/sql/mvcc/ 部分,可以參考該書事務與併發控制章節 和 http://www.jasongj.com/sql/mvcc/
PostgreSQL如何實現MVCC (基於xmin、xmax、cmin、xmax)
一、基於多版本的併發控制
在PostgreSQL中,會爲每一個事務分配一個遞增的、類型爲int32的整型數作爲唯一的一個ID,稱爲xid。可通過txid_current()
函數獲取當前事務的ID。PostgreSQL中,對於每一行數據(稱爲一個tuple),包含有4個隱藏字段,分別是xmin、xmax、cmin、xmax。這四個字段是隱藏的,但可直接訪問。創建一個快照時,將收集當前正在執行的事務id 和 已經提交的最大事務id, 根據快照信息,PostgreSQL可以確定事務的操作是否對執行語句是可見的 。
cmin
和 cmax
分別是插入和刪除該元組的命令在事務中的命令標識。( xmin
: 在創建(insert)記錄(tuple)時,記錄此值爲插入tuple的事務ID; xmax
: 默認值爲0.在刪除tuple時,記錄此值)
測試表準備:
create table tb_mvcc(
id int PRIMARY KEY,
ival int
);
insert into tb_mvcc values(1,1);
啓動psql
[root@instance-o5o8g5v0 ~]# su postgres
bash-4.2$ psql technology postgres
could not change directory to "/root"
psql (9.2.24, server 10.8)
WARNING: psql version 9.2, server version 10.0.
Some psql features might not work.
Type "help" for help.
technology=#
可以通過sql直接查詢四個值
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 1;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
630 | 0 | 0 | 0 | 1 | 1
(1 row)
二、 通過xmin決定事務的可見性
當插入一行數據時,PostgreSQL會將插入這行數據的事務的xid存儲在xmin中。通過xmin值判斷事務中插入的行記錄對其他事務的可見性有兩種情況
(一)由回滾的事務或未提交的事務創建的行對於任何其他事務都是不可見的。
開啓一個新的事務,如下所示:
technology=# begin;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
631
(1 row)
technology=# INSERT INTO tb_mvcc(id,ival) VALUES(2,2);
INSERT 0 1
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 2;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
631 | 0 | 0 | 0 | 2 | 2
(1 row)
SELECT txid_current()
;查詢當事務的xid
是631
。可以看到這條新數據的隱藏列xmin
值爲631
。
開啓另外一個事務,如下所示:
technology=# BEGIN;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
632
(1 row)
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 2;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
(0 rows)
technology=# END;
COMMIT
可以看見由於第一個事務並未提交,所以第一個事務對第二個事務是不可見的。
(二)無論提交成功或回滾的事務,xid 都會遞增,對於repeatable read 和 serializable 隔離級別的事務,如果它的xid 小於另外一個事務的xid 。也就是xmin小於另外一個事務的xmin,那麼另外一個事務對這個事務是不可見的。而read committed 則不會
注意在 read committed(對已提交): PostgreSQL的默認隔離級別,它滿足了一個事務只能看見已經提交事務對關聯數據所做的改變的隔離需求。 該隔離級別可能出現 不可重複讀 和 幻讀。 演示一下:
不可重複讀 : 當一個事務第一次讀取數據之後,被讀取的數據被另一個已經提交的事務進行了修改,事務再次讀取這些數據發現數據已經被另一個事務修改,兩次查詢的結果不一致,這種讀現象稱爲不可重複讀。
- 設置一個
read committed
對已提交隔離級別,
technology=# begin transaction isolation level read committed;
BEGIN
technology=# SELECT ival FROM tb_mvcc WHERE id = 1;
id | ival
----+------
1 | 1
(1 row)
- 另外一個事務對id=1 的進行修改,並
commit
technology=# BEGIN;
BEGIN
technology=# update tb_mvcc set ival = 11 where id = 1;
UPDATE 1
technology=# commit;
- 在從第一個事務進行讀取時,發現數據已經被修改,即在同一個事務中兩次讀取結果不一致。發現ival 被修改成了11
technology=# SELECT id,ival FROM tb_mvcc WHERE id = 1;
id | ival
----+------
1 | 11
(1 row)
完整信息如下所示:
technology=# begin transaction isolation level read committed;
BEGIN
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 1;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
630 | 0 | 0 | 0 | 1 | 1
(1 row)
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 1;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
635 | 0 | 0 | 0 | 1 | 11
(1 row)
technology=# END;
COMMIT
另外
read committed
還可能出現幻讀。
驗證一下,repeatable read
和 serializable
隔離級別的事務:
technology=# begin transaction isolation level repeatable read;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
636
(1 row)
上面語句開啓repeatable read重複讀隔離級別的一個事務,這個事務的xid是636。再開啓另外一個事務,如下所示:
technology=# BEGIN;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
637
(1 row)
technology=# INSERT INTO tb_mvcc(id,ival) VALUES(4,4);
INSERT 0 1
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 4;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
637 | 0 | 0 | 0 | 4 | 4
(1 row)
technology=# COMMIT;
COMMIT
第二個事務的xid 是637。並在第二個事務中插入一條數據,併成功commit。 然後再回到第一個事務中查詢第二個數據提交的數據。如下所示:
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 4;
xmin | xmax | cmin | xmax | id | ival
------+------+------+------+----+------
(0 rows)
由於第一個事務的xid小於第二個事務的xid。所以插入的數據在第一個事務中不可見。正好跟read committed 事務隔離級別形成對比
PostgreSQL 的事務隔離級別與讀現象的關係
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 | 序列化異常 |
---|---|---|---|---|
Read Uncommitted | 不可能 | 可能 | 可能 | 可能 |
Read Committed | 不可能 | 可能 | 可能 | 可能 |
Repeatable Read | 不可能 | 不可能 | 不可能 | 可能 |
Serializable | 不可能 | 不可能 | 不可能 | 不可能 |
postgresql內部將 Read uncommitted與Read Committed 設計成一樣。也就是postgresql數據庫中不會出現髒讀。(可能會出現不可重複讀和幻讀)。而postgresql的Repeatable Read 實現不允許幻讀。 這種隔離級別與其他數據庫定義隔離級別稍有不同。
三、通過xmax 決定事務的可見性
- 如果沒有設置xmax值,該行對其他事務總是可見的。
- 如果它被設置爲回滾事務的xid,該行對其他事務可見
- 如果它被設置成爲一個正在運行,滅有commit 和 rollback的事務xid。該行對其他事務是可見的。
- 如果它被設置爲一個已經提交的事務的xid。該行對在這個已提交的事務之後發起的所有事務都是不可見的。
四、通過pageinspect 觀察 MVCC
可以使用pageinspect 這個外部擴展來觀察數據庫頁面的內容。pageinspect 提供了一些函數可以得到數據庫的文件系統中頁面的詳細內容,使用之前先在數據庫中創建擴展:
technology=# create extension pageinspect;
CREATE EXTENSION
technology=# \dx+ pageinspect
Objects in extension "pageinspect"
Object Description
-------------------------------------------------------------------
function brin_metapage_info(bytea)
……
(26 rows)
創建如下視圖,爲了更清楚的觀察PostgreSQL的MVCC是如何控制併發時得多版本的。
DROP VIEW IF EXISTS v_pageinspect;
CREATE VIEW v_pageinspect AS (
SELECT '(0,' || lp || ')' AS ctid,
CASE lp_flags
WHEN 0 THEN 'unsed'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to ' || lp_off
WHEN 3 THEN 'dead'
END,
t_xmin::text::int8 AS xmin,
t_xmax::text::int8 AS xmax,
t_ctid
FROM heap_page_items(get_raw_page('tb_mvcc',0)))
ORDER BY lp;
對錶tb_mcc 清空數據操作: TRUNCATE TABLE tb_mvcc;
注意關閉所有的事務,否則會刪除失敗。別用delete,不然v_pageinspect 不能清除。
不考慮併發的情況:當insert 數據時,事務將insert 的數據的xmin值設置爲當前事務的xid,xmax設置爲0。
technology=# begin;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
648
(1 row)
technology=# INSERT INTO tb_mvcc(id,ival) values(1,1);
INSERT 0 1
technology=# SELECT * FROM v_pageinspect;
ctid | case | xmin | xmax | t_ctid
-------+--------+------+------+--------
(0,1) | normal | 648 | 0 | (0,1)
(1 row)
technology=# END;
COMMIT
在另外一個事務中,delete數據時,將xmax 的值設置爲當前事務的xid。 如所示:
technology=# BEGIN;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
649
(1 row)
technology=# DELETE FROM tb_mvcc WHERE id = 1;
DELETE 1
technology=# SELECT * FROM v_pageinspect;
ctid | case | xmin | xmax | t_ctid
-------+--------+------+------+--------
(0,1) | normal | 648 | 649 | (0,1)
(1 row)
當UPDATE 數據時,對於每個更新的行,首先DELTE原先行,再執行INSERT。如下所示:
INSERT INTO tb_mvcc(id,ival) values(2,2); -- 預先插入數據
technology=# BEGIN;
BEGIN
technology=# SELECT txid_current();
txid_current
--------------
661
(1 row)
-- 當前事務xid 661
technology=# SELECT * FROM tb_mvcc;
id | ival
----+------
2 | 2
(1 row)
technology=# SELECT * FROM v_pageinspect;
ctid | case | xmin | xmax | t_ctid
-------+--------+------+------+--------
(0,1) | normal | 660 | 0 | (0,1)
(1 row)
technology=# UPDATE tb_mvcc SET ival = 20 WHERE id = 2;
UPDATE 1
technology=# SELECT * FROM v_pageinspect;
ctid | case | xmin | xmax | t_ctid
-------+--------+------+------+--------
(0,1) | normal | 660 | 661 | (0,2)
(0,2) | normal | 661 | 0 | (0,2)
(2 rows)
通過pageinspect 查看page的內部,可以看見update 實際上是先delete 先前的數據(可以看前一個例子),再insert 一行新的數據。在數據庫中就存在兩個版本,一個是被update 之前的那條數據,另外一個是update之後被重新插入的那條數據。
五、其他
5.1 MVCC保證原子性
原子性(Atomicity)指得是一個事務是一個不可分割的工作單位,事務中包括的所有操作要麼都做,要麼都不做。
對於插入操作,PostgreSQL會將當前事務ID存於xmin中。對於刪除操作,其事務ID會存於xmax中。對於更新操作,PostgreSQL會將當前事務ID存於舊數據的xmax中,並存於新數據的xin中。換句話說,事務對增、刪和改所操作的數據上都留有其事務ID,可以很方便的提交該批操作或者完全撤銷操作,從而實現了事務的原子性。
5.2 PostgreSQL中的MVCC優勢
- 使用MVCC,讀操作不會阻塞寫,寫操作也不會阻塞讀,提高了併發訪問下的性能
- 事務的回滾可立即完成,無論事務進行了多少操作
- 數據可以進行大量更新,不像MySQL和Innodb引擎和Oracle那樣需要保證回滾段不會被耗盡
5.3 PostgreSQL中的MVCC缺點
事務ID個數有限制
事務ID由32位數保存,而事務ID遞增,當事務ID用完時,會出現wraparound問題。
PostgreSQL通過VACUUM機制來解決該問題。對於事務ID,PostgreSQL有三個事務ID有特殊意義:
- 0代表invalid事務號
- 1代表bootstrap事務號
- 2代表frozon事務。frozon transaction id比任何事務都要老
可用的有效最小事務ID爲3。VACUUM時將所有已提交的事務ID均設置爲2,即frozon。之後所有的事務都比frozon事務新,因此VACUUM之前的所有已提交的數據都對之後的事務可見。PostgreSQL通過這種方式實現了事務ID的循環利用。
5.4 大量過期數據佔用磁盤並降低查詢性能
由於上文提到的,PostgreSQL更新數據並非真正更改記錄值,而是通過將舊數據標記爲刪除,再插入新的數據來實現。對於更新或刪除頻繁的表,會累積大量過期數據,佔用大量磁盤,並且由於需要掃描更多數據,使得查詢性能降低。
PostgreSQL解決該問題的方式也是VACUUM機制
。從釋放磁盤的角度,VACUUM分爲兩種
VACUUM: 該操作並不要求獲得排它鎖,因此它可以和其它的讀寫表操作並行進行。同時它只是簡單的將dead tuple對應的磁盤空間標記爲可用狀態,新的數據可以重用這部分磁盤空間。但是這部分磁盤並不會被真正釋放,也即不會被交還給操作系統,因此不能被系統中其它程序所使用,並且可能會產生磁盤碎片。
VACUUM FULL :需要獲得排它鎖,它通過“標記-複製”的方式將所有有效數據(非dead tuple)複製到新的磁盤文件中,並將原數據文件全部刪除,並將未使用的磁盤空間還給操作系統,因此係統中其它進程可使用該空間,並且不會因此產生磁盤碎片。