postgreSQL源码分析——存储管理——内存管理(4)

2021SC@SDUSC

目录

概述

上次分析了postgreSQL中的SysCache。这次来分析另一个缓存,RelCache。而RelCache的管理就比SysCache简单很多,这是因为RelCache中存储的RelationData的结构大部分时间时不会改变的,因此postgreSQL只用一个Hash表来维持这样一个结构。

源码分析

表模式信息缓存——RelCache

RelCache用于存放RelationData数据结构,而每个RelationData结构表示一个表的模式信息,这些信息都由系统表元组中的信息构造而来。

RelationData结构体——RelCache中存放的内容

由于该结构体包含比较多的变量,我只展示比较重要的一些。

typedef struct RelationData
{
	RelFileNode rd_node;		//关系(表)的物理标识符,包含了表空间、数据库和表的OID
	
	struct SMgrRelationData *rd_smgr;	//用于缓存关系(表)的文件句柄,如果没有则为NULL

	Form_pg_class rd_rel;		//关系(表)在系统表pg_class中对应的元组里的信息
	TupleDesc	rd_att;			//关系(表)的元组描述符,即表的列名和相关信息。
	Oid			rd_id;			///关系(表)的OID
	RuleLock   *rd_rules;		//关系(表)的重写规则
	MemoryContext rd_rulescxt;	//重写规则的私有内存上下文
	TriggerDesc *trigdesc;		//关系(表)的触发器的相关信息,如果没有则为NULL
	struct RowSecurityDesc *rd_rsdesc;	//行的安全策略,如果没有则为NULL

	List	   *rd_fkeylist;	//外键缓存信息的链表
	bool		rd_fkeyvalid;	//如果链表已经生成则为真

	struct PartitionKeyData *rd_partkey;	//用于分片的关键字
	struct PartitionDescData *rd_partdesc;	//分片的描述符

	List	   *rd_indexlist;	//关系(表)上所有索引的OID链表
	Oid			rd_pkindex;		//主键的OID

	Bitmapset  *rd_indexattr;	//记录上面的re_indexlist中各个索引用到的列名
	Bitmapset  *rd_keyattr;		//记录外键的映射
	Bitmapset  *rd_pkattr;		//记录主键中包含的列名


	Form_pg_index rd_index;		//如果该表是一个索引表,这里用于记录它在pg_index中的信息。
	struct HeapTupleData *rd_indextuple;	//同上,记录表在pg_index中的的所有元组。
	Oid			rd_toastoid;	//toast表的oid,不存在则为invaildOid

	struct PgStat_TableStatus *pgstat_info; //统计信息集合
} RelationData;

RelCache的初始化

源码位于src\backend\utils\cache\relcache.c
相较于上次分析的SysCache,RelCache的初始化要简单许多。这是因为RelCache中存储的上面分析的RelationData结构在多数情况下是不会改变的,因此使用一个Hash表来维持一个这样的结构就可以。
RelCache的初始化是和SysCache类似的,也是在InitPostgres函数中进行,分为三个阶段。

RelationCacheInitialize函数——初始化第一阶段

该函数的作用就是创建一个空的Hash表,或者说把relcache初始化为空。

void
RelationCacheInitialize(void)
{
	HASHCTL		ctl;//Hash表的一些信息

	//确保缓存上下文已经创建
	if (!CacheMemoryContext)//如果缓存上下文还没有创建
		CreateCacheMemoryContext();//创建缓存上下文

	 //创建Hash表作为relcache的索引
	MemSet(&ctl, 0, sizeof(ctl));//初始化HASH表的内存,置为0
	ctl.keysize = sizeof(Oid);//将HASH表的键大小置为表的OID的大小,从而可以将该表的OID作为HASH表项的键
	ctl.entrysize = sizeof(RelIdCacheEnt);//将HASH表的值大小置为表的大小
	RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,
								  &ctl, HASH_ELEM | HASH_BLOBS);
	//通过hash_create方法创建Hash表
	
	 //初始化关系映射表
	RelationMapInitialize();
}

