Postgresql杂谈 19—详解Postgresql中的多版本更新机制

       MVCC(Multi-Version Concurrency Conctrol,多版本控制并发),是数据库中一种常见的存在并发时保证数据一致性的方法。包括Oracle、Mysql、Postgresql在内许多主流数据库都有一套自己的MVCC方法实现。从实现逻辑上来讲,大体分为两种:

       第一种,就是Oracle和Mysql采用的,在更新数据时,把原来的数据放到回滚段中,如果有其它事务需要读取原来的数据,则需要从回滚段中进行读取。

       第二种,是Postgresql采用的,在更新数据时,原来的数据不删除,而是把更新的数据作为新数据插进来。

       本文主要介绍下,Postgresql这种MVCC方法的实现。在文章中,将对以下问题进行解答:

  • Postgresql怎么标记新数据和旧的数据
  • 旧的数据会怎么进行清理
  • 如何结合CommitLog文件来判断事务的状态
  • 什么是事务ID的回卷问题

一、Postgresql怎么标记新数据和旧的数据?

       为解决这个问题,Postgresql引入了两个伪列(数据表上隐藏的、系统自动添加的列)xmin和xmax。这两个伪列记录的都是事务ID,但是在不同的场景之下,记录的位置和数值不同。

       这里首先解释下什么是事务ID?Postgresql为每个事务都分配一个4个子节的数字作为这个事务的唯一性标识,这个标识就是事务ID。事务ID是全局的,起始值是3(稍后解释为什么起始值不是0或者1,而是3),每增加一个事务,它就会加1。当它达到最大值时,它又会回到3开始新一轮增长,这也叫做事务ID的回卷。

       在上文中提到,事务ID是从3开始增长的,是因为0、1、2都用来做了特殊的标记:

0:表示无效的事务ID;

1:初始化数据库时(pg_ctl initdb),用来做系统表(pg_class、pg_type等等)的事务ID;

2:冻结事务的ID,就是在事务ID回卷后,就旧的事务ID进行回收,标记为2;

接下来,笔者以实例的方式,来介绍在不同场景下,数据表中xmin和xmax的配置原则。

  • 在插入数据时,xmin存储当前事务的事务ID,而xmax为0.

       为了验证这个问题,我们开启一个事务,向t1表中插入一行数据:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# insert into t1 values (1,'tom',18);
INSERT 0 1

       在本事务内,执行下面的命令查询该行数据的xmin和xmax:

stock_analysis_data=# select xmin,xmax,* from t1;
  xmin  | xmax | id | name | age 
--------+------+----+------+-----
 353401 |    0 |  1 | tom  |  18

       可以用下面的命令查看到当前事务的ID:

stock_analysis_data=# SELECT CAST(txid_current() AS text);
 txid_current 
--------------
 353401
(1 row)

       这样,就可以确定对于新插入的数据,xmin写的就是当前事务的ID。如果本事务提交之后,另外启动一个终端查询t1表,也可以看到xmin和xmax数据和上面查询到的一致。

stock_analysis_data=# select xmin,xmax,* from t1;
  xmin  | xmax | id | name | age 
--------+------+----+------+-----
 353401 |    0 |  1 | tom  |  18
  • 在更新数据时,新数据行的xmin存储当前事务的事务ID,而xmax为0;原数据行xmin保持不变,xmax存储的是新的事务ID。

       开启一个事务,更新id为1的数据行:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set age=12 where id=1;
UPDATE 1

       在当前事务中查询该行的xmin和xmax:

stock_analysis_data=# select xmin,xmax,ctid,* from t1;
  xmin  | xmax | ctid  | id | name | age 
--------+------+-------+----+------+-----
 353403 |    0 | (0,2) |  1 | tom  |  12
(1 row)

stock_analysis_data=# SELECT CAST(txid_current() AS text);
 txid_current 
--------------
 353403
(1 row)

       可以看到,我们在当前事务中查询到的xmin已经变成了当前事务的ID。接下来,在另外的事务中在进行上述查询:

stock_analysis_data=# select xmin,xmax,ctid,* from t1;
  xmin  |  xmax  | ctid  | id | name | age 
--------+--------+-------+----+------+-----
 353401 | 353403 | (0,1) |  1 | tom  |  18
(1 row)

       细心的朋友可能已经发现,我们这两次的查询的列中包含了列的ctid,而且两次查询ctid的值不一致,这就说明了Postgresql在更新数据时是插入新的数据行,而非在原来的数据行上修改。再来看本次查询出来的xmin和xmax,xmin还是原来的值,而xmax由0变成了是正在修改改行数据的新的事务ID。

       如果我们提交进行的修改,再进行查询,会发现已经查询不到ctid为(0,2)的数据行,而查询出来的数据行的ctid为(0,2),是最新的那一行。

  • 在删除一行时,将原来行的xmax改成删除事务ID

       还是开启一个事务,在事务中删除id为1的数据行。

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# delete from t1 where id=1;
DELETE 1

       此时在当前事务中是无法查询到已经删除的改行数据了,因为没有提交,可以再开启一个事务进行查询:

stock_analysis_data=# select xmin,xmax,ctid,* from t1;
  xmin  |  xmax  | ctid  | id | name | age 
--------+--------+-------+----+------+-----
 353403 | 353404 | (0,2) |  1 | tom  |  12
(1 row)

       发现数据行的xmax已经变成了353404,这正是删除事务的事务ID。

       至此,我们可以得出结论:

