PostgreSQL XMAX深入探究

389 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路


PostgreSQL每张表中都包含系统字段,详见:PostgreSQL表的系统字段

对于XMAX字段,其官方文档的解释如下:
The identity (transaction ID) of the deleting transaction, or zero for an undeleted row version. It is possible for this column to be nonzero in a visible row version. That usually indicates that the deleting transaction hasn’t committed yet, or that an attempted deletion was rolled back.

通过上面的解释我们可以知道,xmax表示删除某行记录的事务ID,这也和我们之前所了解的是一样的,但事实上xmax字段不仅仅如此,不信且看下面的例子:

创建测试表插入一条测试数据,可以看到xmax字段为0。

bill@bill=>create table t1(id int,info text);
CREATE TABLE
bill@bill=>insert into t1 values(1,'bill');
INSERT 0 1
bill@bill=>select xmin,xmax,* from t1;
 xmin | xmax | id | info
------+------+----+------
 2668 |    0 |  1 | bill
(1 row)

接下来对改行数据执行select for update操作。
xmax字段竟然不是0了,可以看到xmax不仅仅是删除数据时会变化啊。

bill@bill=>select * from t1 where id = 1 for update;
 id | info
----+------
  1 | bill
(1 row)

bill@bill=>select xmin,xmax,* from t1;
 xmin | xmax | id | info
------+------+----+------
 2668 | 2669 |  1 | bill
(1 row)

那我们再看看update数据时xmax字段是不是会变化呢?

bill@bill=>select xmin,xmax,* from t1;
 xmin | xmax | id | info
------+------+----+------
 2672 |    0 |  1 | bill
(1 row)

bill@bill=>begin;
BEGIN
bill@bill=>update t1 set info = 'foucus' where id = 1;
UPDATE 1
bill@bill=>commit;
COMMIT
bill@bill=>select ctid,xmin,xmax,* from t1;
 ctid  | xmin | xmax | id |  info
-------+------+------+----+--------
 (0,2) | 2673 |    0 |  1 | foucus
(1 row)

似乎update并不会修改改行的xmax值,真的如此吗?且慢,再看看下面的例子:

bill@bill=>select ctid,xmin,xmax,* from t1;
 ctid  | xmin | xmax | id |  info
-------+------+------+----+--------
 (0,2) | 2673 |    0 |  1 | foucus
(1 row)

bill@bill=>begin;
BEGIN
bill@bill=>update t1 set info = 'bill' where id = 1;
UPDATE 1
bill@bill=>rollback;
ROLLBACK
bill@bill=>select ctid,xmin,xmax,* from t1;
 ctid  | xmin | xmax | id |  info
-------+------+------+----+--------
 (0,2) | 2673 | 2674 |  1 | foucus
(1 row)

奇怪,当我们rollback该事务后,刚刚执行的update操作竟然将xmax字段修改了,这是为什么呢?那么update操作究竟会不会修改xmax的值呢?

其实这些都是因为pg中的行锁也和xmax字段有关导致的。

因为这时候,xmax里的值存储的是一个行锁。当xid为2674的事务修改这条记录的时候,原来的元组的xmax就会写入这个xid号,表示这个元组被某个事务锁住了。而新的数据会被写入一个新的元组中。当我们回滚这个UPDATE操作的时候,新元组就死掉了。而写入了锁的xid的老元组又变得可见了。这时候,我们就看到了xmax中有值了。

我们可以通过pageinspect插件来继续一探究竟。

bill@bill=>select lp,
bill-#        t_ctid  as ctid,
bill-#        t_xmin  as xmin,
bill-#        t_xmax  as xmax,
bill-#        t_attrs [ 1 ] as a,
bill-#        t_attrs [ 2 ] as b
bill-#   from heap_page_item_attrs(get_raw_page('t1', 0), 't1');
 lp | ctid  | xmin | xmax |     a      |        b
----+-------+------+------+------------+------------------
  1 | (0,2) | 2672 | 2673 | \x01000000 | \x0b62696c6c
  2 | (0,3) | 2673 | 2674 | \x01000000 | \x0f666f75637573
  3 | (0,3) | 2674 |    0 | \x01000000 | \x0b62696c6c
(3 rows)

可以看到前面的这条记录产生了3行数据,分别是第一行为insert产生的,第二行是第一次update产生的,第三行是第二次update产生的。而从我们上面查询的结果可以知道第二行数据目前是可见的。

那么我们就无法通过xmax字段来判断数据的可见性了,因为可能像上面这种情况,xmax字段存放的是行锁的信息。那么pg中要怎么确定元组的可见性呢?之前我在PostgreSQL clog详解中也提到过,可以通过infomask字段。

当我们对改行数据进行select for update操作时,可以看到其xmax_is_lock列变为true了。

bill@bill=>begin;
BEGIN
bill@bill=>select * from t1 for update;
 id |  info
----+--------
  1 | foucus
(1 row)

bill@bill=>select lp,
bill-*#        t_ctid as ctid,
bill-*#        t_xmin as xmin,
bill-*#        t_xmax as xmax,
bill-*#        t_attrs [ 1 ] as a,
bill-*#        t_attrs [ 2 ] as b,
bill-*#        (t_infomask &128) ::boolean as xmax_is_lock,
bill-*#        (t_infomask &1024) ::boolean as xmax_commited,
bill-*#        (t_infomask &2048) ::boolean as xmax_rolled_back,
bill-*#        (t_infomask &4096) ::boolean as xmax_multixact
bill-*#   from heap_page_item_attrs(get_raw_page('t1', 0), 't1');
 lp | ctid  | xmin | xmax |     a      |        b         | xmax_is_lock | xmax_commited | xmax_rolled_back | xmax_multixact