上面函数用到的一些数据结构

typedef struct relidcacheent
{
	Oid			reloid;//关系(表)的OID
	Relation	reldesc;//关系(表)描述符
} RelIdCacheEnt;
static HTAB *RelationIdCache;//Hash表

该结构体为RelCache在Hash表中的索引,通过表的OID作为索引(之前的postgreSQL是通过name和OID共同作为索引的),该结构体的指针构成一个Hash表。

RelationCacheInitializePhase2——初始化第二阶段

该函数的作用就是读入一些共享的系统表信息,至少包含pg_database表和认证相关的系统表(pg_authid、pg_auth_members)的信息。

void
RelationCacheInitializePhase2(void)
{
	MemoryContext oldcxt;//用于存放当前的内存上下文

	//初始化RelationMap
	RelationMapInitializePhase2();
	
	 //记录下当前的上下文,同时转换到缓存上下文,即CacheMemoryContext
	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);

	
	 //尝试加载共享的系统表信息
	if (!load_relcache_init_file(true))//如果没有加载relcache的初始化文件
	{
		formrdesc("pg_database", DatabaseRelation_Rowtype_Id, true,
				  Natts_pg_database, Desc_pg_database);//加载pg_database系统表
		formrdesc("pg_authid", AuthIdRelation_Rowtype_Id, true,
				  Natts_pg_authid, Desc_pg_authid);//加载pg_authid系统表
		formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true,
				  Natts_pg_auth_members, Desc_pg_auth_members);//加载pg_auth_members系统表
		formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true,
				  Natts_pg_shseclabel, Desc_pg_shseclabel);//加载pg_shseclabel系统表
		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
				  Natts_pg_subscription, Desc_pg_subscription);//加载pg_subscription系统表

#define NUM_CRITICAL_SHARED_RELS	5	//表示上面加载系统表的数量,如果想要修改上面加载的表的话,记得修改这里
	}
	//回到之前的内存上下文
	MemoryContextSwitchTo(oldcxt);
}

RelationCacheInitializePhase3——初始化第三阶段

该函数的作用就是完成relcache的初始化。

