作者:瀚高PG实验室 (Highgo PG Lab)-波罗
Postgresql表中的每一行数据(称为一个tuple),包含有4个隐藏字段,但可直接访问,它们分别是:
xmin 在创建(insert)记录(tuple)时,记录此值为插入tuple的事务ID
xmax 默认值为0,在删除tuple时,记录此值
cmin和cmax 标识在同一个事务中多个语句命令的序列值,从0开始,用于同一个事务中实现版本可见性判断
下面通过实验具体看看这些标记如何工作。在此之前,先创建测试表
CREATE TABLE test
(
id INTEGER,
value TEXT
);
Session1:
开启一个事务,查询当前事务ID(值为1855),并插入一条数据,xmin为1855,与当前事务ID相等。
符合上文所述——插入tuple时记录xmin,记录未被删除时xmax为0
postgres=> BEGIN;
BEGIN
postgres=> SELECT TXID_CURRENT();
txid_current
--------------
1855
(1 row)
postgres=> INSERT INTO test VALUES(1, 'a');
INSERT 0 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
1 | a | 1855| 0 | 0 | 0
(
(1 row)
继续通过一条语句插入2条记录,xmin仍然为当前事务ID,即1855,xmax仍然为0,同时cmin和cmax为1,符合上文所述cmin/cmax在事务内随着所执行的语句递增。虽然此步骤插入了两条数据,但因为是在同一条语句中插入,故其cmin/cmax都为1,在上一条语句的基础上加一。
INSERT INTO test VALUES(2, 'b'), (3, 'c');
INSERT 0 2
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
1 | a | 1855| 0 | 0 | 0
2 | b | 1855| 0 | 1 | 1
3 | c | 1855| 0 | 1 | 1
(
(3 rows)
将id为1的记录的value字段更新为'd',其xmin和xmax均未变,而cmin和cmax变为2。此时提交事务。
UPDATE test SET value = 'd' WHERE id = 1;
UPDATE 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 1855| 0 | 1 | 1
3 | c | 1855| 0 | 1 | 1
1 | d | 1855| 0 | 2 | 2
(3 rows)
postgres=> COMMIT;
COMMIT
开启一个新事务,通过2条语句分别插入2条id为4和5的tuple
BEGIN;
BEGIN
postgres=> INSERT INTO test VALUES (4, 'x');
INSERT 0 1
postgres=> INSERT INTO test VALUES (5, 'y');
INSERT 0 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 1855| 0 | 1 | 1
3 | c | 1855| 0 | 1 | 1
1 | d | 1855| 0 | 2 | 2
4 | x | 1856| 0 | 0 | 0
5 | y | 1856| 0 | 1 | 1
(5 rows)
此时,将id为2的tuple的value更新为'e',其对应的cmin/cmax被设置为2,且其xmin被设置为当前事务ID,即3278
UPDATE test SET value = 'e' WHERE id = 2;
UPDATE 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
3 | c | 1855| 0 | 1 | 1
1 | d | 1855| 0 | 2 | 2
4 | x | 1856| 0 | 0 | 0
5 | y | 1856| 0 | 1 | 1
2 | e | 1856| 0 | 2 | 2
Session2:
在另外一个窗口中开启一个事务,可以发现id为2的tuple,value值仍然为b,xin仍然为1855,但其xmax被设置为3278,而cmin和cmax均为2。
符合上文所述——若tuple被删除,则xmax被设置为删除tuple的事务的ID。
BEGIN;
BEGIN
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 1855| 1856| 2 | 2
3 | c | 1855| 0 | 1 | 1
1 | d | 1855| 0 | 2 | 2
(
(3 rows)
如果session1更改语句UPDATE test SET value = 'e' WHERE id = 2;没有提交,在session2中执行如下语句会处于等待状态,涉及更改同一条记录的锁等待的问题,直到session1的update语句被提交释放锁资源后,才能执行。
testdb=# UPDATE test SET value = 'f' WHERE id = 2;
这里有几点要注意
新旧窗口中id为2的tuple对应的value和xmin、xmax、cmin/cmax均不相同,实际上它们是该tuple的2个不同版本
在旧窗口中,更新之前,数据的顺序是2,3,1,4,5,更新后变为3,1,4,5,2。
因为在PostgreSQL中更新实际上是将旧tuple标记为删除,并插入更新后的新数据,所以更新后id为2的tuple从原来最前面变成了最后面
在新窗口中,id为2的tuple仍然如旧窗口中更新之前一样,排在最前面。
这是因为旧窗口中的事务未提交,更新对新窗口不可见,新窗口看到的仍然是旧版本的数据
提交旧窗口中的事务后,新旧窗口中看到数据完全一致——id为2的tuple排在了最后,xmin变为3278,xmax为0,cmin/cmax为2。
前文定义中,xmin是tuple创建时的事务ID,并没有提及更新的事务ID,但因为PostgreSQL的更新操作并非真正更新数据,而是将旧数据标记为删除,并插入新数据,所以“更新的事务ID”也就是“创建记录的事务ID”。
SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
3 | c | 1855| 0 | 1 | 1
1 | d | 1855| 0 | 2 | 2
4 | x | 1856| 0 | 0 | 0
5 | y | 1856| 0 | 1 | 1
2 | e | 1856| 0 | 2 | 2
(5 rows)