Postgresql中的MVCC与并发

2022-05-12 10:04:53 浏览数 (1)

1 MVCC

ACID中的C即一致性在PG内部使用MVCC机制来保证。MVCC多版本并发控制为数据加上时间戳,读写需要额外的根据自身时间戳与数据时间戳对比,按照既定的规则可以知道这条数据对当前的SQL是否可见。MVCC避免了传统的锁方法,将锁竞争最小化来获得更高的性能。

使用MVCC模型的主要优点是查询(读)数据的锁请求与写数据的锁请求不冲突,所以读不会阻塞写,写也从不阻塞读。另外在PG里也有表和行级别的锁功能,用于需要显式锁定的场景。

  • 脏读:一个事务读取了另一个并行未提交事务写入的数据。
  • 不可重复读:一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。
  • 幻读:一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合因为另一个最近提交的事务而发生了改变。
  • 序列化异常:成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。

2 隔离级别

SQL标准定义了四种隔离级别。最严格的是可序列化,可序列化事务的任意并发执行的效果 保证和 以某种顺序一个一个执行这些事务得到的结果一样。

隔离级别

脏读

不可重复读

幻读

序列化异常

读未提交

允许,但不在 PG 中

可能

可能

可能

读已提交

不可能

可能

可能

可能

可重复读

不可能

不可能

允许,但不在 PG 中

可能

可序列化

不可能

不可能

不可能

不可能

2.1 读已提交

读已提交是PostgreSQL中的默认隔离级别。 当一个事务运行使用这个隔离级别时, 一个查询只能看到查询开始之前已经被提交的数据。

例子:

代码语言:javascript复制
-- sesscon1
create table tbl1 (i int);
insert into tbl1 values (1);

-- session2
begin;
select * from tbl1;
 i
---
 1

-- session1
insert into tbl1 values (2);

-- session2 在刚才的事务内继续执行
select * from tbl1;
 i
---
 1
 2

-- session1
update tbl1 set i=10 where i=1;

-- session2 在刚才的事务内继续执行
select * from tbl1;
 i
----
  2
 10

可以看到在事务2内执行同样的一条SQL出现了不同的结果,但是没有提交的事务中的数据是看不到的(这里没有测这种情况)。注意一下这里有两种不一样的结果,第一种是数据变了(不可重复读),第二种是多了一行数据(幻读)。

读已提交最重要的一点需要记住:事务中的每条SQL会重新获取数据库快照,所以每次看到的DB是不同的,但每次看到的DB一定是一致的!

2.2 可重复读 / 可序列化

这两个隔离级别放在一块讨论。这两个隔离级别与读已提交不同之处在于:

  • 查询可以看见在事务中第一个非事务控制语句开始时的一个快照,而不是事务中当前语句开始时的快照。
  • 在一个单一事务中的后续SELECT命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。

简单来说就是事务开始后的第一条语句会拿到一个快照,后面的语句都使用这个快照!

每条SQL严格保证看到的是同一个一致性视图。

例如:

代码语言:javascript复制
--session1
create table tbl1 (i int);
insert into tbl1 values (1);

--session2
begin isolation level repeatable read;
BEGIN
select * from tbl1;
 i
---
 1
(1 row)

--session1
insert into tbl1 values (2);

--session2继续刚才的事务执行
select * from tbl1;
 i
---
 1
 
--session1
update tbl1 set i=10 where i=1;

--session2继续刚才的事务执行
select * from tbl1;
 i
---
 1

从事务2可以看到没有出现不可重复读 和 幻读的现象。

2.3 总结

多事务并发下可能会出现很复杂的场景,例如update多行、delete多行等,对于不同的事务隔离级别并发的结果要区别分析,但使用要牢记两点:

  1. 读已提交的每条SQL都会重新拿快照
  2. 可重复读、可串行化只在第一条非控制语句拿快照,后面不再重新获取快照

3 PG的MVCC实现

3.1 MVCC判断可见性的原理

这里我们简单介绍MVCC的理论实现,为后面理解PG实现MVCC打下基础。

使用时间戳的并发控制

  • 除了使用锁以外,可以使用时间戳的方式保证事务了串行。时间戳方式会为每个事务分配一个时间戳,将这些数值与事务时间戳比较,根据事务的时间戳确保串行调度等价与实际事务调度。
  • 这种方式假设没有非可串行化的行为发生,只在例外是进行修复,所以这种方式是“乐观”的(终止事务,但不推迟)。相对于锁的“悲观”方式来看(推迟事务,但不终止),当很多事务只读时乐观的调度器要比悲观的锁机制要好,因为读事务不会发生非可串行化的行为。