void
RelationCacheInitializePhase3(void)
{
	HASH_SEQ_STATUS status;
	RelIdCacheEnt *idhentry;
	MemoryContext oldcxt;
	bool		needNewCacheFile = !criticalSharedRelcachesBuilt;

	

	//和前面一样,存储当前上下文,并切换到缓存上下文
	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
	
	//尝试加载一些系统表相关的索引
	if (IsBootstrapProcessingMode() ||
		!load_relcache_init_file(false))//
	{
		needNewCacheFile = true;

		formrdesc("pg_class", RelationRelation_Rowtype_Id, false,
				  Natts_pg_class, Desc_pg_class);//加载pg_class系统表
		formrdesc("pg_attribute", AttributeRelation_Rowtype_Id, false,
				  Natts_pg_attribute, Desc_pg_attribute);//加载pg_attribute系统表
		formrdesc("pg_proc", ProcedureRelation_Rowtype_Id, false,
				  Natts_pg_proc, Desc_pg_proc);//加载pg_proc系统表
		formrdesc("pg_type", TypeRelation_Rowtype_Id, false,
				  Natts_pg_type, Desc_pg_type);//加载pg_type系统表

#define NUM_CRITICAL_LOCAL_RELS 4	//这里也是加载的表的数量,如果要修改加载的表这里也要修改
	}
	
	//将上下文切回
	MemoryContextSwitchTo(oldcxt);

	if (!criticalRelcachesBuilt)//如果没有将关键的系统表索引加载进来
	{
		//现在需要加载这些关键的系统表索引
		//这些很关键,因为 catcache 或 opclass 缓存依赖于它们在 relcache 加载期间完成的提取。
		load_critical_index(ClassOidIndexId,
							RelationRelationId);
		load_critical_index(AttributeRelidNumIndexId,
							AttributeRelationId);
		load_critical_index(IndexRelidIndexId,
							IndexRelationId);
		load_critical_index(OpclassOidIndexId,
							OperatorClassRelationId);
		load_critical_index(AccessMethodProcedureIndexId,
							AccessMethodProcedureRelationId);
		load_critical_index(RewriteRelRulenameIndexId,
							RewriteRelationId);
		load_critical_index(TriggerRelidNameIndexId,
							TriggerRelationId);

#define NUM_CRITICAL_LOCAL_INDEXES	7	//加载的表数量

		criticalRelcachesBuilt = true;//加载完后标记为真
		//因此,有一个无限递归问题。可以通过在某些关键点进行堆扫描而不是索引扫描来打破递归。为了避免性能下降,在将关键索引加载到 relcache 之前这样做。因此,标志criticalRelcachesBuilt 用于决定是在关键点进行堆扫描还是索引扫描,在加载关键索引后将其设置为true。
	}

	 //处理关键的共享索引
	if (!criticalSharedRelcachesBuilt)//如果关键的共享索引未被加载
	{
		//DatabaseNameIndexId 对 relcache 加载并不重要,但对于 MyDatabaseId 的初始查找而言比较重要,否则将永远找不到任何非共享目录
		load_critical_index(DatabaseNameIndexId,
							DatabaseRelationId);
		//Autovacuum 使用数据库 OID 调用 InitPostgres,因此它依赖于 DatabaseOidIndexId
		load_critical_index(DatabaseOidIndexId,
							DatabaseRelationId);
		//还需要在 pg_authid 和 pg_auth_members 上确定一些索引,以便在客户端身份验证期间使用
		load_critical_index(AuthIdRolnameIndexId,
							AuthIdRelationId);
		load_critical_index(AuthIdOidIndexId,
							AuthIdRelationId);
		load_critical_index(AuthMemMemRoleIndexId,
							AuthMemRelationId);
		//SharedSecLabelObjectIndexId 对核心系统并不重要,但身份验证挂钩可能需要它
		load_critical_index(SharedSecLabelObjectIndexId,
							SharedSecLabelRelationId);

#define NUM_CRITICAL_SHARED_INDEXES 6	//加载的数量
		
		//该变量和上面的有差不多的含义
		criticalSharedRelcachesBuilt = true;
	}

	hash_seq_init(&status, RelationIdCache);
	
	//扫描hash表中所有relcache条目并更新来自 formrdesc 或 relcache 缓存文件的结果中可能有错误的任何内容
	while ((idhentry = (RelIdCacheEnt *) hash_seq_search(&status)) != NULL)
	{
		Relation	relation = idhentry->reldesc;//获取关系描述符
		bool		restart = false;//是否需要重启哈希表

		RelationIncrementReferenceCount(relation);//确保这个relcache条目在处理时还没被flush进磁盘。

		 //如果该条目是通过formrdesc伪造的
		if (relation->rd_rel->relowner == InvalidOid)
		{
			HeapTuple	htup;
			Form_pg_class relp;
			
			//读取真正的pg_class元组来替换虚假的条目
			htup = SearchSysCache1(RELOID,
								   ObjectIdGetDatum(RelationGetRelid(relation)));
			if (!HeapTupleIsValid(htup))
				elog(FATAL, "cache lookup failed for relation %u",
					 RelationGetRelid(relation));
			relp = (Form_pg_class) GETSTRUCT(htup);

			 //把获取的元组复制到relation的rd_rel属性中
			memcpy((char *) relation->rd_rel, (char *) relp, CLASS_TUPLE_SIZE);

			//更新re_options属性
			if (relation->rd_options)
				pfree(relation->rd_options);
			RelationParseRelOptions(relation, htup);
		
			//检查re_att中的值是否正确
			//因为它可能已经被复制到一个或多个catcache条目中,所以formrdesc必须在开始时正确设置re_att数据
			Assert(relation->rd_att->tdtypeid == relp->reltype);
			Assert(relation->rd_att->tdtypmod == -1);

			ReleaseSysCache(htup);

			//如果relowner属性经过上面的处理后仍然不正确,可能会导致无限循环
			if (relation->rd_rel->relowner == InvalidOid)
				elog(ERROR, "invalid relowner in pg_class entry for \"%s\"",
					 RelationGetRelationName(relation));

			restart = true;//标记为需要重新启动哈希表
		}

		 //修复未保存在relcache缓存文件中的数据
		 //修复规则相关信息
		if (relation->rd_rel->relhasrules && relation->rd_rules == NULL)
		//如果relhasrules为真但relation的re_rules为空,则说明相关数据是错误的或者过时的,需要修复
		{
			RelationBuildRuleLock(relation);//获取规则相关的数据,用于修复
			if (relation->rd_rules == NULL)//如果新获取到的rd_rules为NULL
				relation->rd_rel->relhasrules = false;//则标记relhasrules为false
			restart = true;//标记为需要重新启动哈希表
		}
		//修复触发器相关信息
		if (relation->rd_rel->relhastriggers && relation->trigdesc == NULL)
		//如果relhastriggers为真但relation的trigdesc为空,则说明相关数据是错误的或过时的,需要修复
		{
			RelationBuildTriggers(relation);//获取触发器相关的数据,用于修复
			if (relation->trigdesc == NULL)//如果新获取到的trigdesc为NULL
				relation->rd_rel->relhastriggers = false;//则标记relhastriggers为false
			restart = true;//标记为需要重新启动哈希表
		}

		 //修复行安全策略
		if (relation->rd_rel->relrowsecurity && relation->rd_rsdesc == NULL)
		//如果relrowsecurity为真,并且relation的rd_rsdesc为NULL,则说明相关数据是错误的,需要修复。
		{
			RelationBuildRowSecurity(relation);//重新载入行安全策略的信息
			Assert(relation->rd_rsdesc != NULL);
			restart = true;//标记为需要重新启动哈希表
		}

		 //修复分区表的分区键
		if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
			relation->rd_partkey == NULL)
			//如果该表为分区表并且分区键为NULL,说明矛盾,需要修复
		{
			RelationBuildPartitionKey(relation);//重新加载分区表的分区键
			Assert(relation->rd_partkey != NULL);//加载后仍为NULL则报错
			restart = true;//标记为需要重新启动哈希表
		}
		//修复分区表的描述符
		if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
			relation->rd_partdesc == NULL)
			//如果该表为分区表并且描述符为NULL,说明矛盾,需要修复
		{
			RelationBuildPartitionDesc(relation);//重新加载分区表的描述符
			Assert(relation->rd_partdesc != NULL);//如果加载后描述符仍未NULL则报错
			restart = true;//标记为需要重新启动哈希表
		}
		//修复表的访问方法
		if (relation->rd_tableam == NULL &&
			(relation->rd_rel->relkind == RELKIND_RELATION ||
			 relation->rd_rel->relkind == RELKIND_SEQUENCE ||
			 relation->rd_rel->relkind == RELKIND_TOASTVALUE ||
			 relation->rd_rel->relkind == RELKIND_MATVIEW))
			 //如果表的访问方法为NULL并且表的类型为普通表、sequence表、TOAST表、元数据表,则需要修复访问方法
		{
			RelationInitTableAccessMethod(relation);//重新加载表的访问方法
			Assert(relation->rd_tableam != NULL);//加载后仍为NULL则报错
			restart = true;//标记为需要重新启动哈希表
		}

		//释放拿到的表
		RelationDecrementReferenceCount(relation);

		
		if (restart)//如果需要重启,则重启哈希表
		{
			hash_seq_term(&status);
			hash_seq_init(&status, RelationIdCache);
		}
	}

	if (needNewCacheFile)//如果需要写新的relcache缓存文件
	{
		//强制所有 catcaches 完成初始化,从而打开它们使用的目录和索引。 这将使用所有最重要的系统目录和索引的条目预加载 relcache,以便 init 文件对后端最有用
		InitCatalogCachePhase2();

		//写文件
		write_relcache_init_file(true);
		write_relcache_init_file(false);
	}
}

