0
点赞
收藏
分享

微信扫一扫

postgresql源码学习(十六)—— MVCC①-元组上的版本信息


        关于元组的基础知识,曾经在 pg事务篇(一)—— 事务与多版本并发控制MVCC

一、 页与元组的组织结构

1. 页的组织结构

       pg数据库采用页面存储的方式,每个表都会有多个页面,每页大小是8K。

页从前往后保存头信息(Header Info,绿色部分)、元组偏移量(红色部分);从后往前存放元组信息(灰色的tuple);中间为页面空闲空间(Free Space,白色部分)。当空闲空间不足以保存一个元组时,该页面变满。

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_MVCC

2. 元组的组织结构

       把下面那一长条放大,这就是元组的结构,在元组头部分保存了大量的版本信息,用于元组可见性判断。为了提高存储空间利用率,这些头部信息的排列尽量考虑到了字节对齐的问题,每个变量也尽量考虑了复用。例如t_cid变量在不同生命周期中分别代表cmin,cmax,xvax。

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_postgresql_02

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_源码_03

HeapTupleHeaderData的实现在htup_details.h中,下面我们就来看一看。

二、 源码实现

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_源码_04

1. HeapTupleFields

/* We store five "virtual" fields Xmin, Cmin, Xmax, Cmax, and Xvac in three
 * physical fields.  Xmin and Xmax are always really stored, but Cmin, Cmax
 * and Xvac share a field.
*/

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;

2. DatumTupleFields

记录元组长度、类型、oid等信息

typedef struct DatumTupleFields
{
	int32		datum_len_;		/* varlena header (do not touch directly!),变长header */
	int32		datum_typmod;	/* -1, or identifier of a record type,-1或者表示记录类型 */
	Oid			datum_typeid;	/* composite type OID, or RECORDOID,复合类型oid或者记录oid */
} DatumTupleFields;

3. HeapTupleHeaderData

struct HeapTupleHeaderData
{
	union
	{
		HeapTupleFields t_heap; // 主要用于元组可见性检查
		DatumTupleFields t_datum;  
	}			t_choice;
ItemPointerData t_ctid;		

	/* 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 */

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

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

字段说明:参考 http://blog.itpub.net/31493717/viewspace-2220463/

① t_choice:具有两个成员的联合类型:

  • t_heap:用于记录对元组执行插入/删除操作的事务ID和命令ID,这些信息主要用于并发控制时检查元组对事务的可见性。
  • t_datum:当一个新元组在内存中形成的时候,我们并不关心其事务可见性,因此在t_choice中只需用DatumTupleFields结构来记录元组的长度等信息。但在把该元组插入到表文件时,需要在元组头信息中记录插入该元组的事务和命令ID,故此时会把t_choice所占用的内存转换为HeapTupleFields结构并填充相应数据后再进行元组的插入。

② t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。当更新该元组时,t_ctid会指向新版本元组。若元组被更新多次,则该元组会存在多个版本,各版本通过t_cid串联,形成一个版本链。

③ t_infomask2:其低11位表示当前元组的属性个数,其他位则用于包括用于HOT技术及元组可见性的标志位。

④ t_infomask:用于标识元组当前的状态,比如元组是否具有OID、是否有空属性等,t_infomask的每一位对应不同的状态,共16种状态。pg事务篇(三)—— 事务状态与Hint Bits(t_infomask)_Hehuyi_In

⑤ t_hoff:该元组头的大小。

⑥ t_bits[]数组:标识该元组哪些字段为空。

4. ItemIdData

typedef struct ItemIdData
{
	unsigned	lp_off:15,		/* offset to tuple (from start of page),元组在页面的偏移量 */
				lp_flags:2,		/* state of line pointer, see below,元组的line pointer状态,参考下面 */
				lp_len:15;		/* byte length of tuple,元组长度 */
} ItemIdData;

typedef ItemIdData *ItemId;

/*
 * lp_flags 有以下状态. UNUSED状态的line pointer可以立刻重用,其他状态的则不可以
 */
#define LP_UNUSED		0		/* unused (should always have lp_len=0),未使用 */
#define LP_NORMAL		1		/* used (should always have lp_len>0),正在正常使用 */
#define LP_REDIRECT		2		/* HOT redirect (should have lp_len=0),这个line pointer重定向到其他line pointer */
#define LP_DEAD			3		/* dead, may or may not have storage,被删除的元组,处于可vacuum状态 */

三、 查看page header内容

pg提供了pageinspect插件,可查看指定表对应的page header内容。

CREATE EXTENSION pageinspect;

创建一个测试表并插入一条数据

CREATE TABLE tbl (a int,b int);
begin;
INSERT INTO tbl VALUES(1,1);

select txid_current(); -- 看到事务id为737

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_postgresql_05

javascript:void(0)

文章里提供了一个函数可以转换t_infomask和t_infomask2标记值,这里先创建该函数。

SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data 
FROM heap_page_items(get_raw_page('tbl', 0));

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_postgresql_06

做一个提交操作,再查看元组信息

Commit;

发现没有变化

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_MVCC_07

查询一下tbl表

Select * from tbl;

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_postgresql_08

        发现infomask变成了XMAX_INVALID|XMIN_COMMITTED。

        XMIN_COMMITTED提供了元组可见性的快速判断方法。每次对元组进行查询时,如果发现元组所在事务已提交,就设置该标记位,避免每次都要去clog查询事务状态。

启动一个新事务,做更新操作

Begin;
Update tbl set a=2 where a=1;
select txid_current(); -- 看到事务id为738,增加了1

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_事务_09

SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data 
FROM heap_page_items(get_raw_page('tbl', 0));

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_MVCC_10

       此时发现多了一条记录(页面内多了一个元组),从t_data列可以看出,它们分别是新旧两个版本。

在旧元组中:

  • infomask标记只留下了XMIN_COMMITTED去掉了XMAX_INVALID,因为xmax中已经记录了删除这个元组的事务id。在pg中,对元组的update相当于删除旧元组并插入新元组。
  • infomask2标记变为了HOT_UPDATED,表示元组已被更新,且更新产生的新元组与旧元组在同一页面中。这个标记通常与新元组的HEAP_ONLY_TUPLE标记成对出现。

在新元组中:

  • infomask标记为UPDATED|XMAX_INVALID,表示这是一个由更新操作产生的新版本元组。
  • infomask2标记为HEAP_ONLY_TUPLE,表示它目前还是一个HOT元组(可以防止产生重复的索引项,更容易清理)

再更新一次,然后提交

Update tbl set a=3 where a=2;
Commit;

SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data 
FROM heap_page_items(get_raw_page('tbl', 0));

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_MVCC_11

  • 元组1已被删除,其t_ctid指向元组2
  • 元组2已被删除,其t_ctid指向元组3
  • 元组3是当前事务元组,其t_ctid指向自己

执行一个vacuum操作

Vacuum;

SELECT lp,lp_off,lp_flags,t_xmin,t_xmax,t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2 
FROM heap_page_items(get_raw_page('tbl', 0));

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_postgresql_12

  • 元组1中lp_flags=2(LP_REDIRECT),lp_off=3,说明它被重定向到了元组3
  • 元组2中lp_flags=0(LP_UNUSED),说明这个槽位目前未使用
  • 元组3中lp_flags=1(LP_NORMAL),说明在正常使用中

插入一个新元组

INSERT INTO tbl VALUES(3,3);

postgresql源码学习(十六)—— MVCC①-元组上的版本信息_元组_13

可以看到,它占用了原来的2号槽位

参考

《PostgreSQL技术内幕:事务处理深度探索》第3章


举报

相关推荐

0 条评论