postgres 源码解析2 元组可见性判断 t_infomask标识位

1 背景

  对postgres熟悉的DBA学习者应该知道在pg中通过多版本控制技术( MVCC)来解决读写冲突,即读不阻塞写,写不阻塞读,相比于基于锁的并发控制技术进一步提升了事务的并发度。其实现方法是写新数据时(updata),旧数据不删除,直接插入新数据。
为了实现MVCC机制,必须要:

  • 定义多版本的数据——使用元组头部信息的字段来标示元组的版本号
  • 定义数据的有效性、可见性、可更新性——通过当前的事务快照和对应元组的版本号判断
  • 实现不同的数据库隔离级别——通过在不同时机获取快照实现

2 基本概念

2.1 事务标识

当事务开始(执行begin第一条命令时),事务管理器会为该事务分配一个txid(transaction id)作为唯一标识符。txid是一个32位无符号整数,取值空间大小约42亿(2^32-1)。

postgres=# select txid_current();
DEBUG:  StartTransaction(1) name: unnamed; blockState: DEFAULT; state: INPROGRESS, xid/subid/cid: 0/1/0
LOG:  statement: select txid_current();
DEBUG:  CommitTransaction(1) name: unnamed; blockState: STARTED; state: INPROGRESS, xid/subid/cid: 822/1/0
 txid_current 
--------------
          822
(1 row)

三个特殊的txid

0:InvalidTransactionId,表示无效的事务ID
1:BootstrapTransactionId,表示系统表初始化时的事务ID,比任何普通的事务ID都旧。
2:FrozenTransactionId,冻结的事务ID,比任何普通的事务ID都旧。
大于2的事务ID都是普通的事务ID。

2.2 元组结构

在这里插入图片描述
在这里插入图片描述
postgres官网: https://www.postgresql.org/docs/9.6/storage-page-layout.html
t_xmin:保存插入该元组的事务txid(该元组由哪个事务插入)
t_xmax:保存更新或删除该元组的事务txid。若该元组尚未被删除或更新,则t_xmax=0,即invalid
t_cid:保存命令标识(command id,cid),指在该事务中,执行当前命令之前还执行过几条sql命令(从0开始计算)
t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。
t_infomask:标识位

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) */
	/* 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_infomask标识位用于加快元组的可见性判断,其实现原理为:当查询一条数据时,需要判断所涉及元组的可见性,也就需要知道该元组的提交状态( 查看CLOG) ,如果同一条数据经常被查询或被访问,就需要多次去查看CLOG文件,会涉及较高代价的I/O操作。而将可见性标识位t_infomask直接写入把事务状态直接记录在元组头中(HeapTupleHeaderData),避免频繁访问CLOG影响从而加快可见性判断。

/*
 * information stored in t_infomask:
 */
#define HEAP_HASNULL			0x0001	/* has null attribute(s) */
#define HEAP_HASVARWIDTH		0x0002	/* has variable-width attribute(s) */
#define HEAP_HASEXTERNAL		0x0004	/* has external stored attribute(s) */
#define HEAP_HASOID_OLD			0x0008	/* has an object-id field */
#define HEAP_XMAX_KEYSHR_LOCK	0x0010	/* xmax is a key-shared locker */
#define HEAP_COMBOCID			0x0020	/* t_cid is a combo CID */
#define HEAP_XMAX_EXCL_LOCK		0x0040	/* xmax is exclusive locker */
#define HEAP_XMAX_LOCK_ONLY		0x0080	/* xmax, if valid, is only a locker */

 /* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK	(HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)

#define HEAP_LOCK_MASK	(HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \
						 HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMIN_COMMITTED		0x0100	/* t_xmin committed */
#define HEAP_XMIN_INVALID		0x0200	/* t_xmin invalid/aborted */
#define HEAP_XMIN_FROZEN		(HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
#define HEAP_XMAX_COMMITTED		0x0400	/* t_xmax committed */
#define HEAP_XMAX_INVALID		0x0800	/* t_xmax invalid/aborted */
#define HEAP_XMAX_IS_MULTI		0x1000	/* t_xmax is a MultiXactId */
#define HEAP_UPDATED			0x2000	/* this is UPDATEd version of row */
#define HEAP_MOVED_OFF			0x4000	/* moved to another place by pre-9.0
										 * VACUUM FULL; kept for binary
										 * upgrade support */