RelCache的增删查

插入表

这是一个宏定义的函数,为了方便阅读和美观,我会将换行符去掉。

#define RelationCacheInsert(RELATION, replace_allowed)	
do { 
	RelIdCacheEnt *hentry;
	bool found; //标记是否在Hash表中找到对应的表的缓存
	//利用hash_search函数在Hash表中查找对应表的缓存
	hentry = (RelIdCacheEnt *) hash_search(RelationIdCache, 
										   (void *) &((RELATION)->rd_id), 
										   HASH_ENTER, &found); 
	if (found) //如果找到
	{ 
		Relation _old_rel = hentry->reldesc; //存放原来的关系描述符
		Assert(replace_allowed); //如果不允许替换则报错
		hentry->reldesc = (RELATION); //将Hash表项替换为新的关系描述符
		if (RelationHasReferenceCountZero(_old_rel)) //如果旧表缓存的引用次数为0
			RelationDestroyRelation(_old_rel, false); 
	} 
	else //如果没有找到
		hentry->reldesc = (RELATION);//直接将该表缓存插入到Hash表中
} while(0)
do{}while(0)结构

这里的宏定义用到了一个do{……}while(0)的结构,这个结构有什么作用呢?
*上关于这个问题的解答
我参考了这个解答,明白了该结构的作用,我会用我的方式,以实验的形式将其讲解一遍:
首先要明确的一点是,宏定义的本质就是字符串替换
我们定义下面这样三个函数

