传统的基于锁的并发控制存在读事务(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,判断其所作的修改对当前快照是否可见;
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删除了它。
事务创建快照的入口函数是GetTransactionSnapshot,函数在查询执行exec_simple_query中被多处调用。例如当进行某些语法分析需要建立快照时: