tuple表示表中的数据行,在MySQL中用row表示。
在表数据页中,主要分为普通的数据元祖和TOAST元祖。以下是一个普通数据元祖的结构,主要由三部分组成:HeapTupleHeaderData结构、NULL bitmap、user data。
t_xmin : 当一个事务插入一条新的数据行时,将该数据行的xmin标识为当前事务的事务IDt_xmax : 新插入的数据行的xmax默认为0,当一个事务删除一条数据行时,将该数据行的xmax标识为当前事务的事务ID,相当于做了一个删除标记。t_cid : 在同一事务内,当该命令操作导致会插入/删除操作时,使用t_cid进行计数t_ctid : ctid标识了一个数据行tuple在表中的物理位置,ctid可以快速定位数据行,但是随着表数据的变更,在发生vacuum full之后,数据行对应的物理位置可能会发生改变。1、数据库中查看tuple的ctid
## 查看当前数据行对应的ctid db1=# select ctid,* from t1; ctid | id -------+---- (0,1) | 1 (0,2) | 1 (0,3) | 1 (0,4) | 2 (0,5) | 2 (0,6) | 3 (0,7) | 3 (0,8) | 3 (8 rows)2、数据库中查看tuple的xmin、xmax、ctid
db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id | name ---------+------+------+------+----+------ 1738613 | 0 | 0 | 0 | 1 | aa 1738614 | 0 | 0 | 0 | 2 | bb 1738615 | 0 | 0 | 0 | 3 | cc (3 rows)当每个事务开始时,事务管理器都会分配一个唯一的标识符,也就是事务ID(txid), postgresql的txid是一个32位无符号整数,大约42亿。在数据库中可通过内置函数txid_current()查看当前事务ID号。
不会为BEGIN命令分配txid。在PG中,当BEGIN命令后,执行第一个命令时,事务管理器会分配tixd,然后启动其事务。
db1=# begin; BEGIN db1=# SELECT txid_current(); txid_current -------------- 1738649 (1 row)postgres保留了以下三个特殊的事务ID号:
0 :无效事务ID。1 :系统表初始化时事务ID2 :冻结的事务IDtxid之间可以做比较,小于当前txid的事务为“past”,默认都是对当前事务可见的,大于当前txid的事务为称为“Future”,默认都是对当前事务不可见的。
但是我们需要知道的是pg数据的事务是有上限的,随着事务ID的不断增长,达到最大值后再从头开始计数,就会出现以前的事务ID比当前的事务ID大的情况,按照上面的比较规则,就会认为最新产生的事务为“past”,这也是PG数据库中比较严重的事务回卷的问题。
为了解决事务回卷的问题,PG规定最新与最旧的两个事务之间的年龄差最大为231,如果事务之间的年龄差超过231时,就把旧的事务全部转换为事务ID号为2的冻结事务。当新事务与冻结事务做比较时,默认正常事务ID比冻结事务ID新。整体来讲可利用如下公式进行计算:
((int32) (id1-id2)) < 0
如果该公式为真,则表示事务id1比事务id2旧。
当事务没有发生回卷时,上面公式相当于直接比大小即可。
当事务发生回卷时,例如id1=4294967295,id2=5,那么id1-id2=4294967290,将这个个差值转换为有符号的int32时,由于超过了2^31该数据会转换为一个负数,这样一来我们还是可以比较出id1比id2旧。
xmin、xmax、cmin、cmax是每个数据行tuple上的隐藏字段,主要用于区别不同事务以及相同事务内tuple的行版本。在了解这四个参数概念前,我们首先需要知道,每个事务都会分为一个事务ID,在同一个事务中执行的每条命令都会分配一个命令ID。
xmin : 当一个事务插入一条新的数据行时,将该数据行的xmin标识为当前事务的事务IDxmax :新插入的数据行的xmax默认为0,当一个事务删除一条数据行时,将该数据行的xmax标识为当前事务的事务ID,相当于做了一个删除标记。cmin/cmax : 在同一事务内,当该命令操作导致会插入/删除操作时,使用cmin、cmax进行计数当新插入一行数据时,将xmin设置为当前事务ID、xmax设置为0;
当删除某一行记录时,将该记录行的xmax设置为当前的事务ID;
当修改某一行时,实际上是将旧数据行的xmax设置为当前事务ID,然后新插入变更后的数据行,并将该数据行的xmin设置为当前事务ID。
INSERT
db1=# begin; db1=# begin; BEGIN BEGIN db1=# select txid_current(); db1=# select txid_current(); txid_current txid_current -------------- -------------- 536 537 (1 row) (1 row) db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 532 | 0 | 2 | 2 | 11 (1 row) (1 row) db1=# insert into t2 values(12); INSERT 0 1 db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 537 | 0 | 0 | 0 | 12 //可以看到insert操作将xmin设置为当前事务ID、xmax设置为0 (2 rows) db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 //事务一看不到事务二未提交记录 (1 row) db1=# commit; COMMIT db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 537 | 0 | 0 | 0 | 12 (2 rows) db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 537 | 0 | 0 | 0 | 12 (2 rows)DELETE
事务一 事务二 db1=# begin; db1=# begin; BEGIN BEGIN db1=# select txid_current(); db1=# select txid_current(); txid_current txid_current -------------- -------------- 534 531 (1 row) (1 row) db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 532 | 0 | 1 | 1 | 2 532 | 0 | 1 | 1 | 2 532 | 0 | 2 | 2 | 11 532 | 0 | 2 | 2 | 11 (2 rows) (2 rows) db1=# delete from t2 where id=2; DELETE 1 db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 (1 row) db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 534 | 0 | 0 | 2 //可以看到事务一的delete操作其实是会被将删除记录行的xmax置为对应delete事务的事务ID号做标识,由于事务一未提交,所以在事务二中事务二中仍然可见 532 | 0 | 2 | 2 | 11 (2 rows) db1=# commit; COMMIT db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 532 | 0 | 2 | 2 | 11 532 | 0 | 2 | 2 | 11 (1 row) (1 row)UPDATE
db1=# begin; db1=# begin; BEGIN BEGIN db1=# select txid_current(); db1=# select txid_current(); txid_current txid_current -------------- -------------- 556 557 (1 row) (1 row) db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 532 | 555 | 0 | 0 | 11 532 | 555 | 0 | 0 | 11 537 | 555 | 1 | 1 | 12 537 | 555 | 1 | 1 | 12 (2 rows) (2 rows) db1=# update t2 set id=1 where id=11; UPDATE 1 db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 537 | 555 | 1 | 1 | 12 557 | 0 | 0 | 0 | 1 //对源记录tuple进行删除打标,xmax设置为delete操作事务ID,然后新insert进去的一条记录,新记录的xmin设置为当前事务ID号,该记录为新insert的记录 (2 rows) db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 557 | 0 | 0 | 11 //由于update事务未提交,在事务一中可以看到update操作会对源记录行的xmax修改为对应事务ID,表示记录的删除 537 | 555 | 1 | 1 | 12 (2 rows) ## update操作其实就是对源被删除记录tuple进行删除打标,xmax设置为delete操作事务ID,然后新insert进去的一条记录,新记录的xmin设置为当前事务ID号在每个事务在事务开始的时候,命令标识计数器被初置为0,每当执行更新命令(如I\U\D\select … for update)时,SQL执行后的命令标识计数标计数值+1。PG每个事务中的命令标识计数最大为2^23-1个,若超过该数据后就会发生回卷,计数器开始从0开始计数。
INSERT/UPDATE/DELETE db1=# begin; BEGIN db1=# select txid_current(); txid_current -------------- 561 (1 row) db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 559 | 3 | 3 | 11 537 | 559 | 4 | 4 | 12 560 | 0 | 0 | 0 | 13 (3 rows) db1=# insert into t2 values(1); INSERT 0 1 db1=# insert into t2 values(2); INSERT 0 1 db1=# insert into t2 values(3); INSERT 0 1 db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 532 | 559 | 3 | 3 | 11 537 | 559 | 4 | 4 | 12 560 | 0 | 0 | 0 | 13 //同一个事务内,第一次新插入数据行cmin、cmax从0开始计数 561 | 0 | 0 | 0 | 1 //同一个事务内,每次命令执行都会使cmin、cmax的计数+1 561 | 0 | 1 | 1 | 2 561 | 0 | 2 | 2 | 3 (6 rows) db1=# delete from t2 where id=11; DELETE 1 db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 537 | 559 | 4 | 4 | 12 532 | 561 | 3 | 3 | 11 //同一个事务中的delete操作,cmin、cmax计数+1 560 | 0 | 0 | 0 | 13 537 | 559 | 4 | 4 | 12 561 | 0 | 0 | 0 | 1 560 | 0 | 0 | 0 | 13 561 | 0 | 1 | 1 | 2 (3 rows) 561 | 0 | 2 | 2 | 3 (5 rows) db1=# update t2 set id=4 where id=12; UPDATE 1 db1=# select xmin,xmax,cmin,cmax,* from t2; db1=# select xmin,xmax,cmin,cmax,* from t2; xmin | xmax | cmin | cmax | id xmin | xmax | cmin | cmax | id ------+------+------+------+---- ------+------+------+------+---- 560 | 0 | 0 | 0 | 13 532 | 561 | 3 | 3 | 11 561 | 0 | 0 | 0 | 1 537 | 561 | 4 | 4 | 12 //同一个事务中的update操作,先对xmax做删除打标,然后cmin、cmax计数+1 561 | 0 | 1 | 1 | 2 560 | 0 | 0 | 0 | 13 561 | 0 | 2 | 2 | 3 (3 rows) 561 | 0 | 4 | 4 | 4 //同一个事务的update操作,其源记录的删除与新记录的insert的cmin、cmax计数一致 (5 rows)commit log也被称为clog,它是pg数据库共享内存中的一块区域,主要用来保留各个事务的事务状态,主要包含以下四种状态: IN_PROGRESS、COMMITTED、ABORTED、SUB_COMMITTED。clog由一个或多个8K的数据页组成,逻辑上以数组的形式记录每个t_xid的事务状态。随着事务的不断增长,clog也会不断增大,此时pg数据库的vacuum process就会负责对clog中无效数据进行清理。
IN_PROGRESS :表示事务正在进行中COMMITTED :表示事务已提交ABORTED :表示事务已回滚SUB_COMMITTED :表示子事务已提交当数据库关闭或者检查点运行时,pg会将clog的数据全部记录在 $PGDATA 中的 pg_xact 目录下的0000、0001文件中进行落盘;当数据库启动时,加载 pg_xact 文件中对事务状态的记录来初始化clog。
在PG数据库中,若一个事务失败,在数据文件中这个事务产生的数据并不会在事务回滚时被清理掉。这样做主要是出于效率考虑,避免事务在回滚或者提交时再次标记数据而额外的多产生一次IO消耗,当事务提交/回滚时会通过共享内存中的clog将所有事务的事务状态进行记录,只需要通过tuple上的tmin、tmax就可以知道对应的事务是成功提交还是回滚掉了,验证记录行的有效性。
事务快照以“xmin:xmax:xip_list”的形式进行存储,可通过内置函数txid_current_snapshot()进行调用查看当前事务下的事务快照信息。通过事务快照我们可以清楚的知道当前事务下一些活跃与非活跃状态的事务信息,并通过tuple的xmin、xmax、clog的事务状态进行一定的版本控制。
xmin : 事务当前状态下最早的活跃事务,此事务ID之前的所有事务都为非活跃状态xmax : 事务当前状态下下最早的活跃事务,该事务ID之后的所有事务都为活跃状态xip_list : 当前事务链下xmin与xmax之前所有活跃的事务ID号列表需要我们注意的就是,事务快照在同一个事务中并不是一成不变的,若在事务未完成之前并发其他事务提交,在不同的时间点该事务下的事务快照信息是不一样的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wqXB79v-1603262182137)(http://note.youdao.com/yws/res/70009/943181AFB2FD4BEC9EA4D6D6FC004176)]
如上图示例:
(a)
xmin = 100 ,100为最早的活跃事务,该事务ID之前的所有事务都是非活跃状态
xmax = 100 , 100为最早的活跃事务,该事务ID之后的所有事务都是活跃状态
xip_list为空
(b)
xmin = 100 ,100为最早的活跃事务,该事务ID之前的所有事务都是非活跃状态
xmax = 104 , 104为最早的活跃事务,该事务ID之后的所有事务都是活跃状态
xip_list = 100,102 ,为xmin与xmax之间的所有的活跃的事务ID组成的列表
查看当前的事务快照信息
db1=# select txid_current_snapshot(); txid_current_snapshot --------------------------------- 1738651:1738657:1738651,1738655 (1 row)第一种方式是,数据库仅保存最新版本数据,将发生变更的旧行版本数据写到其他地方如undo,当需要读取旧版本数据时,通过undo重构。oracle和MySQL就是通过undo的方式实现。
第二种方式是,数据库保存所有行版本数据,当需要读取旧版本数据时直接读取即可,数据库通过一定的机制定时回收无限的行版本数据释放物理空间。postgres数据库就是通过该方式实现,SQL Server使用的方式也类似这种方式,不过它会将旧版本数据保存在tmpdb中。
FSM : 空闲空间映射,以.fsm进行存储。所有表和索引都有各自的FSM,每个FSM在相应的表或索引文件中存储有关每个页面的可用空间容量的信息。
PG多版本并发的一些优势:
事务回滚可立刻完成,无论事务执行了多少操作数据可以进行很多更新,不必像oracle和innodb那样需要保证回滚段不会被用完 文章参考: http://www.interdb.jp/pg/pgsql05.html https://blog.csdn.net/dazuiba008/article/details/94648314