PostgreSQL如何实现MVCC (基于xmin、xmax、cmin、cmax)
聲明:本文是《PostgreSQL實戰》讀書筆記,參考了http://www.jasongj.com/sql/mvcc/ 部分,可以參考該書事務與并發控制章節 和 http://www.jasongj.com/sql/mvcc/
PostgreSQL如何實現MVCC (基于xmin、xmax、cmin、cmax)
一、基于多版本的并發控制
在PostgreSQL中,會為每一個事務分配一個遞增的、類型為int32的整型數作為唯一的一個ID,稱為xid。可通過txid_current()函數獲取當前事務的ID。PostgreSQL中,對于每一行數據(稱為一個tuple),包含有4個隱藏字段,分別是xmin、xmax、cmin、cmax。這四個字段是隱藏的,但可直接訪問。創建一個快照時,將收集當前正在執行的事務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,cmax,id,ival FROM tb_mvcc WHERE id = 1;xmin | xmax | cmin | cmax | id | ival ------+------+------+------+----+------630 | 0 | 0 | 0 | 1 | 1 (1 row)二、 通過xmin決定事務的可見性
當插入一行數據時,PostgreSQL會將插入這行數據的事務的xid存儲在xmin中。通過xmin值判斷事務中插入的行記錄對其他事務的可見性有兩種情況
(一)由回滾的事務或未提交的事務創建的行對于任何其他事務都是不可見的。
開啟一個新的事務,如下所示:
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的默認隔離級別,它滿足了一個事務只能看見已經提交事務對關聯數據所做的改變的隔離需求。 該隔離級別可能出現 不可重復讀 和 幻讀。 演示一下:
不可重復讀 : 當一個事務第一次讀取數據之后,被讀取的數據被另一個已經提交的事務進行了修改,事務再次讀取這些數據發現數據已經被另一個事務修改,兩次查詢的結果不一致,這種讀現象稱為不可重復讀。
完整信息如下所示:
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 決定事務的可見性
四、通過pageinspect 觀察 MVCC
可以使用pageinspect 這個外部擴展來觀察數據庫頁面的內容。pageinspect 提供了一些函數可以得到數據庫的文件系統中頁面的詳細內容,使用之前先在數據庫中創建擴展:
technology=# create extension pageinspect; CREATE EXTENSION technology=# \dx+ pageinspectObjects 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_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 清空數據操作: TRUNCATE TABLE tb_mvcc; 注意關閉所有的事務,否則會刪除失敗。別用delete,不然v_pageinspect 不能清除。
不考慮并發的情況:當insert 數據時,事務將insert 的數據的xmin值設置為當前事務的xid,xmax設置為0。
在另外一個事務中,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)復制到新的磁盤文件中,并將原數據文件全部刪除,并將未使用的磁盤空間還給操作系統,因此系統中其它進程可使用該空間,并且不會因此產生磁盤碎片。
參考
- 《PostgreSQL實戰》
- http://www.jasongj.com/sql/mvcc/
- https://blog.csdn.net/qq_31156277/article/details/84310746
總結
以上是生活随笔為你收集整理的PostgreSQL如何实现MVCC (基于xmin、xmax、cmin、cmax)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CDN加速的作用以及APP被渗透入侵的解
- 下一篇: mysql如何高效存储IPv4、IPv6