#define foo(x) printf("foo %d",x)
#define bar(x) printf("bar %d",x)
#define FOO(x) foo(x);bar(x)

然后再定义一个特殊情况:

if(1) FOO(x);
else printf("这里不可能输出");

而代码连编译都没通过
这是因为,这里经过预定义的字符串替换,转变成了

if(1) foo(x);
bar(x);
else printf("这里不可能输出");

这样,就可以发现明显的语法错误,if-else之间多了一条语句。
那加上大括号{}可不可以呢?

#define FOO(x) {foo(x);bar(x)}

加上大括号之后的FOO函数变成了上面的模样,看上去没什么问题。
我们继续编译刚才写的代码,发现仍旧编译错误,我们继续尝试字符串替换来找错误

if(1)
{
	foo(x);
	bar(x);
};//出错位置
else pritf("这里不可能输出");

可以很容易的发现,多了一个分号导致编译错误。这意味着我们如果想要使用FOO函数的话,就必须这样使用:

FOO(x) //后面不加分号

否则就可能造成上面的错误。但这样显然是有悖于我们的编码习惯的,很容易忘记而导致编译错误。
这时,do-while(0)就派上用场了。

#define FOO(x) \
do \
{ \
	foo(x); \
	bar(x); \
}while(0) 

这时,再次编译上面编写的if-else就能够编译通过了,并且经过运行结果符合预期。
这里相较于大括号来说,起关键作用的是while(0),因为while(0)后面需要加分号,从而使得FOO(x);的写法变得正确。

删除表

这是一个宏定义的函数,为了方便阅读和美观,我会将换行符去掉。

#define RelationCacheDelete(RELATION) 
do { 
	RelIdCacheEnt *hentry; 
	//调用函数hash_search,指定查询模式为HASH_REVOKE,进行搜索,如果找到对应的Hash桶则会直接删除
	hentry = (RelIdCacheEnt *) hash_search(RelationIdCache, 
										   (void *) &((RELATION)->rd_id), 
										   HASH_REMOVE, NULL); 
	if (hentry == NULL) //未找到则返回NULL
		elog(WARNING, "failed to delete relcache entry for OID %u", 
			 (RELATION)->rd_id); 
} while(0)

查询Hash表

