您的位置:首页 > 其它

Postgres并发控制之快照与MVCC

2015-07-27 20:28 204 查看
传统的基于锁的并发控制存在读事务(Reader)和写事务(Writer)相互阻塞的问题,为此Postgres引入了多版本并发控制机制,简称MVCC。一般说来,支持MVCC机制的数据库管理系统有着如下特点:

数据库管理系统能够得到元组的历史版本
数据库系统中存在判定元组版本对于处在特定上下文的事务是否有效的机制。简单地说,数据库通常会认为只有在事务执行开始之前就已提交的事务所生成的版本是有效的。为此,数据库系统需要记录元组的每个版本由哪个事务创建,以及这个事务是否在当前事务开始执行之前就已提交。

Postgres采用“快照”的方式来实现MVCC。数据库中每一个事务中的查询仅能看到:该事务启动之前已经提交的事务所作出的数据更改;当前事务中该查询之前的查询所作出的更改。Postgres中每一个版本的元组有两个ID,其中一个是CreationID即插入该元组的TransactionID,一个是ExpiredID,即删除或更新该元组的TransactionID。元组版本对一个Transaction可见,其ID要满足以下条件:

1.CreationID<当前TransactionID

2.ExpiredID>当前TransactionID或ExpiredID不存在

事务启动创建快照的过程简单说就是在事务启动的时刻,遍历当前所有活动的(还未提交)事务,记录在一个活动Transaction的ID数组中;选择所有活跃事务中最小的TransactionID,记录在xmin中,选择所有已提交事务中最大的TransactionID,加1后记录在xmax中。那么:

所有事务ID小于xmin的事务可以被认为已经完成,即事务已提交,其所做的修改对当前快照可见;
所有事务ID大于或等于xmax的事务可以被认为是正在执行,其所做的修改对当前快照不可见;
对于事务ID处在 [xmin, xmax)区间的事务, 需要结合活跃事务列表与事务提交日志CLOG,判断其所作的修改对当前快照是否可见;

Postgres中的数据元组在磁盘上存放时,会连同文件头信息一起存储(HeapTupleHeaderData),其中, t_cid用来记录当前元组或新元组的物理位置,有块号和快内偏移组成,可以用来查找元组的下一个版本在磁盘上的位置。联合结构体t_choice中的HeapTupleFields类型数据t_heap用于记录对元组执行插入删除操作的事物ID和命令ID,用于并发控制时检查事物对元组的可见性,其数据结构如下:

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

ItemPointerData t_ctid;		/* current TID of this or newer tuple */

/* 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[1];		/* bitmap of NULLs -- VARIABLE LENGTH */

/* MORE DATA FOLLOWS AT END OF STRUCT */
};
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;
typedef struct ItemPointerData
{
BlockIdData ip_blkid;
OffsetNumber ip_posid;
}
typedef struct HeapTupleData
{
uint32		t_len;			/* length of *t_data */
ItemPointerData t_self;		/* SelfItemPointer */
Oid			t_tableOid;		/* table the tuple came from */
HeapTupleHeader t_data;		/* -> tuple header and data */
} HeapTupleData;




文件块的结构

元组头信息中的t_xmin和t_xmax分别表示创建此Tuple的XID 与删除此Tuple的XID,用于MVCC中的可见性判断。一般情况下,在同一个事务中创建并删除同一个元组的概率比较低,t_cid字段只需记录元组版本生效(或失效)命令ID即可。如果一个事务中确实创建并删除了同一个元组,则我们记录到两个命令组合的一个映射值即可。

BEGIN T1;
INSERT INTO TEST_TABLE VALUES(A);     // CommandId =0
INSERT INTO TEST_TABLE VALUES(B);      //  CommandId =1
DECLARE c1 CURSOR FOR  SELECT * FROM TEST_TALBE;
UPDATE TEST_TABLE SET ID=C WHERE ID=B;  //  CommandId =2
END T1;
<div style="text-align: center;"></div>

B被本事务创建同时也被本事务删除,所以B的xmin和xmax都是T1,为了标识B是被本事务的那条SQL创建和删除的,需要(cmin,cmax),其中cmin标识创建B的SQL语句,cmax标识删除B的SQL语句。但是在在内部数据结构中,cmin和cmax公用一个字段cmin_cmax,所以需要一个cmin_cmax到(cmin,cmax)的映射,同时为了区分cmin_cmax是一个映射还是cmin/cmax,必须要有一个标识,事实上它是由HeapTupleHeaderData的t_infomask标识的,即如果它是一个映射,则将t_infomask的相应位置为HEAP_COMBOCID。仍以上图2为例,值为1的CommandId被标识为映射(comboCid),它的映射值为(1,2),即表示第1条command创建了它,第2条command删除了它。

通过以上分析,我们知道了元组在磁盘上是如何进行存储的,一个新元组被保存在磁盘中的时候,其t_ctid就被初始化化为它自己的实际存储位置。如果这个元组被更新了,该元组的t_ctid域就指向更新后的新元组。如果要找到某个元组的最新版本,只需遍历由t_ctid构成的链表即可,这就是数据库系统如何通过“旧”的元组找到更“新”的有效元组。

事务创建快照的入口函数是GetTransactionSnapshot,函数在查询执行exec_simple_query中被多处调用。例如当进行某些语法分析需要建立快照时:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: