当前位置: 代码迷 >> 综合 >> postgres 源码解析2 元组可见性判断 t_infomask标识位
  详细解决方案

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

热度:36   发布时间:2023-11-24 18:35:03.0

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/0txid_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 2uint16		t_infomask2;	/* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3uint16		t_infomask;		/* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4uint8		t_hoff;			/* sizeof header incl. bitmap, padding *//* ^ - 23 bytes - ^ */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5bits8		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 |           12 |  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 
----12
(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 |           12 |  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指南 内幕探索》