在查看PostgreSQL-11.5官方文档的时候发现,pg中repeatable read隔离级别下是不会出现幻读的。如下图标红处所示:
PostgreSQL事务隔离级别
什么是幻读? 下面是官方的解析:
phantom read
A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.
大概意思指在一个事务中相同的SQL查询条件前后读取到的结果不一致,原因是后者读取到了其他事务中新提交的数据。
数据读取,实际上读取的是一种状态数据;数据库中所说的“事务”,可以理解为数据在某个时间的一致性状态,每当产生一个新的事务,数据将从上一个事务的状态进入到新事务中的状态。“事务快照”本质上代表的是某个时刻数据状态的一个定格。
一、事务快照介绍
事务快照transaction snapshot,表示某个时刻事务的状态。既然是状态,我在这里对事务快照的理解为三个阶段:一个transaction snapshot将事务划分为过去的、当前的、未来的三个范围区域。
PostgreSQL中事务快照的状态信息包括如下内容:
xmin,当前处于active状态的最小事务编号;
xmax,未来产生的事务中,第一个被分配的事务编号;
xip_list,当前处于active 状态的事务列表。
如下,查看当前时刻事务快照:
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
639:642:639,640 <<<事务快照格式:xmin:xmax:xip_list
xmin=639,表示当前快照时刻最小的事务是639这个事务。小于该编号的事务都已经终止(提交、回滚或异常终止),这些事务属于“过去的”范围区域。
xmax=642,表示将来新事务产生时分配到的第一个事务编号txid,大于等于642的事务未产生,属于“将来的”范围区域。
xip_list=(639,640),表示该快照时刻639和640这两个事务正处于active状态,属于“当前的”范围区域。
下面画个图表示:
transaction snapshot examples
二、事务快照有什么用?
一个重要作用就是对某个行记录tuple版本是否可见的重要判断依据。
pg默认的事务隔离级别transaction isolation为read committed,
在read committed级别下,session中同一事务的每一条SQL语句执行的时候都会自动读取当前时刻事务快照;而在repeatable read级别下,session中同一事务只会在事务开始的第一个SQL获取一次事务快照。
这是read committed事务级别下可能产生幻读的原因,也是repeatable read可以实现重复读的原因。
因为read committed级别下,同一事务中不同时刻的SQL获取的快照可能不一样,因此读到的数据可能会不一样;
而repeatable read在整个事务周期只获取一次事务快照,所以同一事务内所有SQL读取的快照信息都是一致的,因此可以实现重复读,规避了幻读的产生。
关于PostgreSQL的事务隔离级别,可查看文章:
从Oracle到PG-PostgreSQL的事务隔离模式
三、提交读、可重复读模式下事务快照对比测试
下面针对read committed和repeatable read两种主要的事务隔离模式下的事务快照进行对比测试,例子如下:
1.T1时间段:
session 1开启事务txid=666,并insert插入tuple 2,获取到的快照为 666:666:,并且在查询结果中能看到tuple2。
session 2在read committed隔离模式下开启事务txid=674;
session 3在可重复读repeatable read隔离模式下开启事务txid=675;
session 4开启事务txid=676。
1)事务开始前table01中只有一行记录:tuple 1
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 row)
(postgres@[local]:5432)[akendb01]#
2)session 1在默认提交读模式下开启事务txid=666,并insert插入tuple 2
(postgres@[local]:5432)[akendb01]#begin;
BEGIN
(postgres@[local]:5432)[akendb01]#show default_transaction_isolation;
default_transaction_isolation
-------------------------------
read committed
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select txid_current();
txid_current
--------------
666
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:666:
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#insert into table01 values(2,'aken02');
INSERT 0 1
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select id,name, ctid,xmin,xmax from public.table01;
id | name | ctid | xmin | xmax
----+--------+-------+------+------
1 | aken01 | (0,1) | 663 | 0
2 | aken02 | (0,2) | 666 | 0
(2 rows)
(postgres@[local]:5432)[akendb01]#
3)session 2:在提交读隔离级别下开启事务txid=674
(postgres@[local]:5432)[akendb01]#start transaction isolation level read committed;
START TRANSACTION
(postgres@[local]:5432)[akendb01]#select txid_current();
txid_current
--------------
674
(1 row)
4)session 3:在可重复读隔离级别下开启事务txid=675
(postgres@[local]:5432)[akendb01]#start transaction isolation level repeatable read;
START TRANSACTION
(postgres@[local]:5432)[akendb01]#select txid_current();
txid_current
--------------
675
(1 row)
5)session 4:开启事务txid=676
(postgres@[local]:5432)[akendb01]#begin;
BEGIN
(postgres@[local]:5432)[akendb01]#select txid_current();
txid_current
--------------
676
(1 row)
2.T2时间段,session 1、2、3对table01执行select语句。
1)session 1:
session 1在事务txid=666中获取的事务快照为'666:676:674,675',查看结果中可以看到自己新插入的tuple 2。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:676:674,675 <<< 实际上txid=676在session 4已经产生,这个和官网将xmax解析为将来产生的第一个事务有矛盾,我的理解是pg获取事务快照时最后一个txid会滞后
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
(postgres@[local]:5432)[akendb01]#
下图是官网对事务快照函数txid_current_snapshot的原文解析:
Table 9.75. Snapshot Components
详细介绍见:https://www.postgresql.org/docs/current/functions-info.html
2)session 2:
session 2在事务txid=674中获取的事务快照为'666:676:666,675',查看结果中看不到事务txid=666新插入的tuple 2。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:676:666,675
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#
3)session 3:
session 3在事务txid=675中获取的事务快照为'666:676:666,674',查看结果中看不到事务txid=666新插入的tuple 2。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:676:666,674
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#
3.T3时间段:
session 1提交,session 1、session 2、session 3对表table01执行select语句。
1)session 1:
(postgres@[local]:5432)[akendb01]#commit;
COMMITTED
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
674:676:674,675
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
(postgres@[local]:5432)[akendb01]#
2)session 2:
因为是read committed隔离模式,txid=666提交后从xip_list中消失,事务快照为 674:676:675,查询结果可以看到tuple2,和事务开始时结果不一致,出现幻读。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
674:676:675
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
(postgres@[local]:5432)[akendb01]#
3)session 3:
因为是repeatable read隔离模式,txid=666仍然当成active状态看待,事务快照依旧为 666:674:666,674,看不到tuple2,查询结果和事务开始时保持一致,无幻读现象发生。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:676:666,674 <<<session 1的事务666即使已经成功提交,但在repeatable read隔离模式的快照中依旧为active状态,整个事务快照不变。
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 row)
(postgres@[local]:5432)[akendb01]#
4.T4时间段
session 2、session 3事务结束,session 1、2、3读取到的事务快照都为“676:676:”,且查询结果相同。
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
676:676: <<<xip_list为空,xmin=xmax,表示当前快照无活跃事务,未来产生的第一个事务为676.(实际上txid=676在session 4已经产生,我的理解是pg获取事务快照时最后一个txid会滞后)
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
查看Aken-甘植恳更过相关文章:
https://www.toutiao.com/c/user/54536888148/#mid=1610143870006285