PostgreSQL如何实现MVCC (基于xmin、xmax、cmin、cmax)
聲明:本文是《PostgreSQL實(shí)戰(zhàn)》讀書筆記,參考了http://www.jasongj.com/sql/mvcc/ 部分,可以參考該書事務(wù)與并發(fā)控制章節(jié) 和 http://www.jasongj.com/sql/mvcc/
PostgreSQL如何實(shí)現(xiàn)MVCC (基于xmin、xmax、cmin、cmax)
一、基于多版本的并發(fā)控制
在PostgreSQL中,會(huì)為每一個(gè)事務(wù)分配一個(gè)遞增的、類型為int32的整型數(shù)作為唯一的一個(gè)ID,稱為xid。可通過txid_current()函數(shù)獲取當(dāng)前事務(wù)的ID。PostgreSQL中,對于每一行數(shù)據(jù)(稱為一個(gè)tuple),包含有4個(gè)隱藏字段,分別是xmin、xmax、cmin、cmax。這四個(gè)字段是隱藏的,但可直接訪問。創(chuàng)建一個(gè)快照時(shí),將收集當(dāng)前正在執(zhí)行的事務(wù)id 和 已經(jīng)提交的最大事務(wù)id, 根據(jù)快照信息,PostgreSQL可以確定事務(wù)的操作是否對執(zhí)行語句是可見的 。
cmin和 cmax 分別是插入和刪除該元組的命令在事務(wù)中的命令標(biāo)識。( xmin: 在創(chuàng)建(insert)記錄(tuple)時(shí),記錄此值為插入tuple的事務(wù)ID; xmax: 默認(rèn)值為0.在刪除tuple時(shí),記錄此值)
測試表準(zhǔn)備:
create table tb_mvcc(id int PRIMARY KEY,ival int ); insert into tb_mvcc values(1,1);啟動(dòng)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直接查詢四個(gè)值
technology=# SELECT xmin,xmax,cmin,cmax,id,ival FROM tb_mvcc WHERE id = 1;xmin | xmax | cmin | cmax | id | ival ------+------+------+------+----+------630 | 0 | 0 | 0 | 1 | 1 (1 row)二、 通過xmin決定事務(wù)的可見性
當(dāng)插入一行數(shù)據(jù)時(shí),PostgreSQL會(huì)將插入這行數(shù)據(jù)的事務(wù)的xid存儲(chǔ)在xmin中。通過xmin值判斷事務(wù)中插入的行記錄對其他事務(wù)的可見性有兩種情況
(一)由回滾的事務(wù)或未提交的事務(wù)創(chuàng)建的行對于任何其他事務(wù)都是不可見的。
開啟一個(gè)新的事務(wù),如下所示:
SELECT txid_current();查詢當(dāng)事務(wù)的xid是631。可以看到這條新數(shù)據(jù)的隱藏列xmin 值為631。
開啟另外一個(gè)事務(wù),如下所示:
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可以看見由于第一個(gè)事務(wù)并未提交,所以第一個(gè)事務(wù)對第二個(gè)事務(wù)是不可見的。
(二)無論提交成功或回滾的事務(wù),xid 都會(huì)遞增,對于repeatable read 和 serializable 隔離級別的事務(wù),如果它的xid 小于另外一個(gè)事務(wù)的xid 。也就是xmin小于另外一個(gè)事務(wù)的xmin,那么另外一個(gè)事務(wù)對這個(gè)事務(wù)是不可見的。而read committed 則不會(huì)
注意在 read committed(對已提交): PostgreSQL的默認(rèn)隔離級別,它滿足了一個(gè)事務(wù)只能看見已經(jīng)提交事務(wù)對關(guān)聯(lián)數(shù)據(jù)所做的改變的隔離需求。 該隔離級別可能出現(xiàn) 不可重復(fù)讀 和 幻讀。 演示一下:
不可重復(fù)讀 : 當(dāng)一個(gè)事務(wù)第一次讀取數(shù)據(jù)之后,被讀取的數(shù)據(jù)被另一個(gè)已經(jīng)提交的事務(wù)進(jìn)行了修改,事務(wù)再次讀取這些數(shù)據(jù)發(fā)現(xiàn)數(shù)據(jù)已經(jīng)被另一個(gè)事務(wù)修改,兩次查詢的結(jié)果不一致,這種讀現(xiàn)象稱為不可重復(fù)讀。
完整信息如下所示:
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 還可能出現(xiàn)幻讀。
驗(yàn)證一下,repeatable read 和 serializable 隔離級別的事務(wù):
technology=# begin transaction isolation level repeatable read; BEGIN technology=# SELECT txid_current();txid_current --------------636 (1 row)上面語句開啟repeatable read重復(fù)讀隔離級別的一個(gè)事務(wù),這個(gè)事務(wù)的xid是636。再開啟另外一個(gè)事務(wù),如下所示:
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第二個(gè)事務(wù)的xid 是637。并在第二個(gè)事務(wù)中插入一條數(shù)據(jù),并成功commit。 然后再回到第一個(gè)事務(wù)中查詢第二個(gè)數(shù)據(jù)提交的數(shù)據(jù)。如下所示:
technology=# SELECT xmin,xmax,cmin,xmax,id,ival FROM tb_mvcc WHERE id = 4;xmin | xmax | cmin | xmax | id | ival ------+------+------+------+----+------ (0 rows)由于第一個(gè)事務(wù)的xid小于第二個(gè)事務(wù)的xid。所以插入的數(shù)據(jù)在第一個(gè)事務(wù)中不可見。正好跟read committed 事務(wù)隔離級別形成對比
PostgreSQL 的事務(wù)隔離級別與讀現(xiàn)象的關(guān)系
| Read Uncommitted | 不可能 | 可能 | 可能 | 可能 |
| Read Committed | 不可能 | 可能 | 可能 | 可能 |
| Repeatable Read | 不可能 | 不可能 | 不可能 | 可能 |
| Serializable | 不可能 | 不可能 | 不可能 | 不可能 |
postgresql內(nèi)部將 Read uncommitted與Read Committed 設(shè)計(jì)成一樣。也就是postgresql數(shù)據(jù)庫中不會(huì)出現(xiàn)臟讀。(可能會(huì)出現(xiàn)不可重復(fù)讀和幻讀)。而postgresql的Repeatable Read 實(shí)現(xiàn)不允許幻讀。 這種隔離級別與其他數(shù)據(jù)庫定義隔離級別稍有不同。
三、通過xmax 決定事務(wù)的可見性
四、通過pageinspect 觀察 MVCC
可以使用pageinspect 這個(gè)外部擴(kuò)展來觀察數(shù)據(jù)庫頁面的內(nèi)容。pageinspect 提供了一些函數(shù)可以得到數(shù)據(jù)庫的文件系統(tǒng)中頁面的詳細(xì)內(nèi)容,使用之前先在數(shù)據(jù)庫中創(chuàng)建擴(kuò)展:
technology=# create extension pageinspect; CREATE EXTENSION technology=# \dx+ pageinspectObjects in extension "pageinspect"Object Description -------------------------------------------------------------------function brin_metapage_info(bytea) …… (26 rows)創(chuàng)建如下視圖,為了更清楚的觀察PostgreSQL的MVCC是如何控制并發(fā)時(shí)得多版本的。
DROP VIEW IF EXISTS v_pageinspect; CREATE VIEW v_pageinspect AS (SELECT '(0,' || lp || ')' AS ctid,CASE lp_flagsWHEN 0 THEN 'unsed'WHEN 1 THEN 'normal'WHEN 2 THEN 'redirect to ' || lp_offWHEN 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 清空數(shù)據(jù)操作: TRUNCATE TABLE tb_mvcc; 注意關(guān)閉所有的事務(wù),否則會(huì)刪除失敗。別用delete,不然v_pageinspect 不能清除。
不考慮并發(fā)的情況:當(dāng)insert 數(shù)據(jù)時(shí),事務(wù)將insert 的數(shù)據(jù)的xmin值設(shè)置為當(dāng)前事務(wù)的xid,xmax設(shè)置為0。
在另外一個(gè)事務(wù)中,delete數(shù)據(jù)時(shí),將xmax 的值設(shè)置為當(dāng)前事務(wù)的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)當(dāng)UPDATE 數(shù)據(jù)時(shí),對于每個(gè)更新的行,首先DELTE原先行,再執(zhí)行INSERT。如下所示:
INSERT INTO tb_mvcc(id,ival) values(2,2); -- 預(yù)先插入數(shù)據(jù) technology=# BEGIN; BEGIN technology=# SELECT txid_current();txid_current --------------661 (1 row) -- 當(dāng)前事務(wù)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的內(nèi)部,可以看見update 實(shí)際上是先delete 先前的數(shù)據(jù)(可以看前一個(gè)例子),再insert 一行新的數(shù)據(jù)。在數(shù)據(jù)庫中就存在兩個(gè)版本,一個(gè)是被update 之前的那條數(shù)據(jù),另外一個(gè)是update之后被重新插入的那條數(shù)據(jù)。
五、其他
5.1 MVCC保證原子性
原子性(Atomicity)指得是一個(gè)事務(wù)是一個(gè)不可分割的工作單位,事務(wù)中包括的所有操作要么都做,要么都不做。
對于插入操作,PostgreSQL會(huì)將當(dāng)前事務(wù)ID存于xmin中。對于刪除操作,其事務(wù)ID會(huì)存于xmax中。對于更新操作,PostgreSQL會(huì)將當(dāng)前事務(wù)ID存于舊數(shù)據(jù)的xmax中,并存于新數(shù)據(jù)的xin中。換句話說,事務(wù)對增、刪和改所操作的數(shù)據(jù)上都留有其事務(wù)ID,可以很方便的提交該批操作或者完全撤銷操作,從而實(shí)現(xiàn)了事務(wù)的原子性。
5.2 PostgreSQL中的MVCC優(yōu)勢
- 使用MVCC,讀操作不會(huì)阻塞寫,寫操作也不會(huì)阻塞讀,提高了并發(fā)訪問下的性能
- 事務(wù)的回滾可立即完成,無論事務(wù)進(jìn)行了多少操作
- 數(shù)據(jù)可以進(jìn)行大量更新,不像MySQL和Innodb引擎和Oracle那樣需要保證回滾段不會(huì)被耗盡
5.3 PostgreSQL中的MVCC缺點(diǎn)
事務(wù)ID個(gè)數(shù)有限制
事務(wù)ID由32位數(shù)保存,而事務(wù)ID遞增,當(dāng)事務(wù)ID用完時(shí),會(huì)出現(xiàn)wraparound問題。
PostgreSQL通過VACUUM機(jī)制來解決該問題。對于事務(wù)ID,PostgreSQL有三個(gè)事務(wù)ID有特殊意義:
- 0代表invalid事務(wù)號
- 1代表bootstrap事務(wù)號
- 2代表frozon事務(wù)。frozon transaction id比任何事務(wù)都要老
可用的有效最小事務(wù)ID為3。VACUUM時(shí)將所有已提交的事務(wù)ID均設(shè)置為2,即frozon。之后所有的事務(wù)都比frozon事務(wù)新,因此VACUUM之前的所有已提交的數(shù)據(jù)都對之后的事務(wù)可見。PostgreSQL通過這種方式實(shí)現(xiàn)了事務(wù)ID的循環(huán)利用。
5.4 大量過期數(shù)據(jù)占用磁盤并降低查詢性能
由于上文提到的,PostgreSQL更新數(shù)據(jù)并非真正更改記錄值,而是通過將舊數(shù)據(jù)標(biāo)記為刪除,再插入新的數(shù)據(jù)來實(shí)現(xiàn)。對于更新或刪除頻繁的表,會(huì)累積大量過期數(shù)據(jù),占用大量磁盤,并且由于需要掃描更多數(shù)據(jù),使得查詢性能降低。
PostgreSQL解決該問題的方式也是VACUUM機(jī)制。從釋放磁盤的角度,VACUUM分為兩種
VACUUM: 該操作并不要求獲得排它鎖,因此它可以和其它的讀寫表操作并行進(jìn)行。同時(shí)它只是簡單的將dead tuple對應(yīng)的磁盤空間標(biāo)記為可用狀態(tài),新的數(shù)據(jù)可以重用這部分磁盤空間。但是這部分磁盤并不會(huì)被真正釋放,也即不會(huì)被交還給操作系統(tǒng),因此不能被系統(tǒng)中其它程序所使用,并且可能會(huì)產(chǎn)生磁盤碎片。
VACUUM FULL :需要獲得排它鎖,它通過“標(biāo)記-復(fù)制”的方式將所有有效數(shù)據(jù)(非dead tuple)復(fù)制到新的磁盤文件中,并將原數(shù)據(jù)文件全部刪除,并將未使用的磁盤空間還給操作系統(tǒng),因此系統(tǒng)中其它進(jìn)程可使用該空間,并且不會(huì)因此產(chǎn)生磁盤碎片。
參考
- 《PostgreSQL實(shí)戰(zhàn)》
- http://www.jasongj.com/sql/mvcc/
- https://blog.csdn.net/qq_31156277/article/details/84310746
總結(jié)
以上是生活随笔為你收集整理的PostgreSQL如何实现MVCC (基于xmin、xmax、cmin、cmax)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CDN加速的作用以及APP被渗透入侵的解
- 下一篇: 我要做-微处理器嵌入式系统设计师