----+-------+------+------+------------+------------------+--------------+---------------+------------------+----------------
  1 | (0,2) | 2672 | 2673 | \x01000000 | \x0b62696c6c     | f            | t             | f                | f
  2 | (0,2) | 2673 | 2675 | \x01000000 | \x0f666f75637573 | t            | f             | f                | f
  3 | (0,3) | 2674 |    0 | \x01000000 | \x0b62696c6c     | f            | f             | t                | f

关于t_infomask的值的含义,可以参考:src/include/access/htup_details.h,这里不再赘述。

既然该行被锁定后,xmax记录的时锁定改行的事务id,那么如果同一行被多个会话同时锁定又是什么情况呢?

创建测试表t1和t2:

bill@bill=>create table t1(c1 int primary key,info text);
CREATE TABLE
bill@bill=>create table t2(c1 int,c2 int references t1(c1));
CREATE TABLE
bill@bill=>insert into t1 values(1,'bill');
INSERT 0 1

更新t1的数据:

bill@bill=>update t1 set info = 'foucus' where c1 = 1;
UPDATE 1
bill@bill=>select lp,
bill-#        t_ctid as ctid,
bill-#        t_xmin as xmin,
bill-#        t_xmax as xmax,
bill-#        t_attrs [ 1 ] as a,
bill-#        t_attrs [ 2 ] as b,
bill-#        (t_infomask &128) ::boolean as xmax_is_lock,
bill-#        (t_infomask &1024) ::boolean as xmax_commited,
bill-#        (t_infomask &2048) ::boolean as xmax_rolled_back,
bill-#        (t_infomask &4096) ::boolean as xmax_multixact
bill-#   from heap_page_item_attrs(get_raw_page('t1', 0), 't1');
 lp | ctid  | xmin | xmax |     a      |        b         | xmax_is_lock | xmax_commited | xmax_rolled_back | xmax_multixact
----+-------+------+------+------------+------------------+--------------+---------------+------------------+----------------
  1 | (0,2) | 2688 | 2689 | \x01000000 | \x0b62696c6c     | f            | f             | f                | f
  2 | (0,2) | 2689 |    0 | \x01000000 | \x0f666f75637573 | f            | f             | t                | f
(2 rows)

我们再接着往子表t2中插入数据,可以看到xmax变化了,同时通过infomask字段可以看到改行被锁住了。

bill@bill=>insert into t2 values(1,1);
INSERT 0 1
bill@bill=>select lp,
bill-#        t_ctid as ctid,
bill-#        t_xmin as xmin,
bill-#        t_xmax as xmax,
bill-#        t_attrs [ 1 ] as a,
bill-#        t_attrs [ 2 ] as b,
bill-#        (t_infomask &128) ::boolean as xmax_is_lock,
bill-#        (t_infomask &1024) ::boolean as xmax_commited,
bill-#        (t_infomask &2048) ::boolean as xmax_rolled_back,
bill-#        (t_infomask &4096) ::boolean as xmax_multixact
bill-#   from heap_page_item_attrs(get_raw_page('t1', 0), 't1');
 lp | ctid  | xmin | xmax |     a      |        b         | xmax_is_lock | xmax_commited | xmax_rolled_back | xmax_multixact
----+-------+------+------+------------+------------------+--------------+---------------+------------------+----------------
  1 | (0,2) | 2688 | 2689 | \x01000000 | \x0b62696c6c     | f            | t             | f                | f
  2 | (0,2) | 2689 | 2690 | \x01000000 | \x0f666f75637573 | t            | f             | f                | f
(2 rows)

接着我们再另一个会话中往子表再插入一条数据:

bill@bill=>begin;
BEGIN
bill@bill=>insert into t2 values(2,1);
INSERT 0 1

观察t1表:

bill@bill=>select lp,
bill-*#        t_ctid as ctid,
bill-*#        t_xmin as xmin,
bill-*#        t_xmax as xmax,
bill-*#        t_attrs [ 1 ] as a,
bill-*#        t_attrs [ 2 ] as b,
bill-*#        (t_infomask &128) ::boolean as xmax_is_lock,
bill-*#        (t_infomask &1024) ::boolean as xmax_commited,
bill-*#        (t_infomask &2048) ::boolean as xmax_rolled_back,
bill-*#        (t_infomask &4096) ::boolean as xmax_multixact
bill-*#   from heap_page_item_attrs(get_raw_page('t1', 0), 't1');
 lp | ctid  | xmin | xmax |     a      |        b         | xmax_is_lock | xmax_commited | xmax_rolled_back | xmax_multixact
----+-------+------+------+------------+------------------+--------------+---------------+------------------+----------------
  1 | (0,2) | 2688 | 2689 | \x01000000 | \x0b62696c6c     | f            | t             | f                | f
  2 | (0,2) | 2689 |    1 | \x01000000 | \x0f666f75637573 | t            | f             | f                | t
(2 rows)

可以看到这个时候xmax字段变成1了,显然这个不是事务id了,同时xmax_multixact字段变成true了。这表示xmax字段插入的值变成了Multixact ID。

这个Multixact ID=1表示什么呢?

bill@bill=>select * from pg_get_multixact_members('1');
 xid  | mode
------+-------
 2692 | keysh
 2693 | keysh
(2 rows)

当有多个事务锁了这个元组的时候,xmax是无法表示的,这时候,需要另外一个数据结构来存储这个信息了。我们通过pg_get_multixact_member可以看到锁定这条记录的两个事务。

总结:
通过上面的例子我们可以得出结论,xmax字段的作用有以下几种:

  1. 存储MVCC副本的最大可见xid,即数据被删除时;或者为0,即第一次插入数据的行。
  2. 当有行锁的时候,存储锁定这个元组的XID。
  3. 当有多个事务锁住这个元组的时候,存储multixact的ID。