Postgresql源码(136)syscache/relcache 缓存及失效机制

相关

《Postgresql源码(45)SysCache内存结构与搜索流程分析》

0 总结速查

syscache:缓存系统表的行。通用数据结构,可以缓存一切数据(hash + dlist)。可以分别缓存单行和多行查询。

  1. syscache使用CatCache数组,定义了一些常用查询的结果集缓存,数据放到CatCache里面的dlist中存放。
  2. syscache查询接口
    • SearchSysCache系列接口时,key须按照cacheinfo的定义来查询
      • pg_class支持where relname = ? and relnamespace = ?的查询:SearchSysCache2(RELNAMENSP,k1,k2)
      • pg_class支持where oid = ?的查询:SearchSysCache1(RELOID,k1)
    • SearchSysCacheList系列接口时,可以使用少于定义的key去查询,例如
      • SearchSysCacheList1(AMPROCNUM, ObjectIdGetDatum(opfamilyoid));
      • SearchSysCacheExists4(AMPROCNUM, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(opcintype), ObjectIdGetDatum(opcintype), Int16GetDatum(procno))
  3. syscache的查询条件(1个或多个健)组合成key,key经过hash后落到某一个dlist上,在用key按顺序遍历dlist确定哪个是想要的,dlist自带lru机制,访问到的会调整到前面。

relcache:缓存RelationData。

  1. relcache就是一张hash表保存RelationIdCache结构。
  2. RelationIdCache结构在进程初始化时分三阶段初始化:创建RelationIdCache hash表、从pg_filenode.map文件导入oid→relfilenode、从pg_internal.init文件导入RelationData(包括RelationDataRelationData->rd_relRelationData->rd_attr)。

失效机制

  1. 进程本地,维护了数组存放失效消息,在事务提交时决定写共享内存或只失效自己。
  2. 进程本地,每一层子事务都会维护一个Group结构(InvalidationMsgsGroup),指向消息数组中的几条属于自己的失效消息。
  3. 进程本地,每一个Group结构中,都会维护一个当前query的group(CurrentCmdInvalidMsgs)、之前消息的group(PriorCmdInvalidMsgs),在事务提交、回滚时,可以分别处理:
    • 事务提交:当前的和之前的都需要发送共享内存,被其他进程消费。
    • 事务回滚:当前的不管了;之前的需要失效本地缓存,不发送到共享内存。
  4. 失效时机:子事务/事务提交/回滚AtEOXact_Inval、AtEOSubXact_Inval、CommandCounterIncrement。

1 系统表

系统表记录的元数据用来组织整库的数据结构。

例如:create table t1(a int, b int)

  • 在pg_class中记录表名、表文件、行统计信息等等信息:说明表名存在,如何找到表文件等。
  • 在pg_attribute中记录列名、列类型等信息:说明表有哪些列、列类型等。
  • 在pg_type中增加一条和表名同名的复合类型:声明一个新的复合类型(a int, b int),类型名同表名。

2 系统表缓存

系统表是需要被高频访问的,所以PG为系统表设计了两种进程级缓存:

  1. syscache:缓存系统表tuple → 缓存行数据。
  2. relcache:缓存系统表RelationData(表模式信息) → 缓存表结构。

两种缓存保存的都是高频访问数据,可以充分利用cpu的cache,进一步减少访问延迟。

缓存为什么要放到进程本地?因为每个进程执行的业务可能完全不同,缓存的数据也会有差异,并且进程天然隔离,做到本地简单、高效。如果放到共享内存中,并发读写需要有非常精细的控制,肯定要引入锁、atomic等同步机制,得不偿失。

3 syscache(catalog cache)

syscache 以一个数组的形式存放在内存中,每一个数组位置存放一个CatCache,每一个CatCache直观上可以看做一个固定SQL的结果集,具体的数据结构参考这里:

《Postgresql源码(45)SysCache内存结构与搜索流程分析》

cacheinfo数组中保存着上面提到的这些"SQL"例如:

