PG核心技术篇--MVCC

Posted 进击的CJR

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PG核心技术篇--MVCC相关的知识,希望对你有一定的参考价值。

为什么需要MVCC

在并发操作中,当正在写时,如果有用户在读,这时写可能只写了一半,如一行的前半部分刚写入,后半部分还没有写入,这时可能读的用户读取到的数据行的前半部分数据是新的,后半部分数据是原来的,这就导致了数据一致性问题。解决这个问题的最简单的方法是使用读写锁,写的时候不允许读,正在读的时候也不允许写,但这种方法会导致读和写的操作不能并发执行。于是,有人想到了一种能够让读写并发执行的方法,这种方法就是MVCC。

MVCC方法是写数据时,原数据并不删除,并发的读还能读到原数据,这样就不会有数据一致性问题了。


实现MVCC的方法

第一种:写新数据时,把原数据移到一个单独的位置,如回滚段中,其他用户读数据时,从回滚段中把原数据读出来。

第二种:写新数据时,原数据不删除,而是把新数据插入进来。

PostgreSQL数据库使用的是第二种方法,而Oracle数据库和mysql数据库中的InnoDB引擎使用的是第一种方法。


PG中MVCC的实现原理

前面讲过,PostgreSQL中的多版本实现是通过把原数据留在数据文件中,新插入一条数据来实现多版本的功能的。如上所述,每张表上都有4个系统字段“xmin”“xmax”“cmin”

“cmax”,这4个字段就是为多版本的功能而添加的。当两个事务同时访问记录时,通过参考xmin和xmax的标记判断记录的版本,根据版本号与自己当前的事务标识进行比较,确定自己的数据权限。当删除数据时,记录并没有从数据块中被删除,空间也没有立即释放。

PostgreSQL的多版本实现中首先要解决的是原数据的空间释放问题。PostgreSQL通过运行Vaccum进程来回收之前的存储空间,默认PostgreSQL数据库中的AutoVacuum是打开的,也就是说,当一个表的更新量达到一定值时,AutoVacuum自动回收空间。当然也可以关闭AutoVacuum进程,然后在业务低峰期手动运行VACUUM命令来回收空间。

在PostgreSQL中,若一个事务执行失败,在数据文件中该事务产生的数据并不会在事务回滚时被清理掉。为什么要这样做呢?为什么不在事务提交时把这些数据标记成有效,而在事务回滚时把这些数据标记成无效呢?这是出于效率的考虑。若事务提交或回滚时再次标记数据,那这些数据就有可能会被刷新到磁盘中,再次标记会导致另一次I/O,从而降低性能。那么如何知道这些数据是有效还是无效呢?PostgreSQL通过记录事务的状态来实现。数据行上记录了xmin和xmax,只需了解xmin和xmax对应的事务是成功提交还是回滚了,就可以知道这些数据行是否有效。PostgreSQL把事务状态记录在Commit Log中,简称CLOG,CLOG在数据目录的pg_clog子目录下,示例如下:

osdba@osdba-VirtualBox:~/pgdata$ ls -l pg_clog
total 8
-rw------- 1 osdba osdba 8192 Nov 30 21:43 0000

事务的状态有以下4种。

·TRANSACTION_STATUS_IN_PROGRESS=0x00:表示事务正在进行中。

·TRANSACTION_STATUS_COMMITTED=0x01:表示事务已提交。

·TRANSACTION_STATUS_ABORTED=0x02:表示事务已回滚。

·TRANSACTION_STATUS_SUB_COMMITTED=0x03:表示子事务已提交。

事务ID,在PostgreSQL中有时缩写为xid,是一个32bit的数字。有以下3个特殊的事务ID是给系统内部使用的,代表特殊的含义。

·InvalidTransactionId=0:表示是无效的事务ID。

·BootstrapTransactionId=1:表示系统表初使化时的事务ID。

·FrozenTransactionId=2:冻结的事务ID。

所以数据库系统第一个正常的事务ID是从3开始的,然后连续递增,达到最大值后,再从3开始。事务ID为0、1、2的始终保留。