这是一个宏定义的函数,为了方便阅读和美观,我会将换行符去掉。

#define RelationIdCacheLookup(ID, RELATION) 
do { 
	RelIdCacheEnt *hentry; 
	//调用函数hash_search,指定查询模式为HASH_FIND,进行搜索
	hentry = (RelIdCacheEnt *) hash_search(RelationIdCache, 
										   (void *) &(ID), 
										   HASH_FIND, NULL); 
	if (hentry) //若找到ID对应的RelIdCacheEnt,则将其reldesc属性的值赋值给RELATION
		RELATION = hentry->reldesc; 
	else //未找到
		RELATION = NULL; //将NULL赋值给RELATION
} while(0)

cache同步

上篇博客提到过,在postgreSQL中每个进程有属于自己的Cache,这意味着会有这样的情况出现:同一个系统表在不同的进程中都有对应的Cache来缓存它的元组。也就是说,同一个系统表的元组可能同时被多个进程的Cache所缓存,因而当某个Cache中的一个元组被删除或者更新时,需要通知其他进程对Cache进行同步,不然会造成不一致的现象。

而postgreSQL是如何实现这一同步的?
postgreSQL会记录下已经被删除的无效元组,并通过共享消息队列(SI Message)的方式在进程间传递这一消息。收到无效消息的进程同步地把无效元组从自己的Cache中删除。

无效消息

无效消息的结构体

typedef union
{
	int8		id;				//为非负数时表示CatCache,-为1时表示RelCache,-2表示SMGR
	//当id为非负数时,也表示产生该无效消息的CatCache的编号
	
	//这些属性分别在id取值为非负数、-1和-2时有有效值,其他情况下会为空。
	SharedInvalCatcacheMsg cc; 
	SharedInvalCatalogMsg cat;
	SharedInvalRelcacheMsg rc;
	SharedInvalSmgrMsg sm;
	SharedInvalRelmapMsg rm;
	SharedInvalSnapshotMsg sn;
} SharedInvalidationMessage;

当前postgreSQL支持三种无效消息传递方式:

  1. 使SysCache中元组失效
  2. 使RelCache中RelationData结构无效
  3. 使SMGR无效(表物理位置发生变化时,需要通知SMGR关闭表文件)

进程间通过调用函数CacheInvalidateHeapTuple对无效消息进行注册,这里我就不详细分析了,大致讲讲这个函数都做了什么:

  1. 注册SysCache无效消息
  2. 如果是对pg_class系统表元组进行的更新或者删除操作,它的relfilenode或者reltablespace可能发生变化,也就是说该表的物理位置发生变化,需要通知其他进程关闭响应的SMGR。这时首先设置relationid和databaseid,然后注册SMGR无效消息。
  3. 如果实对pg_attribute或者pg_index系统表元组进行的更新或者删除操作,则设置relationid和databaseid,否则返回。
  4. 注册RelCache无效消息。

当一个元组被删除或者更新时,在同一个SQL命令的后续执行步骤中依然认为该元组是有效的,知道下一个命令开始或者事务提交时改动才生效。在命令的边界,旧元组失效,同时新元组变为有效。因此当执行heap_delete或者heap_update这样的操作时,不能简单地刷新Cache。而且,即使刷新了,也可能由于同一个命令中地请求把该元组再次加载到Cache中。因此正确的方法是保持一个无效链表用于记录元组的删除或者更新操作。
事务完成后,根据上面的无效链表中的信息广播该事务过程中产生的无效消息,其他进程通过SI Message队列读取无效消息对各自的Cache进行刷新。当子事务提交时,只需要将该事务产生的无效消息队列提交到父事务,最后由上层的事务广播无效消息。

总结

通过分析源码,了解了postgreSQL中的relcache的结构以及操作,还通过阅读相关资料和分析源码了解了cache同步的机制,还掌握了宏定义中do{}while(0)这一结构的特殊用法。

上一篇:JDBC连接数据库报错:java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. ......


下一篇:[java]编程的智慧(转)