一个数据页包含了每一行的多个版本,每一行的可见版本一起构成一个快照。快照只包含在创建快照时当前已提交的数据,在这个特定的时刻提供了一个一致性的视图,这个视图我们就可以叫做快照。
为了确保数据的隔离性,每一个事物都有自己的快照,这就意味着不同的事物在不同的时间点可以看到不同的快照。但是单个快照内部是一致的。
在 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)
事物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
另开一个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)
开启第三个事物,并更新一行
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
再次回到我们开启的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)
通过以上实例,我们可以看到 涉及到事物自身改变的可见性规则则更为复杂一些。例如,在特定时间点打开的游标不能看到以后发生的任何更改,无论是什么隔离级别。 如下一个实例 如前所述,快照的下限由xmin表示,xmin是快照创建时活动的最老事务ID。这个值非常重要,因为它定义了使用此快照的事务的范围。 所有超出水平线的事务(xid < xmin的事务)都被指定为提交。这意味着事务只能看到超过水平线的当前行版本。 虚拟事务没有真正的“事物id”,但它们仍然像常规事务一样使用快照,因此它们也有自己的水平线。而且它们是没有活动快照的虚拟事物。水平线的概念对它们没有任何意义,当涉及到快照和可见性时,它们对系统来说完全是透明的。 我们也可以以类似的方式定义数据库的水平线,出于这个目的,我们应该取这个数据库中所有事务的水平线,选择具有最老xmin的。超出这个水平线,该数据库中过期的堆元组将永远不会对任何事务可见。这些元组可以安全的由vacuum维护清理。如下图: Read Committed隔离级别中的真实事物,一样会持有数据库的水平线,vacuum延迟处理,即使事物是idle in transaction状态。 Read Committed隔离级别的虚拟事务仅在执行操作时保持水平线。 整个数据库只有一个水平线,如果它被一个事务持有,则不能清空这个水平内的任何数据(即使这个事务不访问这些数据)。 理想情况下,应该避免将长事务和频繁更新(这会产生新行版本)同时在一个库中,因为这会导致表和索引膨胀。 即使开启一个REPEATABLE READ事物,系统目录也要保持最新,包含了表定义的最新更改和添加的完整性约束,否则我们看到的就是过时的表定义或者是丢失新添加的完整性约束。如下示例: 如果我们先插入数据,再添加非空约束会怎样?如下: 由上面示例可见,数据库好像在每一次系统目录被查询的时候都会创建单独的快照,实际上,该原理远不止我们看到的这么简单,因为频繁的创建快照会对性能有影响,而且,很多系统目录是被缓存的。 在某些情况下,并发事务需要看到同一个快照。例如,如果pg_dump以并行模式运行,那么它的所有进程必须看到相同的数据库状态,才能生成一致的备份。 我们不能因为事务是“同时”启动的就假定快照是相同的。为了确保所有事务都看到相同的数据,这里必须使用快照导出机制。 使用pg_export_snapshot函数导出一个快照,并可以把快照传递给另一个事务: 看一个例子: 另一个session使用该导出的快照,可以看到accounts表即使已经删除了,但是我们还是6行 导出的快照的生命周期与导出事务的生命周期相同,也就是说导出快照的事物提交后,就无法再使用该快照了,但是还在使用该快照的事物,可以正常使用。 参考:
如果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)
事物水平线
如果一个事务没有活动快照(例如,在语句执行之间的Read Committed隔离级别),如果它被分配,那么它的水平线是由它自己定义的。
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)
根据以上我们得出一些结论:
不管是虚拟事物还是真实的事物,在 Repeatable Read和Serializable隔离级别,长时间运行一个事物,会长时间的保持数据库的水平线,vacuum操作会延迟处理。#开启一个事物,查看当前事物持有水平线
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)
系统目录快照
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
#如果我们先开启一个事物,插入一行带有空的数据
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;
快照导出
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)
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)
https://habr.com/en/company/postgrespro/blog/479512/