时间戳机制简述(详细内容请参考《数据库系统实现》7.8)

  • 调度系统为每个事务分配一个时间戳TS(唯一、递增)
  • 每一个数据库元素增加附加位记录时间戳
代码语言:txt复制
- RT(X) —— X的读时间
- WT(X) —— X的写时间
- C(X) —— X的提交位(真表示事务已提交)

放个书中的例子(《数据库系统实现》237页)

多版本时间戳

多版本时间戳的方式是在时间戳的基础上保留了数据库元素的旧版本,目的是允许一些情况下本来要导致事务终止的读操作继续进行,这是通过让事务从旧版本中找到适合它的数据版本来实现的。

在PG中事务ID可以理解为时间戳(递增、唯一),PG中的MVCC即实现了上述多版本时间戳的串行控制方法,本质上是为了在数据库并发执行事务时,保证整体数据的一致性

3.2 事务ID

事务ID在PG源码中的定义

代码语言:javascript复制
typedef uint32 TransactionId;

Postgresql中使用永远递增(在32位uint范围内)的TransactionId来作为元组、事务的时间戳,txid规定使用的最小值为3,0到2为保留位有特殊用途:

  • txid = 0: Invaild 无效xid
  • txid = 1: Bootstrap 系统初始化时使用的xid
  • txid = 2: Forzen 冻结xid,请见后面的章节

我们可以把事务ID理解为时间戳,在mvcc中,时间戳大的可以理解为在未来,时间戳小的可以理解为在过去。正常来说你应该只能看到发生过的事件(拥有比你小的时间戳),而不应该看到在未来的事件(拥有比你大的时间戳)。

PG中事务ID会持续递增,一个简单的例子,每一个不在事务块中的SQL语句都会自成一个事务,一般写操作与txid_current()会消耗一个事务ID。

代码语言:javascript复制
postgres=# select txid_current();
 txid_current
--------------
         2040
(1 row)

postgres=# select txid_current();
 txid_current
--------------
         2041
(1 row)

postgres=# select txid_current();
 txid_current
--------------
         2042
(1 row)

读操作与写操作:

代码语言:javascript复制
postgres=# select txid_current();
 txid_current
--------------
         2062
(1 row)

postgres=# select * from sssa;
 i
---
(0 rows)

postgres=# select txid_current();
 txid_current
--------------
         2063
(1 row)

postgres=# insert into sssa values (1);
INSERT 0 1
postgres=# select txid_current();
 txid_current
--------------
         2065
(1 row)

事务内的所有操作的事务ID是相同的

代码语言:javascript复制
postgres=# begin;
BEGIN
postgres=# select txid_current();
 txid_current
--------------
         2066
(1 row)

postgres=# insert into sssa values (2);
INSERT 0 1
postgres=# select txid_current();
 txid_current
--------------
         2066
(1 row)

postgres=# end;
COMMIT
postgres=# select txid_current();
 txid_current
--------------
         2067
(1 row)

事务ID使用uint32位无符号整数记录,这就意味着事务ID会有用完的情况,uint32最大值可以到4294967295(2^32-1),当事务ID到最大值会发生什么情况呢?

3.3 事务ID回卷

当事务ID达到4294967295(2^32-1)时,后面 1会是整型溢出归零,PG中可用的最小xid=3,生了溢出后,事务ID继续增长,当事务ID差值达到20左右后,PG就无法判断哪个事务在先,哪个事务在后了!

代码语言:javascript复制
90 --> 100 --> ... --> 20亿 --> 30亿 --> 42亿 --> 3 --> 4 --> 5 --> ...
                        |<-----------距离超过20亿------------>|
                        |---------无法正确判断事物先后!--------|

上述就是PG的事务ID回卷问题,PG中使用Freeze来避免这类问题发生。Freeze的思想很简单,使用事务ID=2表示一类特殊的事务,这类事务表示冻结ID,他比任何事务ID都要老,对于任何事务ID都是可见的。

PG中具体执行freeze动作的是vacuum进程,vacuum会扫描表中的元组,如果发现元组的t_xmin比vacuum_freeze_min_age大,就执行freeze动作,标记该元组为frozen元祖。

  • vacuum_freeze_min_age (integer): Specifies the cutoff age (in transactions) that VACUUM should use to decide whether to freeze row versions while scanning a table. https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-VACUUM-FREEZE-MIN-AGE

