ITPub博客

首页 > 数据库 > 数据库开发技术 > 如何优雅地计算年龄

如何优雅地计算年龄

原创 数据库开发技术 作者:u_a9af4a561c3b 时间:2022-03-20 19:35:36 0 删除 编辑

前言

今天一位朋友找到我,咨询了一点关于数据库年龄计算结果的一点困惑。

光看他提供的截图,可以看到

  • • vacuum full,年龄变成了1

  • • vacuum,不会使年龄降低

  • • vacuum freeze,使年龄降至了0

那么,让我们分析一下年龄的计算方式,查漏补缺。

分析

postgres=# begin;
BEGIN
postgres=*# create table test1(id int);
CREATE TABLE
postgres=*# select txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                      876
(1 row)

postgres=*# commit ;
COMMIT
postgres=# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          876 |   1
(1 row)

可以看到,新建一个表的年龄是1。

postgres=# begin;
BEGIN
postgres=*# insert into test1 values(1);
INSERT 0 1
postgres=*# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          876 |   1
(1 row)

postgres=*# select txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                      877
(1 row)

postgres=*# commit ;
COMMIT
postgres=# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          876 |   2
(1 row)

插入一条数据后,年龄变成了2。

postgres=# vacuum full test1;
VACUUM
postgres=# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          878 |   1
(1 row)

postgres=# vacuum test1;
VACUUM
postgres=# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          878 |   1
(1 row)

postgres=# vacuum freeze test1;
VACUUM
postgres=# select relfrozenxid,age(relfrozenxid) from pg_class where relname = 'test1';
 relfrozenxid | age 
--------------+-----
          879 |   0
(1 row)

根据复现,可以发现:

  • • vacuum full,年龄又变成了1

  • • vacuum,年龄不会改变

  • • vacuum freeze,使年龄降低至了0

那么每种情况是如何计算得到的呢?让我们一一分析。

vacuum full

首先是vacuum full,不难理解,vacuum full由于会对表进行重组,relfilenode会发生变化,也就相当于新建了另一张表,所以年龄和create tables时一样,都是1。

那么1是如何计算的呢?需要看一下age()的计算方式

/*
 *        xid_age            - compute age of an XID (relative to latest stable xid)
 */

Datum
xid_age(PG_FUNCTION_ARGS)
{
    TransactionId xid = PG_GETARG_TRANSACTIONID(0);
    TransactionId now = GetStableLatestTransactionId();

    /* Permanent XIDs are always infinitely old */
    if (!TransactionIdIsNormal(xid))
        PG_RETURN_INT32(INT_MAX);

    PG_RETURN_INT32((int32) (now - xid));
}

计算逻辑挺简单,对于普通的事务ID,就是 PG_RETURN_INT32((int32) (now - xid)); now通过GetStableLatestTransactionId()获取当前事务ID,假如没有分配的话,则获取下一个要分配的事务ID,一旦获取了之后,在当前事务内保持不变。

/*
 *    GetStableLatestTransactionId
 *
 * Get the transaction's XID if it has one, else read the next-to-be-assigned
 * XID.  Once we have a value, return that same value for the remainder of the
 * current transaction.  This is meant to provide the reference point for the
 * age(xid) function, but might be useful for other maintenance tasks as well.
 */

TransactionId
GetStableLatestTransactionId(void)
{
    static LocalTransactionId lxid = InvalidLocalTransactionId;
    static TransactionId stablexid = InvalidTransactionId;

    if (lxid != MyProc->lxid)
    {
        lxid = MyProc->lxid;
        stablexid = GetTopTransactionIdIfAny();
        if (!TransactionIdIsValid(stablexid))
            stablexid = ReadNextTransactionId();
    }

    Assert(TransactionIdIsValid(stablexid));

    return stablexid;
}

看个例子

postgres=# begin;
BEGIN
postgres=*# select txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 row)

postgres=*# create table test3(id int);
CREATE TABLE
postgres=*# select next_xid,txid_current_if_assigned() from pg_control_checkpoint();
 next_xid | txid_current_if_assigned 
----------+--------------------------
 0:881    |                      881
(1 row)

