PostgreSQL采用多版本并发控制(MVCC)来维护数据的一致性。检索数据时,每个事务看到的都只是一段时间之前的数据快照。MVCC并不能解决所有的并发控制情况,需要使用传统数据库的锁机制来保证事务的并发,因此在PostgreSQL里也有表和行级别的锁定机制。此外PostgreSQL还提供了会话锁机制,可以利用它一次对某个对象加锁保证对于多个事务都有效。

事务隔离级别

标准事务隔离级别

三个必须在并行的事务之间避免的现象:

  • 脏读:一个事务读取了另一个未提交的并行事务写的数据

  • 不可重复读:一个事务对一个数据前后读取两次,发现该数据已经被另一个已提交的数据修改过

  • 幻读:事务A读取某一范围内的数据行时,事务B在该范围内插入新行,当事务A再次读取该范围内的数据时无法查询到新增的数据

四个事务隔离级别:

隔离级别 脏读 不可重复读 幻读
读未提交
读已提交
可重复读
可串行化

PostgreSQL的隔离级别

在PostgreSQL中,可以请求以上四种事务隔离级别的任意一种。但是在内部,只有两种独立的隔离级别:读已提交和可串行化。即选择读未提交的隔离级别,实际用的是读已提交。下面介绍PostgreSQL定义的两种隔离级别:

  • 读已提交:缺省隔离级别。当一个事务运行在这个隔离级别时,一个SELECT查询只能看到查询开始之前提交的数据。如果两个事务在对同一元组进行更新,如果第一个事务回滚,则忽略其作用,第二个事务继续更新该元组;第二个事务则等待第一个事务提交或回滚。如果第一个事务提交,系统重新计算查询条件,如果符合则继续更新操作。

  • 可串行化:提供了最严格的事务隔离。模拟串行的事务执行。如果两个事务在对同一元组进行更新,第二个事务则等待第一个事务提交或回滚。如果第一个事务回滚,则忽略其作用,第二个事务继续更新该元组;如果第一个事务提交,那么可串行化事务回滚,从头开始进行整个事务。

在PostgreSQL系统中,事务的隔离级别所涉及的最小对象是元组,所以对元组的操作需要实施访问控制。这个操作是通过锁操作以及MVCC相关的操作来实现的

MVCC架构

在内部,PostgreSQL利用多版本并发控制(MVCC,MultiVersion Concurrency Control)来维护数据的一致性。即当检索数据时,每个事务看到的只是一段时间之前的事务快照,而不是数据的当前状态。这样对每个数据库会话进行事务隔离,就可以避免一个事务看到其他并发时的更新导致不一致的数据

元组相关数据结构

在PostgreSQL系统中,更新数据并不是用新值覆盖旧值,而是在表中开辟一片空间来存放新的元组,新值与旧值同时存在于数据库中,只是通过设置一些参数让系统可以识别他们

元组的事务和命令控制信息存储在HeapTupleFields

1
2
3
4
5
6
7
8
9
10
11
typedef struct HeapTupleFields
{
TransactionId t_xmin; // 创建此tuple的XID
TransactionId t_xmax; // 删除此tuple的XID

union
{
CommandId t_cid; // 创建或删除tuple的CID,也可能二者都保存
TransactionId t_xvac; // 清理操作的事务ID
}t_field3;
} HeapTupleFields;
  • 如果一个事务确实创建并删除了同一个元组,则使用一个Combo Command ID 来保存Cmin和Cmax

元组的相关控制信息存储在元组的头部HeapTupleHeaderData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;

ItemPointerData t_ctid; // 本元组或更新元组的当前TID
uint16 t_infomask2; // 属性、标记位数量标记位
uint16 t_infomask; // 元组事务信息标记位
uint8 t_hoff; // 头部长度
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; // 标记作用的填充位
};
  • 其中,t_ctid当元组被保存在磁盘中时被初始化为自己的实际存储位置。如果元组被更新,t_cid指向更新后的新元组。如果要找到某个元组的最新版本,只需遍历由t_ctid构成的链表即可

  • t_infomask字段表示当前元组的事务信息

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
#define HEAP_HASNULL			0x0001	// 空字段标记位
#define HEAP_HASVARWIDTH 0x0002 // 变长字段标记位
#define HEAP_HASEXTERNAL 0x0004 // 外部存储字段标记位
#define HEAP_HASOID_OLD 0x0008 // 有OID字段
#define HEAP_XMAX_KEYSHR_LOCK 0x0010 // xmax是共享锁
#define HEAP_COMBOCID 0x0020 // t_cid是combo cid
#define HEAP_XMAX_EXCL_LOCK 0x0040 // xmax是排他锁
#define HEAP_XMAX_LOCK_ONLY 0x0080 // xmax如果有效则只是一个锁