3.4 Tuple实现MVCC

3.4.1 Tuple内部结构

数据的基本单位tuple中对实现mvcc机制增加了一些额外的数据项,所有的tuple构造方式都遵循下面规则:

  • 定长的头部
  • 可选的空值位图
  • 字节对齐padding
  • 可选的对象 ID 域
  • 用户数据

头部保存了mvcc机制需要的信息,HeapTupleHeaderData布局

类型

长度

描述

t_xmin

TransactionId

4 bytes

插入XID标志

t_xmax

TransactionId

4 bytes

删除XID标志

t_cid

CommandId

4 bytes

插入和/或删除CID标志(覆盖t_xvac)

t_xvac

TransactionId

4 bytes

VACUUM操作移动一个行版本的XID

t_ctid

ItemPointerData

6 bytes

当前版本的TID或者指向更新的行版本

t_infomask2

uint16

2 bytes

一些属性,加上多个标志位

t_infomask

uint16

2 bytes

多个标志位

| t_hoff | uint8 | 1 byte | 到用户数据的偏移量

HeapTupleHeaderData定义

代码语言:javascript复制
struct HeapTupleHeaderData
{
	union
	{
		HeapTupleFields t_heap;
		DatumTupleFields t_datum;
	}			t_choice;

	ItemPointerData t_ctid;		/* current TID of this or newer tuple (or a
								 * speculative insertion token) */

	/* Fields below here must match MinimalTupleData! */

	uint16		t_infomask2;	/* number of attributes   various flags */

	uint16		t_infomask;		/* various flag bits, see below */

	uint8		t_hoff;			/* sizeof header incl. bitmap, padding */

	/* ^ - 23 bytes - ^ */

	bits8		t_bits[FLEXIBLE_ARRAY_MEMBER];	/* bitmap of NULLs */

	/* MORE DATA FOLLOWS AT END OF STRUCT */
};

更详细的信息可以在htup_details.h文件的注释中找到。

代码语言:javascript复制
/*
 * Heap tuple header.  To avoid wasting space, the fields should be
 * laid out in such a way as to avoid structure padding.
 *
...
 * The overall structure of a heap tuple looks like:
 *			fixed fields (HeapTupleHeaderData struct)
 *			nulls bitmap (if HEAP_HASNULL is set in t_infomask)
 *			alignment padding (as needed to make user data MAXALIGN'd)
 *			object ID (if HEAP_HASOID is set in t_infomask)
 *			user data fields
 *
...
 */

Postgresql提供pageinspect插件来分析页面、元组的状态,插件使用方便我们来看一个例子。

代码语言:javascript复制
create extension pageinspect;
create table test_tbl(a int, b int, c text);
insert into test_tbl values (1,100,'test');
SELECT * FROM page_header(get_raw_page('test_tbl', 0));
    lsn     | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------ ---------- ------- ------- ------- --------- ---------- --------- -----------
 7/A8DC2760 |        0 |     0 |    28 |  8152 |    8192 |     8192 |       4 |         0

观察元组信息

