PostgreSQL如何实现MVCC (基于xmin、xmax、cmin、cmax)

    xiaoxiao2023-11-21  145

    声明:本文是《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值判断事务中插入的行记录对其他事务的可见性有两种情况

    (一)由回滚的事务或未提交的事务创建的行对于任何其他事务都是不可见的。 开启一个新的事务,如下所示:

    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)复制到新的磁盘文件中,并将原数据文件全部删除,并将未使用的磁盘空间还给操作系统,因此系统中其它进程可使用该空间,并且不会因此产生磁盘碎片。


    参考

    《PostgreSQL实战》http://www.jasongj.com/sql/mvcc/https://blog.csdn.net/qq_31156277/article/details/84310746
    最新回复(0)