postgres=*# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 'test3';
 relfrozenxid | age | txid_current_if_assigned 
--------------+-----+--------------------------
          881 |   0 |                      881
(1 row)

postgres=*# commit ;
COMMIT
postgres=# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 'test3';
 relfrozenxid | age | txid_current_if_assigned 
--------------+-----+--------------------------
          881 |   1 |                         
(1 row)

postgres=# checkpoint ;
CHECKPOINT
postgres=# select next_xid,txid_current_if_assigned() from pg_control_checkpoint();
 next_xid | txid_current_if_assigned 
----------+--------------------------
 0:882    |                         
(1 row)

所以,这里的计算方式是:

  • • 在事务里面,由于获取了一个永久事务ID,事务内GetStableLatestTransactionId保持不变,所以是881 - 881 = 0

  • • 提交之后,获取next-to-be-assigned,所以是882 - 881 = 1

vacuum

普通的vacuum也很好理解,因为普通的vacuum (Lazy Mode) 会搭配VM文件,所以有可能部分Tuple没有被冻结到,具体原理可以去翻一下interdb,在此表过不提。

可以看到,第二个数据块由于VM文件的存在被跳过了,原本可以被冻结的元组没有被冻结,因此普通的vacuum不会更新relfrozenxid,因为小于relfrozenxid的元组表示都已被冻结,所以age()计算出来的年龄保持不变。

vacuum freeze

vacuum freeze也不难理解

The VACUUM command with the FREEZE option forces all txids in the specified tables to be frozen. This is performed in eager mode; however, the freezeLimit is set to OldestXmin (not ‘OldestXmin - vacuum_freeze_min_age’). For example, when the VACUUM FULL command is executed by txid 5000 and there are no other running transactions, OldesXmin is set to 5000 and txids that are less than 5000 are frozen.

手动vacuum + freeze的操作,相当于急切模式 (eager mode),会强制冻结所有的元组,忽略VM文件 (不顾9.6引入的all_frozen标志位可以大幅加速静态表的冻结过程),所以这种模式会更新relfrozenxid。

postgres=# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 'test3';
 relfrozenxid | age | txid_current_if_assigned 
--------------+-----+--------------------------
          881 |   1 |                         
(1 row)

postgres=# vacuum freeze test3;
VACUUM
postgres=# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 'test3';
 relfrozenxid | age | txid_current_if_assigned 
--------------+-----+--------------------------
          882 |   0 |                         
(1 row)

postgres=# checkpoint ;
CHECKPOINT
postgres=# select next_xid from pg_control_checkpoint();
 next_xid 
----------
 0:882
(1 row)

postgres=# begin;
BEGIN
postgres=*# insert into test3 values(1);
INSERT 0 1
postgres=*# select xmin,txid_current_if_assigned() from test3;
 xmin | txid_current_if_assigned 
------+--------------------------
  882 |                      882
(1 row)

因此这样的计算方式是 next-to-be-assigned - relfrozenxid = 882 - 882 = 0。

非表对象

另外一个值得注意的是,前面一直演示的是对表的年龄计算,那么假如是非表对象呢?比如索引

postgres=# begin;
BEGIN
postgres=*# create index t_idx on test2(id);
CREATE INDEX
postgres=*# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 't_idx';
 relfrozenxid |    age     | txid_current_if_assigned 
--------------+------------+--------------------------
            0 | 2147483647 |                      869
(1 row)

postgres=*# commit ;
COMMIT
postgres=# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 't_idx';
 relfrozenxid |    age     | txid_current_if_assigned 
--------------+------------+--------------------------
            0 | 2147483647 |                         
(1 row)

postgres=# create view myview as select * from test2;
CREATE VIEW
postgres=# select relfrozenxid,age(relfrozenxid),txid_current_if_assigned() from pg_class where relname = 'myview';
 relfrozenxid |    age     | txid_current_if_assigned 
--------------+------------+--------------------------
            0 | 2147483647 |                         
(1 row)

可以看到非表对象的年龄都是2147483647。官网有注释:如果不是表,则relfrozenxid为0

All transaction IDs before this one have been replaced with a permanent ("frozen") transaction ID in this table. This is used to track whether the table needs to be vacuumed in order to prevent transaction ID wraparound or to allow pg_xact to be shrunk. Zero (InvalidTransactionId) if the relation is not a table.