// xmax is a shared locker
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)

#define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \
HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMIN_COMMITTED 0x0100 // t_xmin已提交
#define HEAP_XMIN_INVALID 0x0200 // t_xmin无效/中断
#define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
#define HEAP_XMAX_COMMITTED 0x0400 // t_xmax已提交
#define HEAP_XMAX_INVALID 0x0800 // t_xmax无效/中断
#define HEAP_XMAX_IS_MULTI 0x1000 // t_xmax是组合事务
#define HEAP_UPDATED 0x2000 // 更新后的新元组
#define HEAP_MOVED_OFF 0x4000 // 被之前版本的VACUUM FULL移到其他地方,用以兼容二进制升级
#define HEAP_MOVED_IN 0x8000 // 被之前版本的VACUUM FULL从其他地方移入,用以兼容二进制升级
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)

#define HEAP_XACT_MASK 0xFFF0 // 可见性相关标记

MVCC

MVCC基本原理如图。有两个并发事务T1,T2,T1将元组C更新为C‘,但并没有提交。此时T2要对该元组进行查询,会通过C和C’的头部信息的Xmin和Xmax以及t_infomask来判断哪个为对当前事务的有效版本

MVCC基本原理

MVCC与快照

讨论MVCC的判断逻辑之前,我们需要先了解快照(snapshot)

快照(snapshot)记录了数据库当前某个时刻的活跃事务列表,通过快照确定某个元组的版本对于当前快照是否可见。快照定义在SnapshotData

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
typedef struct SnapshotData
{
SnapshotType snapshot_type; // 快照类型

TransactionId xmin; // 所有XID < xmin对当前快照可见
TransactionId xmax; // 所有XID >= xmax对当前快照可见

TransactionId *xip; // 当前活跃事务的链表
uint32 xcnt; // 当前活跃事务链表长度

TransactionId *subxip; // 当前活跃子事务链表
int32 subxcnt; // 当前活跃子事务链表的长度
bool suboverflowed; // 活跃子事务数组是否移除

bool takenDuringRecovery; // 是否是在Recovery中的快照
bool copied; // 静态快照则为false

CommandId curcid; // 所有CID < curcid是可见的

// HeapTupleSatisfiesDirty中的额外返回值,并没有在MVCC快照中使用
uint32 speculativeToken;

// 由快照管理器使用的信息
uint32 active_count; // 在活跃快照链表里的引用计数
uint32 regd_count; // 在已注册的快照链表里的引用计数
pairingheap_node ph_node; // 已注册的快照链表

TimestampTz whenTaken; // 记录快照的时间戳
XLogRecPtr lsn; // 记录快照时在WAL中的位置
} SnapshotData;

在PostgreSQL中有默认的7种形式快照(15.1),分别是:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
typedef enum SnapshotType
{
// 元组对于给定的MVCC快照有效
SNAPSHOT_MVCC = 0,

// 元组对于自身有效
SNAPSHOT_SELF,

// 任何元组都是可见的
SNAPSHOT_ANY,

// 元组作为TOAST行 有效
SNAPSHOT_TOAST,

// 下面三个不太好解释,直接贴原文
/*-------------------------------------------------------------------------
* A tuple is visible iff the tuple is valid including effects of open
* transactions.
*
* Here, we consider the effects of:
* - all committed and in-progress transactions (as of the current instant)
* - previous commands of this transaction
* - changes made by the current command
*
* This is essentially like SNAPSHOT_SELF as far as effects of the current
* transaction and committed/aborted xacts are concerned. However, it
* also includes the effects of other xacts still in progress.
*
* A special hack is that when a snapshot of this type is used to
* determine tuple visibility, the passed-in snapshot struct is used as an
* output argument to return the xids of concurrent xacts that affected
* the tuple. snapshot->xmin is set to the tuple's xmin if that is
* another transaction that's still in progress; or to
* InvalidTransactionId if the tuple's xmin is committed good, committed
* dead, or my own xact. Similarly for snapshot->xmax and the tuple's
* xmax. If the tuple was inserted speculatively, meaning that the
* inserter might still back down on the insertion without aborting the
* whole transaction, the associated token is also returned in
* snapshot->speculativeToken. See also InitDirtySnapshot().
* -------------------------------------------------------------------------
*/
SNAPSHOT_DIRTY,

/*
* A tuple is visible iff it follows the rules of SNAPSHOT_MVCC, but
* supports being called in timetravel context (for decoding catalog
* contents in the context of logical decoding).
*/
// 元组对MVCC快照有效,且支持逻辑复制
SNAPSHOT_HISTORIC_MVCC,

/*
* A tuple is visible iff the tuple might be visible to some transaction;
* false if it's surely dead to everyone, i.e., vacuumable.
*
* For visibility checks snapshot->min must have been set up with the xmin
* horizon to use.
*/
// 元组对某些事务可见,如果对所有事务都是dead tuple,也就是vacuumable,则返回false
SNAPSHOT_NON_VACUUMABLE
} SnapshotType;

