PostgreSQL-并发控制

一、事务标识

当事务开始时,事务管理器会为其分配一个事务标识(txid)的唯一标识符。

PostgreSQL有三个特殊txid:

  • 0标识无效的txid;
  • 1表示初始启动的txid,仅用于数据库集群的初始化过程;
  • 2表示冻结的txid。

txid在逻辑上是无限的,但实际系统的txid空间不足(4B整型的取值空间大小约42亿),因此Postgresql将txid空间视为一个环。对于某个txid,其前21亿个txid属于过去,其后约21亿个txid属于未来。

二、元组结构

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
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID; 插入事务的ID */
TransactionId t_xmax; /* deleting or locking xact ID; 删除或锁定事务的ID */

union
{
CommandId t_cid; /* inserting or deleting command ID, or both; 插入或删除的命令ID */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID; 老式VACUUM FULL的事务ID */
} t_field3;
} HeapTupleFields;

struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;

//当前元组或更新元组的TID
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
* speculative insertion token) */

/* Fields below here must match MinimalTupleData! */

#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */ //主要保存了事务的状态标记位
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */

/* ^ - 23 bytes - ^ */

#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */

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

t_xmin:代表插入此元组的事务xid;

t_xmax:代表更新或者删除此元组的事务xid,如果该元组插入后未进行更新或者删除,t_xmax=0;

t_cid:command id,代表在当前事务中,已经执行过多少条sql,例如执行第一条sql时cid=0,执行第二条sql时cid=1;

t_ctid:保存着指向自身或者新元组的元组标识(tid),由两个数字组成,第一个数字代表物理块号,或者叫页面号,第二个数字代表元组号。在元组更新后tid指向新版本的元组,否则指向自己,这样其实就形成了新旧元组之间的“元组链”,这个链在元组查找和定位上起着重要作用。

三、元组的增、删、改

1、更新元组

左图是一条新插入的元组,可以看到元组是xid=100的事务插入的,没有进行更新,所以t_xmax=0,同时t_ctid指向自己,0号页面的第一号元组。
右图是发生xid=101的事务更新该元组后的状态,更新在pg里相当于插入一条新元组,原来的元组的t_xmax变为了更新这条事务的xid=101,同时t_ctid指针指向了新插入的元组(0,2),0号页面第二号元组,第二号元组的t_xmin=101(插入该元组的xid),t_ctid=(0,2),没有发生更新,指向自己。

2、删除元组

上图代表该元组被xid=102的事务删除,将t_xmax设置为删除事务的xid,t_ctid指向自己。

四、提交日志(clog)

Postgresql在提交日志(CLOG)中保存事务的状态。CLOG分配于共享内存中,并用于事务处理过程的全过程。

每次事务提交和回滚的时候,都需要更新该状态(调用CommitTransactionCommand(void)),PostgreSQL服务器访问该文件确定事务的状态,保存在pg_xact目录中,每个文件大小为256KB,每个事务2位(bit),故1个文件可以包含131072个事务。对于第一次修改的数据行来说,因为事务状态存储在clog中,所以修改后第一次判断行的可见性需要通过访问clog来确定,而访问clog是一个非常耗费性能的过程,

1、事务状态

  • IN_PROGRESS
  • COMMITTED
  • ABORTED
  • SUB_COMMITTED
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Possible transaction statuses --- note that all-zeroes is the initial
* state.
*
* A "subcommitted" transaction is a committed subtransaction whose parent
* hasn't committed or aborted yet.
*/
typedef int XidStatus;

#define TRANSACTION_STATUS_IN_PROGRESS 0x00
#define TRANSACTION_STATUS_COMMITTED 0x01
#define TRANSACTION_STATUS_ABORTED 0x02
#define TRANSACTION_STATUS_SUB_COMMITTED 0x03

2、事务特性

1
2
3
4
5
6
7
8
9
10
11
/* We need two bits per xact, so four xacts fit in a byte */
/* pg中通过2个bit位来标识4种事务状态(0、1、2、3),那么1个字节便可以存储4个事务,对于1个8k的page则可以存储8k * 4 = 32k个事务。 */
#define CLOG_BITS_PER_XACT 2
#define CLOG_XACTS_PER_BYTE 4
#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE)
#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1)

#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE) // 通过xid获取页
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE) // 获取xid在页中的索引
#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE) // 获取xid在页中的位置(第几个字节)
#define TransactionIdToBIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE) // 获取xid在页中的位置(在某个字节中位于第几个事务)
  • 事务id并不是在事务开始时就会被分配,而是当事务进行了数据修改之类的操作时才会分配xid,而当事务提交或回滚时,其事务状态便会被写入clog中。

  • 前一个事务所有修改的数据,它没有在提交或者回滚的当时改掉所有的修改标记,而是等到下次查询时进行修改(为了提升性能)。

五、事务快照

1、快照定义

事务快照在postgresql中的文本表示格式为xmin:xmax:xip_list。100:100:意味着txid < 100的事务处于非活跃状态,而txid ≥ 100的事务处于活跃状态。

100:100:含义如下

  • xmin=100,所以txid<100的事务均不活跃
  • xmax=100,所以txid>=100的事务均活跃或未启动
  • xip_list为空,表示[xmin,xmax)范围内无活跃事务

