文章目录
-
- 0.简介
- 1.MVCC介绍
- 2.MVCC常见的实现方式
- 3.PG的MVCC实现
-
- [3.1 可见性判断](#3.1 可见性判断)
- [3.2 提交/取消](#3.2 提交/取消)
0.简介
本文主要介绍在事务模块中MVCC(多版本并发控制)常见的实现方式,优缺点以及PG事务模块中MVCC(多版本并发控制)的实现。
1.MVCC介绍
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种用于数据库管理系统中的并发控制的方法。在传统的并发控制中,常用的简单方式是通过加锁来保证某一时刻数据只被一个事务修改,但这种方式可能会带来并发度的下降,尤其在高并发场景下,很可能导致性能瓶颈。MVCC主要通过维护数据的多个版本来解决传统锁机制的一些局限性,每个事务可以看到一个特定版本,从而使得读写操作可以互不干扰地执行,其核心在于,对于每个修改,不直接在原始数据上修改,而是创建一个新的数据版本来做修改,其他事务依然可以访问旧的数据,以此来提高并发度。当然MVCC也有其局限性,比如在高并发场景下可能因为多个版本导致占用较高内存。
2.MVCC常见的实现方式
MVCC常见的实现方式有两种:
1)修改旧数据前备份,在写新的数据时,把旧的数据备份到单独的一块空间,其他事务读取数据时,可以在备份空间中获取,比如MySQL innodb引擎的回滚段。
2)新数据不直接修改,而是采用插入的方式。
以上两种方式,功能上都能实现MVCC,都需要占用一定的空间,两者相比较,二的事务回滚更为方便,不会出现备份空间用尽的问题;一的话清理上会更为简单,不会导致数据扫描使得读数据增加。PG采用的是二,即采用插入方式实现的MVCC。
3.PG的MVCC实现
3.1 可见性判断
MVCC的实现,首先要有版本的概念,下面来看PG中的定义,然后以一个实际的例子来分析可见性的判断,定义如下:
cpp
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
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) */
....
}
可以看到,在上面代码结构中,每个元组头部存储事务的t_xmin(数据插入的事务id),t_xmax(数据删除或更新的事务id)如果为0则表示还未被删除和更新,这两个值一旦被设计就不会再次变化。
下面通过一个例子来看一个更新操作的修改以及可见性的判断。
上述描述了向当前表插入了一条数据的过程,初始a=2,b=2的记录是由事务id为10的事务插入,xmax为0即还没被删除或更新;然后执行update语句,将a的值设置为6,xmax的值更新为11(表示由id为11的事务删除),同时新增一条记录(不在原记录修改),此时虽然有两条数据,但其实应该只有一条,所以需要根据事务的快照和提交的记录来进行判断,也就是可见性的判断。
在PG中,是用snapshot来获取那些事务正在执行,通过snapshot来区分事务是正在执行还是已经完成了,如果事务尚未完成,那么事务的更新和写入对其他事务来说是不可见的。snapshot数据结构如下:
cpp
typedef struct SnapshotData
{
SnapshotSatisfiesFunc satisfies; /* tuple test function */
TransactionId xmin; /* all XID < xmin are visible to me */
TransactionId xmax; /* all XID >= xmax are invisible to me */
TransactionId *xip;
uint32 xcnt; /* # of xact ids in xip[] */
TransactionId *subxip;
int32 subxcnt; /* # of xact ids in subxip[] */
bool suboverflowed; /* has the subxip array overflowed? */
bool takenDuringRecovery; /* recovery-shaped snapshot? */
bool copied; /* false if it's a static snapshot */
CommandId curcid; /* in my xact, CID < curcid are visible */
uint32 speculativeToken;
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 */
} SnapshotData;
其中所有XID<xmin(已完成的)的事务都可见,所有XID>xmax的事务都不可见,而介于xmin和xmax之间的事务可能已完成也可能进行中,所以需要一个数组来存储(xip),如果在xmin和xmax之间的事务id在这个数组被发现,说明事务正在进行且尚未完成,不可见。
获取数据时,会先根据snapshot来判断事务是否已完成,如果未完成,则不可见,对于已完成的需要判断时提交还是取消,查询clog来进行判断。这里还有个性能优化,就是使用tuple中标志位来进行判断,减少clog查询。
3.2 提交/取消
在PG中,一个事务最终状态可能有两种:Commit/Abort。
1)Commit:提交时会写WAL和CLOG,提交后对于其他事务可见。
2)Abort: abort时会写WAL和CLOG,abort后对于其他事务不可见。