1 概述

快照的原理本篇不在介绍,可以参考之前的文章,本篇主要快照堆栈。

2 实验

为什么需要快照堆栈?

因为在事务中,有些行为是需要看到最新数据的,比如一个RR事务拿到一个快照后执行了一段时间,这时运行了一条CALL Func或触发器语句,开始进入函数的执行逻辑。

那么这个函数按照PG的定义,是需要自己重新拿最新的快照去执行的,调用或被触发的函数,直观上理解应该能看到 调用时刻的最新数据才对,而不是看到很久前事务启动时的数据。

实例:

drop table if exists t_plpgsql_transaction_20230406_01;
create table t_plpgsql_transaction_20230406_01(a int);

CREATE or replace PROCEDURE p_inner()
LANGUAGE plpgsql
AS $$
DECLARE
    carry float;
    cnt int[];
BEGIN
    INSERT INTO t_plpgsql_transaction_20230406_01 (a) VALUES (10);
    INSERT INTO t_plpgsql_transaction_20230406_01 (a) VALUES (20);

    select array_agg(a) into cnt from t_plpgsql_transaction_20230406_01;
    raise notice 'count: %', cnt;
END;
$$;

CREATE or replace PROCEDURE p_outter()
LANGUAGE plpgsql
AS $$
DECLARE
    carry float;
    cnt int[];
BEGIN
    INSERT INTO t_plpgsql_transaction_20230406_01 (a) VALUES (1);
    INSERT INTO t_plpgsql_transaction_20230406_01 (a) VALUES (2);

    select array_agg(a) into cnt from t_plpgsql_transaction_20230406_01;
    raise notice 'count: %', cnt;
    
    perform pg_sleep(5);

    call p_inner();
END;
$$;

truncate t_plpgsql_transaction_20230406_01;

call p_outter();

例如存在上述两个函数,执行结果为:

postgres=# truncate t_plpgsql_transaction_20230406_01;
TRUNCATE TABLE

postgres=# call p_outter();
NOTICE:  count: {1,2}
NOTICE:  count: {1,2,10,20}

那么如果在函数p_outter执行pg_sleep期间内,在另一个会话中插入一条数据后会发生什么?

结果是:

-- 并发会话执行
postgres=# INSERT INTO t_plpgsql_transaction_20230406_01 (a) VALUES (999);
INSERT 0 1

-- 当前会话能看到999这条数据
postgres=# truncate t_plpgsql_transaction_20230406_01;
TRUNCATE TABLE
postgres=# call p_outter();
NOTICE:  count: {1,2}
NOTICE:  count: {1,2,999,10,20}

从结果可以看到,单条SQL call p_otter()中每次Call函数都会重新拿快照的。

代码中是在CallStmt时判断procedure则拿新的快照,旧的入栈。
Postgresql快照堆栈ActiveSnapshot-LMLPHP

3 快照堆栈

实际上PG中PushActiveSnapshot的用处非常多,例如创建索引、vacuum等等。

编码的时候需要注意快照几个接口函数的使用:

3.1 接口函数与内部变量

堆栈相关:

  • PushActiveSnapshot
    • 新建ActiveSnapshotElt,ActiveSnapshot全局变量永远指向栈顶。OldestActiveSnapshot永远指向栈底。
  • PopActiveSnapshot
    • 如果栈顶snapshot的引用计数为0,则释放快照,同时释放栈顶ActiveSnapshotElt结构,调整ActiveSnapshot重新指向新栈顶。
      Postgresql快照堆栈ActiveSnapshot-LMLPHP

几个内部变量:

  1. CurrentSnapshot:表示当前连接正在使用的活动事务快照。
  2. SecondarySnapshot:永远指向一个最新的快照。
  3. CatalogSnapshot:表示访问系统表的专用快照,如果不被其他进程通知失效能一直用。
  4. HistoricSnapshot:用于逻辑复制。

快照获取:

  • GetLatestSnapshot
    • 永远拿一个最新的快照回来,注意这个接口使用的是局部变量SecondarySnapshot。
  • GetCatalogSnapshot
    • 给table_beginscan_catalog专用扫系统表用的,用的是局部变量CatalogSnapshot,在有其他进程把他失效掉之前,一直使用一个快照。
  • GetSnapshotData

4 快照跟随事务销毁

最后提一下快照跟随事务的销毁机制。

PG中的快照和其他资源一样,生命周期也是严格跟随事务系统的,也就是在事务提交、撤销、子事务提交、子事务撤销时,都会有快照的销毁或转移动作。具体是由下面几个函数完成的:
Postgresql快照堆栈ActiveSnapshot-LMLPHP
主事务系统

  • AtEOXact_Snapshot:事务提交或清理时,会顺便清理ActiveSnapshot等变量。

子事务系统

  • AtSubCommit_Snapshot:子事务提交时,会遍历快照堆栈,把与该子事务同层的快照 降低一层,交给上层子事务。
  • AtSubAbort_Snapshot:子事务回滚时,会遍历快照堆栈,把该子事务同层的快照直接释放掉。
04-07 23:03