通常,使用值为0的事务ID是为了让内部编程更为方便,当PostgreSQL内部的事务ID设置为0时,表示它是一个无效的事务ID。比如,使用函数GetCurrentTransactionIdIfAny查询当前的事务ID时,如果返回的事务ID为0,则表示当前还没有分配事务ID。

值为1的事务ID是Initdb服务初始化系统表时在表上填写的事务ID,此时数据库还没有启动,但在系统表中的cmin下也需要一个有效的事务ID,这个事务ID就为1,示例如下:

os dba=# select cmin, cmax, relname from pg_class where relname in (pg_type,pg_
attribute);
cmin | cmax | relname
------+------+--------------
1 | 1 | pg_type
1 | 1 | pg_attribute
(2 rows)


事务ID一直递增,总会到达4字节整数的最大值,到达最大值后再从头开始时,以前的事务ID都会比当前的事务ID大,在进行比较时,会认为以前的事务ID是将来的事务ID,这会导致严重的问题,即事务ID回卷的问题。另外,PostgreSQL中多版本实现中经常需要判断事务之间的新旧关系,例如:如果数据行中的已提交的事务比当前事务更早,则在当前事务中这行数据应该是可见的。在事务ID没有回卷时,简单比较两个事务ID的大小就可以知道事务之间的先后关系。如4294967290<4294967295,所以事务ID为4294967290的事务必然比事务ID4294967295的事务更早。但在事务ID回卷后,事务ID为5的事务应该比事务ID4294967295的事务更新,再简单地比较大小就行不通了。为了解决事务回卷问题和满足比较事务新旧的需求,PostgreSQL中规定,存在的最早和最新两个事务之间的年龄差最多是231,而不是无符号整数的最大范围232,只有该范围的一半,当要超过231时,就把旧的事务换成一个特殊的事务ID,也就是前面介绍的名为“FrozenTransactionId”的特殊事务。当正常事务ID与冻结事务ID进行比较时,会认为正常事务ID比冻结事务ID更新。做了以上的规定后,两个普通的事务ID比较新旧就可以使用如下公式:

((int32) (id1 - id2)) < 0

如果该公式的返回结果为真,则表明事务id1比事务id2更早。从这个公式中可以看出,当事务ID没有回卷时,上面的公式相当于直接比较大小,在事务ID回卷后,如id1=4294967295,id2=5,id1-id2=4294967290,这是一个正数,但转换成有符号的int32时,由于超出了有符号数的取值范围,会转换成一个负数,这样的结果对于事务ID回卷后的情况也适用。


PostgreSQL多版本的优劣分析

Oracle数据库和MySQL数据库的InnoDB引擎也都实现了多版本的功能,但它们与PostgreSQL的实现方式是不一样的,在这两个数据库中,旧版本的数据并不记录在原先的数据块中,而是被记录在回滚段中,如果要读取旧版本的数据,需要根据回滚段的数据重构旧版本数据。

PostgreSQL的多版本机制与Java虚拟机的垃圾回收机制比较相像。事务提交前,只需要访问原来的数据即可;提交后,系统更新元组的存储标识,直到Vaccum进程收回为止。

相对于InnoDB和Oracle,PostgreSQL的多版本的优势在于以下几点:

·事务回滚可以立即完成,无论事务进行了多少操作。

·数据可以进行很多更新,不必像Oracle和InnoDB那样需要经常保证回滚段不会被用完,也不会像Oracle数据库那样,经常遇到“ORA-1555”错误的困扰。

相对于InnoDB和Oracle,PostgreSQL的多版本的劣势在于以下几点:

·旧版本数据需要清理。PostgreSQL清理旧版本称为VACUUM,并提供了VACUUM命令进行清理。

·旧版本的数据会导致查询更慢一些,因为旧版本的数据存储于数据文件中,查询时需要扫描更多的数据块。




以上是关于PG核心技术篇--MVCC的主要内容,如果未能解决你的问题,请参考以下文章

从并发控制(MVCC)角度看deadtuple死数据的产生

PG-日常运维

数据库篇:mysql事务原理之MVCC视图+锁

react中的核心概念

核心面试题:MVCC间隙锁Undo Log链表级锁行级锁页级锁共享锁排它锁记录锁等等

如何解决IndexedDB在webkit内核下新旧版本的兼容问题