代码语言:javascript复制
select * from heap_page_items(get_raw_page('test_tbl', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin  | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |            t_data
---- -------- ---------- -------- --------- -------- ---------- -------- ------------- ------------ -------- -------- ------- ------------------------------
  1 |   8152 |        1 |     37 | 3081402 |      0 |        0 | (0,1)  |           3 |       2050 |     24 |        |       | x01000000640000000b74657374

对照上面表格,可以得到当前元组的具体信息:

  • lp: tuple在当前页面中的索引
  • lp_off: tuple在当前页面中的偏移量
  • lp_len: tuple长度
  • t_xmin: 创建者的事务ID
  • t_xmax: 删除者的事务ID
  • t_field3: 事务状态、vacuum使用的标志位
  • t_ctid: 当前tid
  • t_infomask2: 标志位
  • t_infomask: 标志位
  • t_hoff: tuple中用户数据的偏移量
3.4.2 Tuple的增删改流程

pageinspect插件可以帮助我们分析页面内部的数据情况,这里我们分别看下增、删、改对页面的影响。

INSERT

t_xmin:当前的事务ID已经使用到了3081404

t_xman:数据没有被删除或者更新

t_ctid:指向自己说明没有更新过

代码语言:javascript复制
create table test_insert (id int, info text);
insert into test_insert values (1, 'information');
select * from heap_page_items(get_raw_page('test_insert', 0));
-[ RECORD 1 ]-----------------------------------
lp          | 1
lp_off      | 8152
lp_flags    | 1
lp_len      | 40
t_xmin      | 3081404
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x0100000019696e666f726d6174696f6e

DELETE

t_xmax:数据被事务ID为3081405的SQL删除

vacuum后数据才被回收,delete并不直接回收数据,只做标记

代码语言:javascript复制
delete from test_insert;
select * from heap_page_items(get_raw_page('test_insert', 0));
-[ RECORD 1 ]-----------------------------------
lp          | 1
lp_off      | 8152
lp_flags    | 1
lp_len      | 40
t_xmin      | 3081404
t_xmax      | 3081405
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 8194
t_infomask  | 258
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x0100000019696e666f726d6174696f6e

vacuum test_insert;

select * from heap_page_items(get_raw_page('test_insert', 0));
ERROR:  block number 0 is out of range for relation "test_insert"

UPDATE

下面插入一条数据后,进行了两次update。

代码语言:javascript复制
insert into test_insert values (2, 'information');

select * from heap_page_items(get_raw_page('test_insert', 0));
-[ RECORD 1 ]-----------------------------------
lp          | 1
lp_off      | 8152
lp_flags    | 1
lp_len      | 40
t_xmin      | 3081407
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x0200000019696e666f726d6174696f6e

update test_insert set info='updated information' where id=2;

select * from heap_page_items(get_raw_page('test_insert', 0));
-[ RECORD 1 ]---------------------------------------------------
lp          | 1
lp_off      | 8152
lp_flags    | 1
lp_len      | 40
t_xmin      | 3081407
t_xmax      | 3081408
t_field3    | 0
t_ctid      | (0,2)
t_infomask2 | 16386
t_infomask  | 258
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x0200000019696e666f726d6174696f6e
-[ RECORD 2 ]---------------------------------------------------
lp          | 2
lp_off      | 8104
lp_flags    | 1
lp_len      | 48
t_xmin      | 3081408
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,2)
t_infomask2 | 32770
t_infomask  | 10242
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x02000000297570646174656420696e666f726d6174696f6e

update test_insert set info='updated information3' where id=2;

select * from heap_page_items(get_raw_page('test_insert', 0));
-[ RECORD 1 ]-----------------------------------------------------
lp          | 1
lp_off      | 8152
lp_flags    | 1
lp_len      | 40
t_xmin      | 3081407
t_xmax      | 3081408
t_field3    | 0
t_ctid      | (0,2)
t_infomask2 | 16386
t_infomask  | 1282
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x0200000019696e666f726d6174696f6e
-[ RECORD 2 ]-----------------------------------------------------
lp          | 2
lp_off      | 8104
lp_flags    | 1
lp_len      | 48
t_xmin      | 3081408
t_xmax      | 3081409
t_field3    | 0
t_ctid      | (0,3)
t_infomask2 | 49154
t_infomask  | 8450
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x02000000297570646174656420696e666f726d6174696f6e
-[ RECORD 3 ]-----------------------------------------------------
lp          | 3
lp_off      | 8048
lp_flags    | 1
lp_len      | 49
t_xmin      | 3081409
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,3)
t_infomask2 | 32770
t_infomask  | 10242
t_hoff      | 24
t_bits      |
t_oid       |
t_data      | x020000002b7570646174656420696e666f726d6174696f6e33

update并不会真正的更新某一条数据,而是插入一条新数据后,把就数据标记为删除。总结上述流程:

SQL

t_xmin

t_xmax

t_ctid

data

insert into test_insert values (2, ‘information’);

3081407

0

(0,1)

information

update test_insert set info=‘updated information’ where id=2;

3081407

3081408

(0,2)

information

3081408

0

(0,2)

updated information

update test_insert set info=‘updated information3’ where id=2;

3081407

3081408

(0,2)

information

3081408

3081409

(0,3)

updated information

3081409

0

(0,3)

updated information3’

3.4.3 空闲空间映射表

数据库的增删改都会增加页面内的tuple数量,PG中对于不在使用的tuple进行统一的vacuum回收动作,一个频繁更新的表可以想象经过回收后必然出现很多“空洞”,想使用这些空间的话需要遍历整个页面,这种开销是非常大的。

PG中对于每个表文件,同时构造名为oid_fsm的文件,这类文件记录每个表文件空间的空闲状况。