MVCC机制的实现

以函数HeapTupleSatisfiesSelf为例,下面介绍MVCC机制的具体实现。如果返回值为True,则该元组是可见的。判断时会考虑三个方面的因素:所有已提交的事务,当前事务前的所有命令以及当前命令前的所有操作。执行过程如下:

  • 检查Xmin是否为已提交,如果元组的Xmin还未提交

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Xmin未提交
if (!HeapTupleHeaderXminCommitted(tuple))
{
// Xmin无效,元组不可见
if (HeapTupleHeaderXminInvalid(tuple))
return false;

// 兼容二进制升级
if (tuple->t_infomask & HEAP_MOVED_OFF)
{
...
}
// 兼容二进制升级
else if (tuple->t_infomask & HEAP_MOVED_IN)
{
...
}
// Xmin为当前事务
else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple)))
{
// Xmax无效,则XID无效,可见
if (tuple->t_infomask & HEAP_XMAX_INVALID)
return true;

// Xmax被锁定,即元组被锁定,可见
if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
return true;

// Xmax是组合事务
if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
{
xmax = HeapTupleGetUpdateXid(tuple);
// 更新子事务必须已回滚,因为前提是Xmin未提交
if (!TransactionIdIsCurrentTransactionId(xmax))
return true;
else
return false;
}

// Xmax为当前事务
if (!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple)))
{
// 删除子事务必须已终止
SetHintBits(tuple, buffer, HEAP_XMAX_INVALID,
InvalidTransactionId);
return true;
}

return false;
}
// Xmin正在某个后端进程中运行
else if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmin(tuple)))
return false;
// Xmin已经被提交
else if (TransactionIdDidCommit(HeapTupleHeaderGetRawXmin(tuple)))
SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,
HeapTupleHeaderGetRawXmin(tuple));
else
{
// 否则一定是中止或崩溃
SetHintBits(tuple, buffer, HEAP_XMIN_INVALID,
InvalidTransactionId);
return false;
}
}
  • Xmin已提交。如果Xmax无效或中断,元组可见

1
2
if (tuple->t_infomask & HEAP_XMAX_INVALID)
return true;
  • Xmin已提交,Xmax已提交

1
2
3
4
5
6
7
if (tuple->t_infomask & HEAP_XMAX_COMMITTED)
{
// 如果Xmax被锁定,可见
if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
return true;
return false;
}
  • Xmin已提交,Xmax为组合事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
{
TransactionId xmax;

// Xmax被锁定,可见
if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
return true;

xmax = HeapTupleGetUpdateXid(tuple);

// Xmax是当前事务
if (TransactionIdIsCurrentTransactionId(xmax))
return false;
// Xmax正在被某个后端进程执行
if (TransactionIdIsInProgress(xmax))
return true;
// Xmax已经被提交
if (TransactionIdDidCommit(xmax))
return false;
// 否则事务被中断或崩溃
return true;
}
  • Xmin已提交,Xmax不是组合事务

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
// Xmax是当前事务
if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple)))
{
if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
return true;
return false;
}
// Xmax正在被某个后端进程执行
if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmax(tuple)))
return true;
// Xmax已经被提交
if (!TransactionIdDidCommit(HeapTupleHeaderGetRawXmax(tuple)))
{
// it must have aborted or crashed
SetHintBits(tuple, buffer, HEAP_XMAX_INVALID,
InvalidTransactionId);
return true;
}
// Xmax被锁定
if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
{
SetHintBits(tuple, buffer, HEAP_XMAX_INVALID,
InvalidTransactionId);
return true;
}

SNAPSHOT_SELF所对应的MVCC判断机制如上,其他snapshot类型对应的判断逻辑类似,就不再详细介绍了