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是如何实现的

//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默认为0

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的结果

//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)可以看到之前的版本信息,不同的事务拥有各自的数据空间,其操作不会对对方产生干扰,保证了事务的隔离性。

提交事务,查看最终结果如下:

//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命令操作来清理过期的数据。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据和云

【常见错误分析】ORA-01555错误解决一例

ORA-01555错误是一种在Oracle数据库中很常见的错误。尤其在Oracle 8i及之前的版本最多。从9i开始的undo自动管理,至现在的10g、11g中...

2826
来自专栏chenssy

【死磕Sharding-jdbc】---结果合并总结

这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能可想而知。而在分库分表的情况下(假设分为2个库),为了保证数...

933
来自专栏乐沙弥的世界

参数CONTROL_FILE_RECORD_KEEP_TIME和MAXLOGHISOTRY

--**************************************************

763
来自专栏杨建荣的学习笔记

MySQL句柄恢复文件的简单尝试

今天突然想起一个问题,那就是对于ibdata的恢复,如果我们简单模拟一下,就会发现还是蛮有意思的。 首先我们得到两个参数值,一个是刷脏页的指标,另外一个是数据文...

2808
来自专栏乐沙弥的世界

db_block_checking与db_block_checksum

    db_block_checking与db_block_checksum两个参数都是对block进行检查,然而两者很容易混淆。事实上,两个参数中前者是对块...

723
来自专栏腾讯云数据库团队的专栏

MySQL 语句复制(SBR)的缺陷列举

MySQL ( 这里的 MySQL 是指广义的mysql,包括oracle,mysql,percona,mariadb 等 )的Statement Based ...

4840
来自专栏数据之美

MySQL 死锁与日志二三事

最近线上 MySQL 接连发生了几起数据异常,都是在凌晨爆发,由于业务场景属于典型的数据仓库型应用,白天压力较小无法复现。甚至有些异常还比较诡异,最后 root...

2696
来自专栏沃趣科技

Oracle Real Time SQL Monitoring

术语说明 TableQueue,消息缓冲区,在并行操作中使用,用于PX进程之间的通信,或者PX进程与QC进程之间的通信,是内存中的一些page,每个消息缓冲区的...

4068
来自专栏乐沙弥的世界

Oracle 实例恢复

Oracle实例失败多为实例非一致性关闭所致,通常称为崩溃(crash)。实例失败的结果等同于shutdown abort。

805
来自专栏任浩强的运维生涯

mongodb 性能篇

一、  索引及其优化 索引的概述 数据库的索引好比是一本书前面的目录,能加快数据查询的速度。 适当的地方增加索引,不合理的地方删除次优索引,能优化性能较差的应用...

39110

扫码关注云+社区