sql 复制代码
static const struct cachedesc cacheinfo[] = {
	...
	...
	...
	[RELNAMENSP] = {
		RelationRelationId,
		ClassNameNspIndexId,
		KEY(Anum_pg_class_relname, Anum_pg_class_relnamespace),
		128
	},
	[RELOID] = {
		RelationRelationId,
		ClassOidIndexId,
		KEY(Anum_pg_class_oid),
		128
	},
	...
	...
	...

功能上可以看做:

  • RELNAMENSP
    • 等价为:select * from pg_class where relname = ? and relnamespace = ?
    • 走索引:ClassNameNspIndexId
  • RELOID
    • 等价为:select * from pg_class where oid = ?
    • 走索引:ClassOidIndexId

查询出来的结果(tuple)存放在CatCache的dlist中,CatCache还支持一批数据缓存,具体在上面文章中介绍,不再展开。

初始化流程:

c 复制代码
void
InitCatalogCache(void)
{
	int			cacheId;
	SysCacheRelationOidSize = SysCacheSupportingRelOidSize = 0;
	for (cacheId = 0; cacheId < SysCacheSize; cacheId++)
	{
		SysCache[cacheId] = InitCatCache(cacheId,
										 cacheinfo[cacheId].reloid,
										 cacheinfo[cacheId].indoid,
										 cacheinfo[cacheId].nkeys,
										 cacheinfo[cacheId].key,
										 cacheinfo[cacheId].nbuckets);
		SysCacheRelationOid[SysCacheRelationOidSize++] =
			cacheinfo[cacheId].reloid;
		SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =
			cacheinfo[cacheId].reloid;
		SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =
			cacheinfo[cacheId].indoid;
	}
	qsort(SysCacheRelationOid, SysCacheRelationOidSize,
		  sizeof(Oid), oid_compare);
	SysCacheRelationOidSize =
		qunique(SysCacheRelationOid, SysCacheRelationOidSize, sizeof(Oid),
				oid_compare);
	qsort(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,
		  sizeof(Oid), oid_compare);
	SysCacheSupportingRelOidSize =
		qunique(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,
				sizeof(Oid), oid_compare);
	CacheInitialized = true;
}

4 relcache

hash表缓存最常用的数据结构RelationData:

c 复制代码
typedef struct RelationData
{
	RelFileLocator rd_locator;	/* relation physical identifier */
	SMgrRelation rd_smgr;		/* cached file handle, or NULL */
	int			rd_refcnt;		/* reference count */
	ProcNumber	rd_backend;		/* owning backend's proc number, if temp rel */
	bool		rd_islocaltemp; /* rel is a temp rel of this session */
	bool		rd_isnailed;	/* rel is nailed in cache */
	bool		rd_isvalid;		/* relcache entry is valid */
	bool		rd_indexvalid;	/* is rd_indexlist valid? (also rd_pkindex and
								 * rd_replidindex) */
	bool		rd_statvalid;	/* is rd_statlist valid? */
...
...
	Form_pg_class rd_rel;		/* RELATION tuple */
	TupleDesc	rd_att;			/* tuple descriptor */
	Oid			rd_id;			/* relation's object id */
	LockInfoData rd_lockInfo;	/* lock mgr's info for locking relation */
...
...
} RelationData;

4.1 重要数据文件

pg_filenode.map

问题:在backend进程启动过程中,需要使用一张系统表,代码中是知道系统表具体oid的,oid对应磁盘上哪个文件,正常需要在pg_class中查询relfilenode,但是pg_class表还没加载。所以现在需要提供一个系统表oid → relfilenode的映射关系,可以找到一些最基础的系统表。

解法:pg_filenode.map提供了表oid到relfilenode的映射关系。

pg_relation_filenode函数可以查询表对应的relfilenode

pg_internal.init

问题:要构造一个RelationData需要访问pg_class、pg_arrtibute、pg_type等等系统表的数据,才能构造出来。但进程启动阶段,一些基础系统表的RelationData 如果每次扫描表再去构造效率会很差。

解法:pg_internal.init提供了预先计算好的系统表的 RelationData 结构。

4.2 初始化一阶段:RelationCacheInitialize

创建hash表RelationIdCache

c 复制代码
RelationCacheInitialize
  ctl.keysize = sizeof(Oid);
	ctl.entrysize = sizeof(RelIdCacheEnt);
	RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,
								  &ctl, HASH_ELEM | HASH_BLOBS);

	RelationMapInitialize();
    	shared_map.magic = 0;		/* mark it not loaded */
		local_map.magic = 0;
		shared_map.num_mappings = 0;
		local_map.num_mappings = 0;
		active_shared_updates.num_mappings = 0;
		active_local_updates.num_mappings = 0;
		pending_shared_updates.num_mappings = 0;
		pending_local_updates.num_mappings = 0;

4.3 初始化二阶段:RelationCacheInitializePhase2

  • 读共享库的pg_filenode.map
  • 读共享库的pg_internal.init