代码语言:javascript复制
ls  | grep 46843
46843
46843_fsm
46843_vm

FSM内部使用最大堆树来记录表文件的空闲块位置,引用《Postgresql数据库内核分析》的图解:

  • FSM树中的每个块大小为8K,内部保存了空闲空间的大小、页面编号
  • 三层树结构的一、二层不指向具体数据,与B 树类似可以理解为索引
  • 每个FSM块内部维护最大堆的数据结构,跟节点保证为最大的页面空间大小
  • 第三层节点执行具体页面编号

插件pg_freespacemap可以观察表的空闲空间情况

代码语言:javascript复制
CREATE EXTENSION pg_freespacemap;
select * from pg_freespace('test_insert');
 blkno | avail
------- -------
     0 |  8096

3.5 Clog

3.5.1 Clog内部实现

Clog记录数据库事务的最终状态,PG中对于事务状态有四种定义:

代码语言:javascript复制
#define TRANSACTION_STATUS_IN_PROGRESS		0x00
#define TRANSACTION_STATUS_COMMITTED		0x01
#define TRANSACTION_STATUS_ABORTED			0x02
#define TRANSACTION_STATUS_SUB_COMMITTED	0x03

对应:事务执行中、事务已提交、事务已回滚、子事务提交。

CLOG可以理解为使用类似List<Txid, TRANSACTION_STATUS>的形式保存事务状态信息,例如

代码语言:javascript复制
事务100       事务101     Clog状态
                         ------------------------- 
ID:100        ID:101    |ID:100      |ID:101      |
                         ------------------------- 
begin                   |inProcess   |            |  <--- CLog块
                         ------------------------- 
              begin     |inProcess   |inProcess   |  <--- CLog块
                         ------------------------- 
select                  |inProcess   |inProcess   |
                         ------------------------- 
              update    |inProcess   |inProcess   |
                         ------------------------- 
...                     |inProcess   |inProcess   |
                         ------------------------- 
              ...       |inProcess   |inProcess   |
                         ------------------------- 
commit                  |commited    |inProcess   |
                         ------------------------- 
             abort      |            |aborted     |
                         ------------------------- 

事务状态保存在CLOG文件中,文件内部也是按8KB一个页面的结构存储的,CLOG使用独立的SLRU缓冲池,控制CLOG数据块加载到共享内存中。

在查询CLOG时使用(Segmentno, Pageno, Byte, Bindex)四元组可以定位具体的CLOG记录,其中Segmentno为CLOG文件名,Pageno为文件内的段偏移量,Byte为页面中的偏移量,Bindex为字节内的偏移量。

3.5.2 Clog的清理

CLOG清理在pg_database.datfrozenxid更新是会触发,清理条件是根据pg_database.datfrozenxid的位置来计算的,如果全库中最小的datfrozenxid包含在某一个clog文件中,那么更久的clog文件都认为是可以被清理的。

代码语言:javascript复制
postgres=> SELECT datname, datfrozenxid FROM pg_database;
   datname   | datfrozenxid
------------- --------------
 postgres    |   1223384381
 etest       |   1231504552
 template1   |   1281562302
 template0   |   1281621518

[postgres@xxx /home/pgsql/xxx/data/pg_xact]
$ll
total 47428
-rw------- 1 postgres users 262144 May 27 19:07 048E
-rw------- 1 postgres users 262144 May 27 19:07 048F
-rw------- 1 postgres users 262144 May 27 19:12 0490
-rw------- 1 postgres users 262144 May 27 19:12 0491
...
...

3.6 事务快照

上面提到,不同事务隔离级别会使用不同的策略获取数据库快照。这里说的数据库快照可以理解为当前这条语句看到数据库的一个瞬间,注意这个瞬间马上就会过去,执行下一条SQL会看到下一个瞬间。每一个瞬间的数据都会保证一致性。

为实现上述的功能,快照实际保存的是当前时间点所有活跃事务的状态信息。

PG中使用min:xmax:xip_list的结构表示当前的快照信息。数据库当前的快照信息可以使用函数txid_current_snapshot抓取,结合函数txid_current可以观察数据库快照的变化。

代码语言:javascript复制
SELECT txid_current_snapshot();
      txid_current_snapshot
