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指南 内幕探索》