c 复制代码
void
RelationMapInitializePhase2(void)
{
	load_relmap_file(true, false);
	...
	...
	if (!load_relcache_init_file(true))
	{
	// 失败了要兜底!
		formrdesc("pg_database", DatabaseRelation_Rowtype_Id, true,
				  Natts_pg_database, Desc_pg_database);
		formrdesc("pg_authid", AuthIdRelation_Rowtype_Id, true,
				  Natts_pg_authid, Desc_pg_authid);
		formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true,
				  Natts_pg_auth_members, Desc_pg_auth_members);
		formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true,
				  Natts_pg_shseclabel, Desc_pg_shseclabel);
		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
				  Natts_pg_subscription, Desc_pg_subscription);

#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
	}
}

load_relmap_file加载pg_filenode.map

数据

typedef struct RelMapFile
{
	int32		magic;			/* always RELMAPPER_FILEMAGIC */
	int32		num_mappings;	/* number of valid RelMapping entries */
	RelMapping	mappings[MAX_MAPPINGS];
	pg_crc32c	crc;			/* CRC of all above */
} RelMapFile;

(gdb) p shared_map
$1 = {
  magic = 5842711, 
  num_mappings = 50, 
  mappings = {
    {mapoid = 1262, mapfilenumber = 1262}, 
    {mapoid = 2964, mapfilenumber = 2964}, 
    {mapoid = 1213, mapfilenumber = 1213}, 
    ...
    ...
    {mapoid = 1260, mapfilenumber = 1260},  
    {mapoid = 6115, mapfilenumber = 6115}, 
    {mapoid = 0, mapfilenumber = 0}}, 
  crc = 1938758537}

load_relcache_init_file加载pg_internal.init

4.4 初始化三阶段:RelationCacheInitializePhase3

  • 读非共享库的pg_filenode.map
  • 读非共享库的pg_internal.init
c 复制代码
void
RelationMapInitializePhase3(void)
{
	load_relmap_file(false, false);

	if (IsBootstrapProcessingMode() ||
		!load_relcache_init_file(false))
	{
		// 失败了兜底!
		needNewCacheFile = true;

		formrdesc("pg_class", RelationRelation_Rowtype_Id, false,
				  Natts_pg_class, Desc_pg_class);
		formrdesc("pg_attribute", AttributeRelation_Rowtype_Id, false,
				  Natts_pg_attribute, Desc_pg_attribute);
		formrdesc("pg_proc", ProcedureRelation_Rowtype_Id, false,
				  Natts_pg_proc, Desc_pg_proc);
		formrdesc("pg_type", TypeRelation_Rowtype_Id, false,
				  Natts_pg_type, Desc_pg_type);

#define NUM_CRITICAL_LOCAL_RELS 4	/* fix if you change list above */
	}
}

数据

typedef struct RelMapFile
{
	int32		magic;			/* always RELMAPPER_FILEMAGIC */
	int32		num_mappings;	/* number of valid RelMapping entries */
	RelMapping	mappings[MAX_MAPPINGS];
	pg_crc32c	crc;			/* CRC of all above */
} RelMapFile;

(gdb) p local_map
{
  magic = 5842711, 
  num_mappings = 17, 
  mappings = {
    {mapoid = 1259, mapfilenumber = 1259}, 
    {mapoid = 1249, mapfilenumber = 1249}, 
    {mapoid = 1255, mapfilenumber = 1255}, 
    ...
    ...
    {mapoid = 3455, mapfilenumber = 3455}, 
    {mapoid = 0, mapfilenumber = 0}}, 
  crc = 3752523506}

5 缓存同步

失效消息处理是通过共享内存和轮询的机制实现的。

5.1 进程本地失效消息记录

本地的操作在事务操作之前,不应该通知任何其他进程,所以机制上会先把需要失效的信息记录到进程本地InvalMessageArrays数组中,等事务提交时在做统一处理,这里先看下本地进程如何保存失效消息的。

例如relcache失效入口之一:

  • CacheInvalidateRelcache
    • PrepareInvalidationState
      • 构造TransInvalidationInfo结构,与子事务绑定
      • TransInvalidationInfo中记录了当前的InvalidationMsgsGroup和上一个InvalidationMsgsGroup。
      • InvalidationMsgsGroup里面记录了数组的起始位置和结束位置。
    • RegisterRelcacheInvalidation
      • AddRelcacheInvalidationMessage
        • 检查InvalMessageArrays数组中没有这一条
        • AddInvalidationMessage
          • 插入InvalMessageArrays数组中,并更新InvalidationMsgsGroup中记录的位置。