(1)插入和更新数据时,数据行的xmin将最终存储相关事务的ID,而xmax将为0;

(2)当数据行的xmax不为0时,说明改行是要被删除的行。

二、Postgresql旧数据的删除?

       在上文中,我们已经知道xmax被标记为事务ID的数据行是要被删除的行,由此带来了两个问题:

(1)一个是被标记为要删除的行怎么去删除,由谁去删除?

(2)在执行select查询语句时,如果表中存在xmax不为0的数据说明有事务在修改或者删除该表,那么select怎么知道事务有没有提交,从而确定该行数据显不显示?

       现在,我们先来说明下第一个问题。Postgresql中使用autovacuum进程进行这些存储空间的回收,回收的触发时机是:当表的更新量到达一定的规模时触发。AutoVacuum是否打开在postgresql.conf文件中配置,默认是打开的,如果我们关闭了也可以使用VACUUM命令手动进行回收,如下所示:

stock_analysis_data=# VACUUM t1; 
VACUUM

       对表t1进行VACUUM时,只是单纯的回收空间已被重用,被回收的空间不会返还给操作系统,而是仍然保留在原来的表中以进行重用。

       与VACUUM命令相近的命令是VACUUM FULL命令,它会将整个表的内容重新写到一个磁盘文件中,类似于Java的JVM垃圾回收的复制算法,此时被整理后的数据行的ctid都会改变,特别是,VACUUM FULL命令还会进行锁表。

       接下来,我们演示下VACUUM FULL的用法。首先修改t1表里面的数据并进行提交:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set age=18;
UPDATE 2
stock_analysis_data=# commit;
COMMIT

       然后查看t1表里面数据的ctid:

stock_analysis_data=# select xmin,xmax,ctid,* from t1;
  xmin  | xmax | ctid  | id | name  | age 
--------+------+-------+----+-------+-----
 353409 |    0 | (0,3) |  1 | tom   |  18
 353409 |    0 | (0,4) |  2 | jerry |  18
(2 rows)

       可以看到,两条数据的ctid分别是(0,3)和(0,4),然后执行VACUUM FULL命令,再去查询,发现两条数据的ctid变成了(0,1)和(0,2):

stock_analysis_data=# VACUUM FULL t1;
VACUUM
stock_analysis_data=# select xmin,xmax,ctid,* from t1;
  xmin  | xmax | ctid  | id | name  | age 
--------+------+-------+----+-------+-----
 353409 |    0 | (0,1) |  1 | tom   |  18
 353409 |    0 | (0,2) |  2 | jerry |  18
(2 rows)

三、如何结合CommitLog文件来判断事务的状态

       在上文中提到,使用多版本更新数据带来的第二个问题就是:怎么在select时,准确判断出数据行xmax记录的事务ID对应的事务的状态,是提交了,还是未提交?进而影响查询的结果集。实际上,Postgresql会将事务的状态写到CommitLog文件中,而执行select时,正是根据这个文件中记录的事务状态来判断数据行是否有效。

       CommitLog文件,在Postgresql 9.X之前是存放到pg_clog目录下的,而在Postgresql10之后,该文件存放到了pg_xact目录下。

[root@VM-115-39-centos pg_xact]# pwd
/var/lib/pgsql/11/data/pg_xact
[root@VM-115-39-centos pg_xact]# ls -l
total 92
-rw------- 1 postgres postgres 90112 Jul  7 14:44 0000

       我们看到的这个名为0000的文件就是Postgresql的CommitLog,它实际上是一个位图文件。用两位表示一个事务的状态:

  • 0x00:表示事务正在进行中。
  • 0x01:表示事务已经提交
  • 0x02:表示事务已经回滚
  • 0x03:表示子事务已经提交

       上文中提到Postgresql的事务ID其实是一个连续增长的四个子节的数字,而CommitLog是一个位图,那么CommitLog每2位划分出来一个表示事务状态,事务ID就是这个位图的索引:

  为了做到快速的通过判断出事务的状态,Postgresql也做了一系列的优化,这就包括将CommitLog全部缓存到内存中,在数据行上使用t_infomask标志字段表示事务提交状态(第一次查询数据行时,根据CommitLog记录的状态更新t_infomask标志)等等,总之Postgresql通过CommitLog日志和上述优化手段,保证了在多版本更新机制下,数据的查询效率。

四、事务ID的回卷

       最后一个问题是事务ID回卷的问题,上文已经提到,Postgresql的事务ID是1个连续增长的四字节整数,那么它势必有一个最大值。当达到最大值之后,它又会回到初始值,重新开始增长,这就是事务ID的回卷。

       在回卷的过程中,必然会出现事务ID重新利用的问题,Postgresql的方法就是对旧的事务ID一律设置成冻结ID2。比如说新开启一个事务,为它分配事务ID为1000,而原来的数据中有部分数据xmin的值是1000,此时会把这部分数据的xmin统一设置成2,这样就实现对事务ID 1000的回收和再利用。

       实际上旧的数据ID的回收工作也是由VACUUM进程进行的,如果旧的事务ID没有来得及回收,当新事务ID和旧的事务ID距离还有1千万时,数据库会在日志中报警。如果这个距离还有1百万时,数据库会直接宕机,并再日志文件中报错。我们应该特别留意数据库事务ID回卷的问题,否则很可能引起数据库宕机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值