• Postgresql事物快照介绍


    什么是事物快照?

    一个数据页包含了每一行的多个版本,每一行的可见版本一起构成一个快照。快照只包含在创建快照时当前已提交的数据,在这个特定的时刻提供了一个一致性的视图,这个视图我们就可以叫做快照。

    为了确保数据的隔离性,每一个事物都有自己的快照,这就意味着不同的事物在不同的时间点可以看到不同的快照。但是单个快照内部是一致的。

    在 Read Committed 隔离级别中,每个语句开始都会有一个快照,并且在该语句执行期间,仍然保持活动状态。

    在Repeatable Read和Serializable隔离级别中,每个事物的第一条语句开始之处只会有一个快照,在整个事物结束之前,该快照一直是活动的。

    可以参考下图:
    在这里插入图片描述

    事物快照结构

    事物快照在创建的时候,由几个值构成,xmin,xmax,xip_list,之前文章有介绍过,xmin表示最早还活跃的事物ID,xmax表示尚未分配的事物id,xip_list表示xmin到xmax之间活动的事物id。通过下图可以更加直观的了解:
    在这里插入图片描述
    为了了解上图,我们给出一个实例
    事物1为一个活跃事物,做了如下操作:

    postgres=# create table accounts(id int,client text,amount float);
    CREATE TABLE
    postgres=# begin;
    BEGIN
    postgres=*# INSERT INTO accounts VALUES (1, 'alice', 1000.00);
    INSERT 0 1
    postgres=*# SELECT pg_current_xact_id();
     pg_current_xact_id 
    --------------------
                2254346
    (1 row)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    事物2插入第二行,并提交事物

    postgres=# begin;
    BEGIN
    postgres=*# INSERT INTO accounts VALUES (2, 'bob', 100.00); 
    INSERT 0 1
    postgres=*# SELECT pg_current_xact_id();
     pg_current_xact_id 
    --------------------
                2254347
    (1 row)
    
    postgres=*# commit;
    COMMIT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    另开一个session,开启一个REPEATABLE READ的事物,创建了一个快照,我们通过相关函数查看快照

    #这里2254346:2254348:2254346就是该快照的组成,最小活跃事物为2254346,是我们第一个事物的事物号,2254348是在第二个事物的事物号基础上加1,即2254347+1,后面的2254346表示这个区间内活跃的事物是2254346,因为2254346这个事物没提交,所以是活跃的。
    postgres=# BEGIN ISOLATION LEVEL REPEATABLE READ;
    BEGIN
    postgres=*# SELECT pg_current_snapshot();
       pg_current_snapshot   
    -------------------------
     2254346:2254348:2254346
    (1 row)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    开启第三个事物,并更新一行

    postgres=# begin;
    BEGIN
    postgres=*# UPDATE accounts SET amount = amount + 100 WHERE id = 2;
    UPDATE 1
    postgres=*# SELECT pg_current_xact_id();
     pg_current_xact_id 
    --------------------
                2254348
    (1 row)
    postgres=*# commit ;
    COMMIT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    再次回到我们开启的REPEATABLE READ会话,查看accounts表,发现只能看到一行,但是我们查看0号page实际有三行

    postgres=*# SELECT ctid, * FROM accounts;
     ctid  | id | client | amount 
    -------+----+--------+--------
     (0,2) |  2 | bob    |    100
    (1 row)
    postgres=*# SELECT * FROM heap_page('accounts',0);
     ctid  | state  |    xmin     |  xmax   | hhu | hot | t_ctid 
    -------+--------+-------------+---------+-----+-----+--------
     (0,1) | normal | 2254346     | 0 (a)   |     |     | (0,1)
     (0,2) | normal | 2254347 (c) | 2254348 | t   |     | (0,3)
     (0,3) | normal | 2254348     | 0 (a)   |     | t   | (0,3)
    (3 rows)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过以上实例,我们可以看到
    如果xid 如果xmin⩽xid

    事物自身改变的可见性

    涉及到事物自身改变的可见性规则则更为复杂一些。例如,在特定时间点打开的游标不能看到以后发生的任何更改,无论是什么隔离级别。
    为了解决这种情况,元组的头部信息提供了一个特殊字段(显示为cmin和cmax伪列),显示事务中操作的序列号。cmin列标识插入,而cmax用于标识删除操作。为了节省空间,这些值存储在tuple头的一个字段中,而不是存储在两个不同的字段中。假设同一个行在单个事务中几乎不会同时插入和删除。(如果发生这种情况,Postgre将写入一个特殊的组合标识符到该字段中,在这种情况下,实际的cmin和cmax值由后端存储。)

    如下一个实例

    #开启事物,插入一行
    postgres=# begin;
    BEGIN
    postgres=*# INSERT INTO accounts VALUES (3, 'charlie', 100.00);
    INSERT 0 1
    postgres=*# SELECT pg_current_xact_id();
     pg_current_xact_id 
    --------------------
                2254350
    (1 row)
    #定义一个游标查询该表有多少行
    postgres=*# DECLARE c CURSOR FOR SELECT count(*) FROM accounts;
    DECLARE CURSOR
    #定义游标后,再插入一行
    postgres=*#  INSERT INTO accounts VALUES (4, 'charlie', 200.00);
    INSERT 0 1
    #显示cmin
    postgres=*#  SELECT xmin, CASE WHEN xmin = 2254350 THEN cmin END cmin, * FROM accounts;
      xmin   | cmin | id | client  | amount 
    ---------+------+----+---------+--------
     2254346 |      |  1 | alice   |   1000
     2254348 |      |  2 | bob     |    200
     2254350 |    0 |  3 | charlie |    100
     2254350 |    1 |  4 | charlie |    200
    (4 rows)
    
    #游标查询只能获得三行;当游标已经打开时插入的行不会进入快照,因为不满足cmin < 1
    postgres=*# fetch c;
     count 
    -------
         3
    (1 row)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    事物水平线

    如前所述,快照的下限由xmin表示,xmin是快照创建时活动的最老事务ID。这个值非常重要,因为它定义了使用此快照的事务的范围。
    如果一个事务没有活动快照(例如,在语句执行之间的Read Committed隔离级别),如果它被分配,那么它的水平线是由它自己定义的。

    所有超出水平线的事务(xid < xmin的事务)都被指定为提交。这意味着事务只能看到超过水平线的当前行版本。
    Postgre会跟踪所有进程的当前水平线;事务可以在pg_stat_activity中看到它自己的水平线:

    postgres=# begin;
    BEGIN
    postgres=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
     backend_xmin 
    --------------
          2254351
    (1 row)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    虚拟事务没有真正的“事物id”,但它们仍然像常规事务一样使用快照,因此它们也有自己的水平线。而且它们是没有活动快照的虚拟事物。水平线的概念对它们没有任何意义,当涉及到快照和可见性时,它们对系统来说完全是透明的。

    我们也可以以类似的方式定义数据库的水平线,出于这个目的,我们应该取这个数据库中所有事务的水平线,选择具有最老xmin的。超出这个水平线,该数据库中过期的堆元组将永远不会对任何事务可见。这些元组可以安全的由vacuum维护清理。如下图:
    在这里插入图片描述
    根据以上我们得出一些结论:
    不管是虚拟事物还是真实的事物,在 Repeatable Read和Serializable隔离级别,长时间运行一个事物,会长时间的保持数据库的水平线,vacuum操作会延迟处理。

    Read Committed隔离级别中的真实事物,一样会持有数据库的水平线,vacuum延迟处理,即使事物是idle in transaction状态。

    Read Committed隔离级别的虚拟事务仅在执行操作时保持水平线。

    整个数据库只有一个水平线,如果它被一个事务持有,则不能清空这个水平内的任何数据(即使这个事务不访问这些数据)。

    理想情况下,应该避免将长事务和频繁更新(这会产生新行版本)同时在一个库中,因为这会导致表和索引膨胀。

    #开启一个事物,查看当前事物持有水平线
    postgres=# begin;
    BEGIN
    postgres=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
     backend_xmin 
    --------------
          2254351
    (1 row)
    
    #另一个session消费一个事物
    postgres=# SELECT pg_current_xact_id();
     pg_current_xact_id 
    --------------------
                2254352
    (1 row)
    #如果事物不提交,则水平线不会向前移动
    postgres=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
     backend_xmin 
    --------------
          2254351
    postgres=*# commit;
    COMMIT
    #提交以后,水平线才会向前移动,过期的数据才会被vacuum处理
    postgres=# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
     backend_xmin 
    --------------
          2254353
    (1 row)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    系统目录快照

    即使开启一个REPEATABLE READ事物,系统目录也要保持最新,包含了表定义的最新更改和添加的完整性约束,否则我们看到的就是过时的表定义或者是丢失新添加的完整性约束。如下示例:

    postgres=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN
    postgres=*# select 1;
     ?column? 
    ----------
            1
    (1 row)
    #另一个session设置一个非空约束
    postgres=# ALTER TABLE accounts ALTER amount SET NOT NULL;
    ALTER TABLE
    #回到REPEATABLE READ事物中,无法插入数据,这里我们可以看到实际上是违反了事物隔离级别的规则
    postgres=*# INSERT INTO accounts(client, amount) VALUES ('alice', NULL);
    ERROR:  null value in column "amount" of relation "accounts" violates not-null constraint
    DETAIL:  Failing row contains (null, alice, null).
    postgres=!# rollback ;
    ROLLBACK
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果我们先插入数据,再添加非空约束会怎样?如下:

    #如果我们先开启一个事物,插入一行带有空的数据
    postgres=# BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN
    postgres=*# INSERT INTO accounts(client, amount) VALUES ('alice', NULL);
    INSERT 0 1
    #如果上面事物不提交,那么我们下面的语句一直会被阻塞
    postgres=# ALTER TABLE accounts ALTER amount SET NOT NULL;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由上面示例可见,数据库好像在每一次系统目录被查询的时候都会创建单独的快照,实际上,该原理远不止我们看到的这么简单,因为频繁的创建快照会对性能有影响,而且,很多系统目录是被缓存的。

    快照导出

    在某些情况下,并发事务需要看到同一个快照。例如,如果pg_dump以并行模式运行,那么它的所有进程必须看到相同的数据库状态,才能生成一致的备份。

    我们不能因为事务是“同时”启动的就假定快照是相同的。为了确保所有事务都看到相同的数据,这里必须使用快照导出机制。

    使用pg_export_snapshot函数导出一个快照,并可以把快照传递给另一个事务:

    看一个例子:

    postgres=# BEGIN ISOLATION LEVEL REPEATABLE READ;
    BEGIN
    postgres=*# SELECT count(*) FROM accounts;
     count 
    -------
         6
    (1 row)
    
    postgres=*# SELECT pg_export_snapshot();
     pg_export_snapshot  
    ---------------------
     00000003-0005C9A3-1
    (1 row)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    另一个session使用该导出的快照,可以看到accounts表即使已经删除了,但是我们还是6行

    postgres=# delete from accounts ;
    DELETE 6
    postgres=# BEGIN ISOLATION LEVEL REPEATABLE READ;
    BEGIN
    postgres=*# SET TRANSACTION SNAPSHOT '00000003-0005C9A3-1';
    SET
    postgres=*# SELECT count(*) FROM accounts;
     count 
    -------
         6
    (1 row)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    导出的快照的生命周期与导出事务的生命周期相同,也就是说导出快照的事物提交后,就无法再使用该快照了,但是还在使用该快照的事物,可以正常使用。

    参考:
    https://habr.com/en/company/postgrespro/blog/479512/

  • 相关阅读:
    JVM学习-类加载机制
    day57 集合 List Set Map
    ChatGPT在虚拟旅游和文化体验中的潜在作用如何?
    需求管理手册-对需求描述的要求(8)
    将Shopee带到巴黎,印尼MSME产品抢走参观者注意力
    低代码平台适用于大中型企业吗?
    激励-保健理论和公平理论
    FPGA设计时序约束三、设置时钟组set_clock_groups
    首届数据安全大赛初赛web
    短视频账号矩阵系统saas源码搭建/技术
  • 原文地址:https://blog.csdn.net/dazuiba008/article/details/127727258