在此之前的所有事务ID在表中已经被替换为一个永久的("冻结的") 事务ID。这用于跟踪表是否需要被清理,以便阻止事务ID回卷或者允许pg_xact被收缩。如果该关系不是一个表则为0(InvalidTransactionId)。

回到计算逻辑,因为是0,非普通的TransactionID (需要大于等于3),所以计算方式就变成了PG_RETURN_INT32(INT_MAX);,直接返回int32的最大值

/*
 *        xid_age            - compute age of an XID (relative to latest stable xid)
 */

Datum
xid_age(PG_FUNCTION_ARGS)
{
    TransactionId xid = PG_GETARG_TRANSACTIONID(0);
    TransactionId now = GetStableLatestTransactionId();

    /* Permanent XIDs are always infinitely old */
    if (!TransactionIdIsNormal(xid))
        PG_RETURN_INT32(INT_MAX);

    PG_RETURN_INT32((int32) (now - xid));
}
(gdb) b xid_age
Breakpoint 1 at 0xa550a6: file xid.c, line 103.
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000a550a6 in xid_age at xid.c:103
(gdb) c
Continuing.

Breakpoint 1, xid_age (fcinfo=0x1273b60) at xid.c:103
103             TransactionId xid = PG_GETARG_TRANSACTIONID(0);
(gdb) p xid
$3 = 0
(gdb) n
104             TransactionId now = GetStableLatestTransactionId();
(gdb) n
107             if (!TransactionIdIsNormal(xid))
(gdb) finish
Run till exit from #0  xid_age (fcinfo=0x1273b60) at xid.c:107
0x00000000006dc299 in ExecInterpExpr (state=0x1273278, econtext=0x1272d90, isnull=0x7ffde5a1b447) at execExprInterp.c:749
749                             d = op->d.func.fn_addr(fcinfo);
Value returned is $4 = 2147483647

可以看到,最终返回2147483647。所以其他的类似1、2的年龄都是2147483647

postgres=# select age('1'::xid);
    age     
------------
 2147483647
(1 row)

postgres=# select age('2'::xid);
    age     
------------
 2147483647
(1 row)

postgres=# select age('3'::xid);
 age 
-----
 852
(1 row)

小结

另外还有一个MultiXact,一样的计算方式,因为对于for shared和for key share行锁,一行上面可能会被多个事务加锁,Tuple上动态维护这些事务代价很高,为此引入了multixact机制,将多个事务记录到MultiXactId,再将MultiXactId记录到tuple的xmax中。因此也有类似的冻结和计算年龄的过程。

postgres=# select * from test_lock as t,pgrowlocks('test_lock') as lc where t.ctid = lc.locked_row;
 id | locked_row | locker | multi |   xids    |       modes       |     pids      
----+------------+--------+-------+-----------+-------------------+---------------
  1 | (0,1)      |    545 | f     | {545}     | {"No Key Update"} | {10698}
  2 | (0,2)      |    545 | f     | {545}     | {"For Update"}    | {10698}
  3 | (0,3)      |      1 | t     | {545,551} | {Share,Share}     | {10698,17936}
(3 rows)

postgres=# show autovacuum_multixact_freeze_max_age ;
 autovacuum_multixact_freeze_max_age 
-------------------------------------
 400000000
(1 row)
/*
 *        mxid_age            - compute age of a multi XID (relative to latest stable mxid)
 */

Datum
mxid_age(PG_FUNCTION_ARGS)
{
    TransactionId xid = PG_GETARG_TRANSACTIONID(0);
    MultiXactId now = ReadNextMultiXactId();

    if (!MultiXactIdIsValid(xid))
        PG_RETURN_INT32(INT_MAX);

    PG_RETURN_INT32((int32) (now - xid));
}

现在,知道年龄是如何计算的了吧?

嘿嘿,欢迎各位微信骚扰我,我看到了都会回答的 (假如我知道的话😁😁),不知道我也会去骚扰德哥、vage这些大咖的。


原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2020-07-14

  • 博文量
    306
  • 访问量
    139324