PG中MVCC的实现原理
1.表中的系统字段
- oid:行对象标识符(对象ID)。该字段只有在创建表时使用了“with oids”或配置参数“default_with_oids”的值为true时才会存在
- tableoid:包含本行的表的oid。对父表进行查询时,会用到这个字段,通过该字段就可以知道某一行来自父表还是子表。tableoid可以和pg_class的oid字段连接起来获取表名。
- xmin:插入改行版本的事务ID
- xmax:删除此行时的事务ID,第一次插入时,此字段为0。如果该字段不为0,可能是删除这行的事务还没提交,或者是删除此行的事务回滚了,或者是还没有来得及vacuum
- cmin:事务内部插入类操作的命令ID,此标识是从1开始的
- cmax:事务内部删除类操作的命令ID,如果不是删除命令,此字段为0
- ctid:数据行在它所处的表内的物理位置
2.事务内部的多版本一致
xmin、xmax、cmin、cmax这四个字段在MVCC中用于控制数据行是否对用户可见。PG会将修改前后的数据都存储在相同的结构中
- 新插入一行时,将新插入行的xmin填写为当前事务ID,xmax填0
- 修改某一行时,实际上是新插入一行,旧行上的xmin不变,旧行上的xmax改为当前事务ID,新行上的xmin填为当前事务ID,新行上的xmax填为0
- 删除一行时,把被删除行上的xmax填为当前事务ID
cmin和cmax用于判断同一个事务内的不同命令导致的行版本变化是否可见,每个命令使用事务内一个命令标识计数器的值作为当前命令标识,事务开始时,命令标识计数器被置为0。执行一条DML后,命令标识计数器加1,当命令标识计数器不断累加又循环到0时,会报错,因为一个事务中命令的个数最多为2^32-1个,当某个事务内的命令读到某行数据时,会根据cmin和cmax做出判断:
- 若当前命令ID>=当前行的cmax且cmax不等于0,说明当前行对此命令不可见
- 若当前命令ID>=当前行的cmin,说明当前行对此命令可见
3.不同事务之间的多版本一致
PG是通过数据行上的xmin和xmax来判断对某事务是否可见,那么只需要确认xmin和xmax对应的事务是提交了还是回滚了,就可以知道这些数据行是否可见。PG把事务状态记录在commit log中,简称clog,事物的状态有以下四种:
- transaction_status_in_progress =0x00:表示事务正在进行中
- transaction_status_committed =0x01:表示事务已提交
- transaction_status_aborted =0x02:表示事务已回滚
transaction_status_sub_committed =0x03:表示子事务已提交
事务ID有时会缩写为XID,是一个32bit的数字。有三个特殊的事务ID是给系统内部使用的,如下:
invalidtransaction=0:表示是无效的事务ID
- bootstraptransactionid=1:表示系统表初始化时的事务ID
frozentransactionid=2:冻结的事务ID
由上可知,正常事务ID是从3开始的,然后不停递增,达到最大值后,再从3开始。事务ID 0、1、2的始终保留
若事务ID一直递增,总会到达4字节整数的最大值,达到最大值后再从头开始时,就会出现之前事务ID比当前事务ID大,而在比较时,就会任务以前事务ID是将来的事务ID,这自然会导致严重的问题,即事务ID回卷的问题。
在事务ID没有回卷时,简单比较两个事务ID的大小就知道事务之间的先后关系,但当事务ID回卷后,简单的比较大小就不行了。为了解决事务回卷和满足满足比较事务新旧的需求,在PG中规定,最旧和最新事务之间的年龄差最大为2^31,而不是无符号整数的最大范围2^32,当事务ID要超过2^31时,就把旧事务换成一个特殊的事务ID,也就是frozentransactionid的特殊事务。当正常事务ID与冻结事务ID比较时,会认为正常事务ID比冻结事务ID新。然后对于普通的事务比较新旧时就可以套用如下公式了:
((int32) (id1 - id2)) < 0