100:104:100,102含义如下

  • xmin=100,所以txid<100的事务均不活跃
  • xmax=104,所以txid>=104的事务均活跃或未启动
  • xip_list为100,102,表示[xmin,xmax)范围内100,102为活跃事务

2、快照与隔离级别

pg会根据不同隔离级别设置,获取不同时刻的快照:

  • 已提交读:在该事务的每条SQL执行之前都会重新获取一次快照
  • 可重复读和可串行化:该事务只在第一条SQL执行之前获取一次快照

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 快照相关的数据结构
typedef struct SnapshotData
{
SnapshotType snapshot_type; /* type of snapshot; 事务类型 */

/*
* The remaining fields are used only for MVCC snapshots, and are normally
* just zeroes in special snapshots. (But xmin and xmax are used
* specially by HeapTupleSatisfiesDirty, and xmin is used specially by
* HeapTupleSatisfiesNonVacuumable.)
*
* An MVCC snapshot can never see the effects of XIDs >= xmax. It can see
* the effects of all older XIDs except those listed in the snapshot. xmin
* is stored as an optimization to avoid needing to search the XID arrays
* for most tuples.
*/
TransactionId xmin; /* all XID < xmin are visible to me; 若事务ID小于xmin,则对当前事务可见 */
TransactionId xmax; /* all XID >= xmax are invisible to me; 若事务ID大于xmax,则对当前事务不可见 */

/*
* For normal MVCC snapshot this contains the all xact IDs that are in
* progress, unless the snapshot was taken during recovery in which case
* it's empty. For historic MVCC snapshots, the meaning is inverted, i.e.
* it contains *committed* transactions between xmin and xmax.
*
* note: all ids in xip[] satisfy xmin <= xip[i] < xmax
*/
TransactionId *xip; // 快照生成时对应的活跃事务列表
uint32 xcnt; /* # of xact ids in xip[]; xip数组大小 */

/*
* For non-historic MVCC snapshots, this contains subxact IDs that are in
* progress (and other transactions that are in progress if taken during
* recovery). For historic snapshot it contains *all* xids assigned to the
* replayed transaction, including the toplevel xid.
*
* note: all ids in subxip[] are >= xmin, but we don't bother filtering
* out any that are >= xmax
*/
TransactionId *subxip; // 活跃的子事务列表
int32 subxcnt; /* # of xact ids in subxip[]; sub xip数组大小 */
bool suboverflowed; /* has the subxip array overflowed? ;当子事务过多时,数组有可能overflow */

bool takenDuringRecovery; /* recovery-shaped snapshot? */
bool copied; /* false if it's a static snapshot; 如果是静态快照则为false */

CommandId curcid; /* in my xact, CID < curcid are visible */

/*
* An extra return value for HeapTupleSatisfiesDirty, not used in MVCC
* snapshots.
*
* HeapTupleSatisfiesDirty使用的一个变量,在判断可见性的同时,会借助这
* 个变量将元组上的speculativeToken返回给上层
*/
uint32 speculativeToken;

/*
* For SNAPSHOT_NON_VACUUMABLE (and hopefully more in the future) this is
* used to determine whether row could be vacuumed.
* 元组是否可vacuum(用于SNAPSHOT_NON_VACUUMABLE状态)
*/
struct GlobalVisState *vistest;

/*
* Book-keeping information, used by the snapshot manager
*/
uint32 active_count; /* refcount on ActiveSnapshot stack */
uint32 regd_count; /* refcount on RegisteredSnapshots */
pairingheap_node ph_node; /* link in the RegisteredSnapshots heap */

TimestampTz whenTaken; /* timestamp when snapshot was taken */
XLogRecPtr lsn; /* position in the WAL stream when taken */

/*
* The transaction completion count at the time GetSnapshotData() built
* this snapshot. Allows to avoid re-computing static snapshots when no
* transactions completed since the last GetSnapshotData().
*/
uint64 snapXactCompletionCount;
} SnapshotData;

六、可见性检查

事务并发可能出现的现象:

  • 脏读:某一事务读取了另一个事务未提交的脏数据。

  • 幻读:读取了前一事务提交的数据(针对的是一批数据整体,比如数据的个数 )。

  • 不可重复读:读取了前一事务提交的数据(查询的都是 同一个数据项 )。

事务隔离级别:

  • READ UNCOMMITTED(读未提交)------ postgresql不存在这个隔离级别,所以不会出现脏读的现象。
  • READ COMMITTED(读已提交)------ 可能会发生幻读和不可重复读。
  • REPEATABLE READ(可重复读)------ 不会发生幻读和不可重复读(标准sql事务隔离级别允许幻读)。
  • SERIALIZABLE(序列化)------ 不会发生幻读和不可重复读。
隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 Allowed, but not in PG
可串行化 不可能 不可能 不可能

元组的可见性:

确定一条元组是否对一个事务可见,可见性检查规则会用到元组的t_xmin和t_xmax,提交日志ClOG,以及已获取的事务快照。

t_xmin状态为 ABORTED的元组始终不可见

t_xmin状态为IN_PROGRESS的元组基本上是不可见的(对自身事务可见)

t_xmin状态为COMMITTED的元组是可见的