---------------------------------
 3081423:3081431:3081423,3081428
  • xmin = 3081423:表示正在活跃事务最小的事务ID,小于这个ID的事务一定已经结束了。
  • xmax = 3081431:表示还没有分配的最小事务ID,大于等于这个ID的事务一定还没有开始。
  • xip_list = 3081423,3081428:活跃事务ID列表(一定在xmin和xmax之间)

注意:3081425等事务也可能是是非活跃事务,即没有任何写操作的事务对数据库的一致性没有影响,所以和已结束事务归位一类。一旦事务内出现写操作,事务即变为活跃状态。

不同的隔离级别下快照的获取有不同的规则:

  • 读已提交:事务内的每一个SQL执行都会重新拿快照
  • 可重复读/可串行化:事务开始时拿一个快照,后面不再重新获取

事务A

代码语言:javascript复制
postgres=# begin;
BEGIN
postgres=# select * from test;
  id  | time
------ ------
 1024 | 5432
(1 row)

postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081433:3081433:
(1 row)

事务B

代码语言:javascript复制
postgres=# begin;
BEGIN
postgres=# select * from test;
  id  | time
------ ------
 1024 | 5432
(1 row)

postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081433:3081433:
(1 row)

事务A修改表,读已经提交情况下,事务B不应该看到未提交事务造成的的影响,所以不应该看到数据。

代码语言:javascript复制
postgres=# insert into test values (3123,3333);
INSERT 0 1
postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081433:3081433:
(1 row)

事务B查询

代码语言:javascript复制
postgres=# select * from test;
  id  | time
------ ------
 1024 | 5432
(1 row)

postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081433:3081433:
(1 row)

事务A提交

代码语言:javascript复制
postgres=# commit;
COMMIT
postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081434:3081434:
(1 row)

事务B查询数据,最小活跃事务ID变成3081434(就是B事务),所以B事务可以看到已经提交事务3081433写入的数据了。

代码语言:javascript复制
postgres=# select * from test;
  id  | time
------ ------
 1024 | 5432
 3123 | 3333
(2 rows)
postgres=# SELECT txid_current_snapshot();
 txid_current_snapshot
-----------------------
 3081434:3081434:
(1 row)

那么事务B是根据什么原则看到3081433事务写入的数据的呢?下面我们继续看可见判断原则。

3.7 可见性检测

可见性检测的过程就是SQL能否看见一条数据的判断过程,判断依据的数据包括tuple自身的xmin,xman,t_max,Clog数据,快照。

可见性判断是通过一系列复杂的逻辑实现的,源码中对应一系列Satisfied函数实现。

判断规则是非常复杂的这里截取部分代码和规则,更详细的判断逻辑需要具体分析代码。

代码语言:javascript复制
bool
HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot,
					   Buffer buffer)
{
	HeapTupleHeader tuple = htup->t_data;

	Assert(ItemPointerIsValid(&htup->t_self));
	Assert(htup->t_tableOid != InvalidOid);

	if (!HeapTupleHeaderXminCommitted(tuple))
	{
		if (HeapTupleHeaderXminInvalid(tuple))
			return false;

		/* Used by pre-9.0 binary upgrades */
		if (tuple->t_infomask & HEAP_MOVED_OFF)
		{
			TransactionId xvac = HeapTupleHeaderGetXvac(tuple);

			if (TransactionIdIsCurrentTransactionId(xvac))
				return false;
			if (!XidInMVCCSnapshot(xvac, snapshot))
			{
				if (TransactionIdDidCommit(xvac))
				{
					SetHintBits(tuple, buffer, HEAP_XMIN_INVALID,
								InvalidTransactionId);
					return false;
				}
				SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,
							InvalidTransactionId);
			}
		}
...
...

判断规则:

代码语言:javascript复制
Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible
Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible
Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible
Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible
Rule 5: If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible
Rule 6: If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible
Rule 7: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible
Rule 8: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible
Rule 9: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible
Rule 10: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible

3.8 Freeze

有了对tuple、MVCC机制的了解后,我们继续讨论事务ID回卷的问题。事务ID的回卷会直接导致可见性判断错误,对于数据库来说这是致命的问题。

处理这类情况PG中使用Freeze的方式,将tuple的事务ID强制设为2(txid=2冻结专用事务ID),表示该tuple比任何事务ID都要旧,也就是可以对任何事务都可见。

PG中的vacuum进程会将事务ID超过一定值的tuple标记为freezed(在tuple的infomask的标识为中进行标记)。

关于VACUUM更多的分析在这里

https://cloud.tencent.com/developer/article/2000884

0 人点赞