注意:InvalidationMsgsGroup的作用就是记录InvalMessageArrays数组中的起始、终止位置。

进程本地保存失效消息数据结构:

(为什么交nestmsg:最后一条失效消息的下一个)

5.2 进程提交、回滚时对失效消息的处理

见注释:

c 复制代码
void
AtEOXact_Inval(bool isCommit)
{
	...
	if (isCommit)
	{
		if (transInvalInfo->RelcacheInitFileInval)
			RelationCacheInitFilePreInvalidate();

		// 把当前的失效消息追加到prior中
		AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
								   &transInvalInfo->CurrentCmdInvalidMsgs);

		// 顶层事务提交时:共享内存发送失效消息
		ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,
										 SendSharedInvalidMessages);

		if (transInvalInfo->RelcacheInitFileInval)
			RelationCacheInitFilePostInvalidate();
	}
	else
	{
		// 顶层事务回滚时:只需要把自己的失效掉,不需要发送出去
		ProcessInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
									LocalExecuteInvalidationMessage);
	}
	...
}

注意,当进程回滚时,为什么要把自己本地的失效掉?因为事务内的一些写、读操作,可能已经cache了一些会被回滚调的数据,cache没有mvcc机制,这里必须把回滚调(不可见)的数据失效掉,否则后面在读到这些数据就是脏读了。

5.3 CommandCounterIncrement触发本地失效

一个事务中执行了多个命令,但直到事务最终提交之前,这些更改都是暂时的。意味着在事务提交之前,肯定不会将失效消息发送到共享队列。但是,即使事务最终回滚,每个命令执行后的本地缓存仍需要反映这些暂时的更改,保证事物内的后续查询能拿到正确的结果。

c 复制代码
CommandCounterIncrement
	AtCCI_LocalCache
		CommandEndInvalidationMessages
			// 先把当前query造成的失效消息做 到 本地
			ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs, 
										LocalExecuteInvalidationMessage)
			// 把当前的失效消息 追加到 历史消息中 PriorCmdInvalidMsgs
			AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
										&transInvalInfo->CurrentCmdInvalidMsgs);

5.4 为什么TransInvalidationInfo有两个Group?

InvalidationMsgsGroup记录消息队列中的起止位置,这几个消息是当前Group管理的。

TransInvalidationInfo中记录了两个Group?当前CurrentCmdInvalidMsgs、历史PriorCmdInvalidMsgs。

  • 当前的失效消息需要再每个命令执行后,应用到本地,保证事物内的后续SQL能查到正确的缓存数据。

  • 当前的失效消息在事务回滚时,不需要处理,只需要把历史PriorCmdInvalidMsgs做到本地即可。

    typedef struct TransInvalidationInfo
    {
    /* Back link to parent transaction's info */
    struct TransInvalidationInfo *parent;

      /* Subtransaction nesting depth */
      int			my_level;
    
      /* Events emitted by current command */
      InvalidationMsgsGroup CurrentCmdInvalidMsgs;
    
      /* Events emitted by previous commands of this (sub)transaction */
      InvalidationMsgsGroup PriorCmdInvalidMsgs;
    
      /* init file must be invalidated? */
      bool		RelcacheInitFileInval;
    

    } TransInvalidationInfo;

相关推荐
刘九灵1 小时前
Redis突然变慢,有哪些原因?
redis·缓存
猿月亮1 小时前
MySQL自启动失败(MySQL不能开机自启)解决方案_MySQL开机自启疑难杂症解决,适用Win11/Win10
数据库·mysql
酷炫码神2 小时前
MySQL查询
数据库·mysql
大明湖的狗凯.2 小时前
MySQL 中的排序:索引排序与文件排序
数据库·mysql·oracle
XMYX-02 小时前
深入解析 Django 中数据删除的最佳实践:以动态管理镜像版本为例
数据库·django·sqlite
Lostgreen2 小时前
SQL on Hadoop
数据库·hadoop·笔记·分布式·sql·学习
Karoku0662 小时前
【docker集群应用】Docker常用命令
运维·数据库·docker·容器
小小宇宙中微子2 小时前
MySQL INSERT CRTATE DELETE DORP UPDATE WHERE 的用法
数据库·mysql
swiftlzk3 小时前
redmi 12c 刷机
android·数据库
人才程序员3 小时前
详解Qt 中使用虚拟键盘(软键盘qtvirtualkeyboard)
开发语言·数据库·c++·qt·计算机外设·界面·c语音