#define HEAP_MOVED_IN			0x8000	/* moved from another place by pre-9.0
										 * VACUUM FULL; kept for binary
										 * upgrade support */
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)

#define HEAP_XACT_MASK			0xFFF0	/* visibility-related bits */

2.3 t_infomask的计算

postgres=# insert into test values(1);
postgres=# insert into test values(2);
postgres=# select *, t_xmin,t_xmax, t_infomask,t_infomask2 from heap_page_items(get_raw_page('test',0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |   t_data   | t_xmin | t_xmax | t_infomask | t_infomask2 
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------+--------+--------+------------+-------------
  1 |  16352 |        1 |     28 |    824 |      0 |        0 | (0,1)  |           1 |       2048 |     24 |        |       | \x01000000 |    824 |      0 |       2048 |           1
  2 |  16320 |        1 |     28 |    825 |      0 |        0 | (0,2)  |           1 |       2048 |     24 |        |       | \x02000000 |    825 |      0 |       2048 |           1
(2 rows)

可以看出插入 1 的事务id 为824, 插入 2 的事务id 为825。两者的标志位 t_infomask = 2048, 换算成16进制为0x0800;根据上述t_infomask的宏定义可知 t_xmax invalid/aborted,这是因为插入数据t_xmax不发生变化,为0;并不知道t_xmin是否提交,也就是说下次查询的时候并不能直接判断该元组的可见性,需要从CLOG读取事务的提交状态。

问题来了,那怎么才能避免后续重读读取CLOG文件,加快元组可见性判断和数据的读取呢?见下:

postgres=# select * from test;
 id 
----
  1
  2
(2 rows)
postgres=# select *, t_xmin,t_xmax, t_infomask,t_infomask2 from heap_page_items(get_raw_page('test',0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |   t_data   | t_xmin | t_xmax | t_infomask | t_infomask2 
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------+--------+--------+------------+-------------
  1 |  16352 |        1 |     28 |    824 |      0 |        0 | (0,1)  |           1 |       2304 |     24 |        |       | \x01000000 |    824 |      0 |       2304 |           1
  2 |  16320 |        1 |     28 |    825 |      0 |        0 | (0,2)  |           1 |       2304 |     24 |        |       | \x02000000 |    825 |      0 |       2304 |           1
(2 rows)

从上述例子看出,执行select 语句后发现t_infomask从2048转变成2304,换算成16进制为0x0900;即
0x0800 | 0x0100 = 0x0900,表明设置了HEAP_XMIN_COMMITTED(0x0100)这个标志位,该元组插入成功并已提交,对后续事务均可见。

等到第一次访问(可能是VACUUM,DML或SELECT)该元组并进行可见性判断时:

  • 如果Hint Bits已设置,直接读取Hint Bits的值。
  • 如果Hint Bits未设置,则调用函数从CLOG中读取事务状态。如果事务状态为COMMITTED或ABORTED,则将Hint Bits设置到元组的t_informask字段。如果事务状态为INPROCESS,由于其状态还未到达终态,无需设置Hint Bits。

Hint Bits可以理解为是事务状态在元组头上的一份缓存,减少访问链路的长度,让事务状态触手可及

3 Hint Bits与日志

在开启CHECKSUM或者wal_log_hints=true的情况下,如果CHECKPOINT后第一次使页面dirty的操作是更新Hint Bits,则会产生一条WAL日志,将当前数据块写入WAL日志中(Full Page Image),避免产生部分写,导致数据CHECKSUM异常。

因此,在开启CHECKSUM或者wal_log_hints=true时,即便执行SELECT,也可能更改页面的Hint Bits,从而导致产生WAL日志,这会在一定程度上增加WAL日志占用的存储空间。如果在使用pg中发现执行SELECT会触发磁盘的写入操作,可以检查一下是否开启了CHECKSUM或者wal_log_hints。

注意,以上写FullPageImage日志的行为与是否开启full_page_writes没有关系。相关代码实现可以参考MarkBufferDirtyHint这个函数。

参考:https://blog.csdn.net/Hehuyi_In/article/details/102920988
《PostgreSQL指南 内幕探索》

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值