SysCache的部分匹配机制
- 精确匹配査找
- 搜索的主力代码:
- 在第一次进入SearchCatCache函数时,由于系统表还没有加载到SysCache的相关结构体中,需要调用一次CatalogCacheInitializeCache(cache)。
- 计算哈希值,得到哈希桶
- SearchCatCache函数途中有调用函数CatalogCacheCompareTuple
- 如果迭代结束都没有找到匹配的元组,就到了SearchCatCacheMiss函数,不只是返回缓存查找失败。
- 对物理表进行扫描时需要调用函数IndexScanOK
- 将元组放入哈希桶,或者创建负元组时,需要调用函数CatalogCacheCreateEntry
- 调用RehashCatCache
- 需要调用函数CatCacheCopyKeys
- 部分查找
catcache代码位于src/backend/utils/cache/catcache.c,包含了对SysCache结构体的初始化和数据结构之间指针关系的链接以及操作。
在CatCache中査找元组
在CatCache中查找元组有两种方式:精确匹配SearchCatCache和部分匹配SearchCatcacheList。前者用于给定CatCache所需的所有键值,并返回CatCache中能完全匹配这个键值的元组;而后者只需要给出部分键值,并将部分匹配的元组以一个CatCList的方式返回。
精确匹配査找
由函数SearchCatCache函数实现,其函数如下:
SearchCatCache(CatCache *cache,//要查找的cacheID
Datum v1, //v1\v2\v3\v4都用于查找元组的键值,分别对应该Cache中记录的元组搜索键。
Datum v2,
Datum v3,
Datum v4)
{
return SearchCatCacheInternal(cache, cache->cc_nkeys, v1, v2, v3, v4);//调用SearchCatCache函数进行查找
}
当参数数量特定时,编译器会选择SearchCatCacheN()内联主体和展开循环,使它们比SearchCatCache()快一些。
//第一种,一个键的部分搜索
HeapTuple
SearchCatCache1(CatCache *cache,Datum v1)
{return SearchCatCacheInternal(cache, 1, v1, 0, 0, 0);}
//第二种,两个键的部分搜索
HeapTuple
SearchCatCache2(CatCache *cache,Datum v1, Datum v2)
{return SearchCatCacheInternal(cache, 2, v1, v2, 0, 0);}
//第三种,三个键的部分搜索
HeapTuple
SearchCatCache3(CatCache *cache,Datum v1, Datum v2, Datum v3)
{return SearchCatCacheInternal(cache, 3, v1, v2, v3, 0);}
//第四种,四个键的部分搜索(和精确查找没有区别)
HeapTuple
SearchCatCache4(CatCache *cache,Datum v1, Datum v2, Datum v3, Datum v4)
{return SearchCatCacheInternal(cache, 4, v1, v2, v3, v4);}
调用SearchCatCacheInternal函数时,把没有的key用0代替。其余步骤都和精确搜索一样。
搜索的主力代码:
static inline HeapTuple
SearchCatCacheInternal(CatCache *cache,int nkeys,Datum v1,Datum v2,Datum v3,Datum v4)
{
Datum arguments[CATCACHE_MAXKEYS];//参数数组,用于存放键值
uint32 hashValue;
Index hashIndex;
dlist_iter iter;
dlist_head *bucket;
CatCTup *ct;
/* 确保我们在一个精确的行动,即使这最终是一个缓存命中 */
Assert(IsTransactionState());
Assert(cache->cc_nkeys == nkeys);
/* 每个缓存的一次性启动开销 */
if (unlikely(cache->cc_tupdesc == NULL))
CatalogCacheInitializeCache(cache);
#ifdef CATCACHE_STATS
cache->cc_searches++;
#endif
/* 初始化局部参数数组 */
arguments[0] = v1;
arguments[1] = v2;
arguments[2] = v3;
arguments[3] = v4;
/* 计算出哈希值
* 根据哈希值找到对应的哈希桶序号
*/
hashValue = CatalogCacheComputeHashValue(cache, nkeys, v1, v2, v3, v4);
hashIndex = HASH_INDEX(hashValue, cache->cc_nbuckets);
/*
* 根据刚才得到的哈希桶序号获取哈希桶
* Note: 在这里可以使用dlist_foreach,因为即使我们在循环中修改了dlist,之后也不会继续循环。
*/
bucket = &cache->cc_bucket[hashIndex];
dlist_foreach(iter, bucket)//对哈希桶进行迭代
{
ct = dlist_container(CatCTup, cache_elem, iter.cur);
if (ct->dead)
continue; /* 忽略死亡的元组 */
if (ct->hash_value != hashValue)
continue; /* 哈希值错误则快速跳过 */
if (!CatalogCacheCompareTuple(cache, nkeys, ct->keys, arguments))//如果键值不匹配则跳过
continue;
/*
* 能够执行到这里说明在cache中找到了匹配的元组,将该元组移动到该哈希桶的链表头,这
* 是为了加快后续搜索。
* 这种采取了将最近访问最频繁的元素放在链表前部的做法能够使得频繁访问的元素得到快速
* 访问。
*/
dlist_move_head(bucket, &ct->cache_elem);
/* 对该元组进行判断 */
if (!ct->negative)//如果该元组不是负元组
{
ResourceOwnerEnlargeCatCacheRefs(CurrentResourceOwner);
ct->refcount++;//将它的引用技术加一
ResourceOwnerRememberCatCacheRef(CurrentResourceOwner, &ct->tuple);
CACHE_elog(DEBUG2, "SearchCatCache(%s): found in bucket %d",
cache->cc_relname, hashIndex);
#ifdef CATCACHE_STATS
cache->cc_hits++;//缓存命中次数加一
#endif
return &ct->tuple;//返回该元组
}
else//如果是负元组
{
CACHE_elog(DEBUG2, "SearchCatCache(%s): found neg entry in bucket %d",
cache->cc_relname, hashIndex);
#ifdef CATCACHE_STATS
cache->cc_neg_hits++;//缓存的负元组命中加一
#endif
return NULL;//返回未找到
}
}
//如果执行到这里,说明迭代结束都没有找到匹配的元组
return SearchCatCacheMiss(cache, nkeys, hashValue, hashIndex, v1, v2, v3, v4);//缓存查找失败
}
在第一次进入SearchCatCache函数时,由于系统表还没有加载到SysCache的相关结构体中,需要调用一次CatalogCacheInitializeCache(cache)。
/*
* 初始化目录缓存
* 这个函数对catcache进行最终的初始化:获取元组描述符并设置哈希和等式函数链接。
*/
#ifdef CACHEDEBUG
#define CatalogCacheInitializeCache_DEBUG1 \ elog(DEBUG2, "CatalogCacheInitializeCache: cache @%p rel=%u", cache, \ cache->cc_reloid)
#define CatalogCacheInitializeCache_DEBUG2 \ do { \
if (cache->cc_keyno[i] > 0) { \
elog(DEBUG2, "CatalogCacheInitializeCache: load %d/%d w/%d, %u", \
i+1, cache->cc_nkeys, cache->cc_keyno[i], \
TupleDescAttr(tupdesc, cache->cc_keyno[i] - 1)->atttypid); \
} else { \
elog(DEBUG2, "CatalogCacheInitializeCache: load %d/%d w/%d", \
i+1, cache->cc_nkeys, cache->cc_keyno[i]); \
} \
} while(0)
#else
#define CatalogCacheInitializeCache_DEBUG1
#define CatalogCacheInitializeCache_DEBUG2
#endif
/*
* 调用 CatalogCacheInitializeCache 初始化cache相关信息
* (这个时候不会查数据的,但是会把字段类型,hash函数什么的设置好
* 所以这个函数中会调用 heap_open 读取字段类型)
*/
static void
CatalogCacheInitializeCache(CatCache *cache)
{
Relation relation;//存放表
MemoryContext oldcxt;//存储上下文
TupleDesc tupdesc; //永久缓存存储
int i; //用于循环调用,初始化缓存的关键信息
CatalogCacheInitializeCache_DEBUG1;
//打开对应的系统表
relation = table_open(cache->cc_reloid, AccessShareLock);
/* 切换到缓存上下文,这样我们的分配不会在事务结束时消失 */
Assert(CacheMemoryContext != NULL);
oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
/* 将Relcache的元组描述符复制到永久缓存存储中 */
tupdesc = CreateTupleDescCopyConstr(RelationGetDescr(relation));
/* 也保存关系的名称和relisshared标志(cc_relname仅用于调试目的) */
cache->cc_relname = pstrdup(RelationGetRelationName(relation));
cache->cc_relisshared = RelationGetForm(relation)->relisshared;
/* 返回调用者的内存上下文并关闭rel */
MemoryContextSwitchTo(oldcxt);
table_close(relation, AccessShareLock);
CACHE_elog(DEBUG2, "CatalogCacheInitializeCache: %s, %d keys",
cache->cc_relname, cache->cc_nkeys);
/* 初始化缓存的关键信息 */
for (i = 0; i < cache->cc_nkeys; ++i)
{
Oid keytype;
RegProcedure eqfunc;
CatalogCacheInitializeCache_DEBUG2;
if (cache->cc_keyno[i] > 0)
{
Form_pg_attribute attr = TupleDescAttr(tupdesc,
cache->cc_keyno[i] - 1);
keytype = attr->atttypid;
/* 缓存键列应该总是NOT NULL */
Assert(attr->attnotnull);
}
else
{if (cache->cc_keyno[i] < 0)//在缓存中不支持Sys属性
elog(FATAL, "sys attributes are not supported in caches");
keytype = OIDOID;
}
GetCCHashEqFuncs(keytype,
&cache->cc_hashfunc[i],
&eqfunc,
&cache->cc_fastequal[i]);
/* 进行相等函数的查找(我们假设这将不需要任何支持类型的目录查找) */
fmgr_info_cxt(eqfunc,
&cache->cc_skey[i].sk_func,
CacheMemoryContext);
/* 为HeapKeyTest()和堆扫描适当地初始化sk_attno */
cache->cc_skey[i].sk_attno = cache->cc_keyno[i];
/* 也要填入sk_strategy——总是标准的等式 */
cache->cc_skey[i].sk_strategy = BTEqualStrategyNumber;
cache->cc_skey[i].sk_subtype = InvalidOid;
/* 如果一个catcache键需要排序规则,它必须是C排序规则 */
cache->cc_skey[i].sk_collation = C_COLLATION_OID;
//初始化目录缓存
CACHE_elog(DEBUG2, "CatalogCacheInitializeCache %s %d %p",
cache->cc_relname, i, cache);
}
/* 标记此缓存已完全初始化 */
cache->cc_tupdesc = tupdesc;
}
调用查找支持函数
/* 类型的查找支持函数。 */
static void
GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEqualFN *fasteqfunc)
{
switch (keytype)
{
case BOOLOID:
*hashfunc = charhashfast;
*fasteqfunc = chareqfast;
*eqfunc = F_BOOLEQ;
break;
case CHAROID:
*hashfunc = charhashfast;
*fasteqfunc = chareqfast;
*eqfunc = F_CHAREQ;
break;
case NAMEOID:
*hashfunc = namehashfast;
*fasteqfunc = nameeqfast;
*eqfunc = F_NAMEEQ;
break;
case INT2OID:
*hashfunc = int2hashfast;
*fasteqfunc = int2eqfast;
*eqfunc = F_INT2EQ;
break;
case INT4OID:
*hashfunc = int4hashfast;
*fasteqfunc = int4eqfast;
*eqfunc = F_INT4EQ;
break;
case TEXTOID:
*hashfunc = texthashfast;
*fasteqfunc = texteqfast;
*eqfunc = F_TEXTEQ;
break;
case OIDOID:
case REGPROCOID:
case REGPROCEDUREOID:
case REGOPEROID:
case REGOPERATOROID:
case REGCLASSOID:
case REGTYPEOID:
case REGCONFIGOID:
case REGDICTIONARYOID:
case REGROLEOID:
case REGNAMESPACEOID:
*hashfunc = int4hashfast;
*fasteqfunc = int4eqfast;
*eqfunc = F_OIDEQ;
break;
case OIDVECTOROID:
*hashfunc = oidvectorhashfast;
*fasteqfunc = oidvectoreqfast;
*eqfunc = F_OIDVECTOREQ;
break;
default:
//关键字的类型不被支持作为缓存键
elog(FATAL, "type %u not supported as catcache key", keytype);
*hashfunc = NULL; /* 让编译器保持安静 */
*eqfunc = InvalidOid;
break;
}
}
计算哈希值,得到哈希桶
将v1\v2\v3\v4设置到cur_skey数组相应元素中,调用CatalogCacheComputeHashValue(cache, cache->cc_nkeys, cur_skey)计算哈希值
/*
* CatalogCacheComputeHashValue
* 通过之前已经设定好的hash函数 计算哈希值
* 计算与给定一组查找键相关联的散列值
*/
static uint32
CatalogCacheComputeHashValue(CatCache *cache, int nkeys,
Datum v1, Datum v2, Datum v3, Datum v4)
{
uint32 hashValue = 0;
uint32 oneHash;
CCHashFN *cc_hashfunc = cache->cc_hashfunc;
CACHE_elog(DEBUG2, "CatalogCacheComputeHashValue %s %d %p",cache->cc_relname, nkeys, cache);
switch (nkeys)
{
case 4:
oneHash = (cc_hashfunc[3]) (v4);
hashValue ^= oneHash << 24;
hashValue ^= oneHash >> 8;
/* 失败 */
case 3:
oneHash = (cc_hashfunc[2]) (v3);
hashValue ^= oneHash << 16;
hashValue ^= oneHash >> 16;
/* 失败 */
case 2:
oneHash = (cc_hashfunc[1]) (v2);
hashValue ^= oneHash << 8;
hashValue ^= oneHash >> 24;
/* 失败 */
case 1:
oneHash = (cc_hashfunc[0]) (v1);
hashValue ^= oneHash;
break;
default:
//散列的键数错误:
elog(FATAL, "wrong number of hash keys: %d", nkeys);
break;
}
return hashValue;
}
使用HASH_INDEX宏计算哈希桶的索引,按照该索引得到该CatCache在cc_bucket数组中对应的Hash桶的下标。
遍历Hash桶链找到满足查询需求的Dlelem,并将其结构体中dle_val属性强制转换为CatCTup类型,使用HeapKeyTest测试缓存的tuple是否匹配输入的键。如果找到,使用DLMoveToFront将该元组放到Hash桶的首位。如果是正元组,refcount和cc_hits都加1,返回元组。如果为负元组,cc_neg_hits加1,返回NULL。
如果没有找到,说明SysCache中没有缓存相应的元组,需要进一步对物理系统表进行扫描,以确认要查找的元组是确实不存在还是没有缓存在CatCache中。如果扫描物理系统表能够找到满足条件的余罪女主,则需要将元组包装成Dlelem之后加入到其对应的Hash桶内链表头部,并返回元组,如果在物理系统表中找不到要查找的元组,则说明该元组确实不存在,此时构建一个只有键值但没有实际元组的负元组,并将它包装好加入到Hash桶内链表头部。
/*
* CatalogCacheComputeTupleHashValue
* 计算元组哈希值
* 计算与要缓存的给定元组关联的哈希值
*/
static uint32
CatalogCacheComputeTupleHashValue(CatCache *cache, int nkeys, HeapTuple tuple)
{
Datum v1 = 0,//v1\v2\v3\v4都用于计算元组的哈希值,分别对应该Cache中记录的元组搜索键。
v2 = 0,
v3 = 0,
v4 = 0;
bool isNull = false;
int *cc_keyno = cache->cc_keyno;
TupleDesc cc_tupdesc = cache->cc_tupdesc;
/* 从元组提取关键字段,插入到scankey */
switch (nkeys)
{
case 4:
v4 = fastgetattr(tuple,
cc_keyno[3],
cc_tupdesc,
&isNull);
Assert(!isNull);
/* 失败 */
case 3:
v3 = fastgetattr(tuple,
cc_keyno[2],
cc_tupdesc,
&isNull);
Assert(!isNull);
/* 失败 */
case 2:
v2 = fastgetattr(tuple,
cc_keyno[1],
cc_tupdesc,
&isNull);
Assert(!isNull);
/* 失败 */
case 1:
v1 = fastgetattr(tuple,
cc_keyno[0],
cc_tupdesc,
&isNull);
Assert(!isNull);
break;
default:
elog(FATAL, "wrong number of hash keys: %d", nkeys);
break;
}
return CatalogCacheComputeHashValue(cache, nkeys, v1, v2, v3, v4);
}
SearchCatCache函数途中有调用函数CatalogCacheCompareTuple
/*
* CatalogCacheCompareTuple
* 目录缓存比较元组
* 比较一个元组和传递的参数。
*/
static inline bool
CatalogCacheCompareTuple(const CatCache *cache, int nkeys,
const Datum *cachekeys,
const Datum *searchkeys)
{
const CCFastEqualFN *cc_fastequal = cache->cc_fastequal;
int i;
for (i = 0; i < nkeys; i++)
{
if (!(cc_fastequal[i]) (cachekeys[i], searchkeys[i]))
return false;
}
return true;
}
如果迭代结束都没有找到匹配的元组,就到了SearchCatCacheMiss函数,不只是返回缓存查找失败。
static pg_noinline HeapTuple
SearchCatCacheMiss(CatCache *cache,int nkeys,uint32 hashValue,Index hashIndex,
Datum v1,Datum v2,Datum v3,Datum v4)
{
ScanKeyData cur_skey[CATCACHE_MAXKEYS];//存放扫描用关键字
Relation relation;//存放表
SysScanDesc scandesc;//系统表扫描描述符
HeapTuple ntp;//存放元组
CatCTup *ct;//即哈希桶中元组指针
Datum arguments[CATCACHE_MAXKEYS];//存放关键字参数
//初始化参数数组
arguments[0] = v1;
arguments[1] = v2;
arguments[2] = v3;
arguments[3] = v4;
//将物理系统表
memcpy(cur_skey, cache->cc_skey, sizeof(ScanKeyData) * nkeys);
cur_skey[0].sk_argument = v1;
cur_skey[1].sk_argument = v2;
cur_skey[2].sk_argument = v3;
cur_skey[3].sk_argument = v4;
//打开对应的系统表
relation = table_open(cache->cc_reloid, AccessShareLock);
//对物理系统表进行扫描,确认要查找的元组是确实不存在还是没有缓存在CatCache中
scandesc = systable_beginscan(
relation,cache->cc_indexoid,
IndexScanOK(cache,cur_skey),NULL,nkeys,cur_skey);
ct = NULL;
while (HeapTupleIsValid(ntp = systable_getnext(scandesc)))//从打开的系统表中获取元组
{
ct = CatalogCacheCreateEntry(cache, ntp, arguments,
hashValue, hashIndex,
false);//将元组放入哈希桶中
ResourceOwnerEnlargeCatCacheRefs(CurrentResourceOwner);
ct->refcount++;//立刻将引用次数设置为1
ResourceOwnerRememberCatCacheRef(CurrentResourceOwner, &ct->tuple);
break;//这是break是因为默认只找一个符合的
}
systable_endscan(scandesc);//关闭描述符
table_close(relation, AccessShareLock);//关闭打开的表
if (ct == NULL)//如果刚才没有在系统表中找到对应元组
{
ct = CatalogCacheCreateEntry(cache, NULL, arguments,
hashValue, hashIndex,
true);//构建一个负元组
CACHE_elog(DEBUG2, "SearchCatCache(%s): Contains %d/%d tuples",
cache->cc_relname, cache->cc_ntup, CacheHdr->ch_ntup);
CACHE_elog(DEBUG2, "SearchCatCache(%s): put neg entry in bucket %d",
cache->cc_relname, hashIndex);
//由于返回未找到,因此引用计数不必加一
return NULL;//返回未找到
}
CACHE_elog(DEBUG2, "SearchCatCache(%s): Contains %d/%d tuples",
cache->cc_relname, cache->cc_ntup, CacheHdr->ch_ntup);
CACHE_elog(DEBUG2, "SearchCatCache(%s): put in bucket %d",
cache->cc_relname, hashIndex);
#ifdef CATCACHE_STATS
cache->cc_newloads++;//缓存载入次数加一
#endif
return &ct->tuple;//返回装入ct数据结构中的元组
}
对物理表进行扫描时需要调用函数IndexScanOK
/*
* IndexScanOK
* 扫描索引
*
* 这个函数检查IndexSupportInitialize()在relcache初始化期间为支持关键syscache的某些系统索引获取的元组。
* 我们不能使用indexscan来获取这些,否则我们将进入无限递归。不过,普通的堆扫描可以工作。一旦我们完成relcache初始化(由criticalRelcachesBuilt发出信号),我们就不用再担心了。
*
* 类似地,在后端启动期间,我们必须能够使用pg_authid和pg_auth_members syscaches进行身份验证,即使我们还没有这些目录索引的relcache条目。
*/
static bool
IndexScanOK(CatCache *cache, ScanKey cur_skey)
{
switch (cache->id)
{
case INDEXRELID:
/*
* 在我们使用indexscans(它总是在变化)之前,我们不需要精确地跟踪哪些索引必须被加载,只要强制所有的pg_index搜索都是堆扫描,直到我们建立了关键的relcache。
*/
if (!criticalRelcachesBuilt)
return false;
break;
case AMOID:
case AMNAME:
/*
* 总是在pg_am中做堆扫描,因为它是如此之小,无论如何indexscan都没有多少意
* 义。在最初构建关键的relcache条目时,我们“必须”这样做,但我们也可以一直这
* 样做。
*/
return false;
case AUTHNAME:
case AUTHOID:
case AUTHMEMMEMROLE:
/*
* 保护在relcache为共享索引收集条目之前发生的身份验证查找。
*/
if (!criticalSharedRelcachesBuilt)
return false;
break;
default:
break;
}
/* 正常情况,允许索引扫描 */
return true;
}
将元组放入哈希桶,或者创建负元组时,需要调用函数CatalogCacheCreateEntry
/*
* CatalogCacheCreateEntry
* CatalogCache创建条目
* 创建一个新的catcup条目,将给定的HeapTuple和其他提供的数据复制到其中。
* 新条目最初的refcount为0。
*/
static CatCTup *
CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
uint32 hashValue, Index hashIndex,
bool negative)
{
CatCTup *ct;
HeapTuple dtp;
MemoryContext oldcxt;
/* 负的项没有关联的元组 */
if (ntp)
{
int i;
Assert(!negative);
/*
* 如果在元组中有任何超出行的toasted字段,请将它们内联展开。这在以后使用catcache
* 项时节省了周期,而且还防止了toast元组在试图获取它们之前被释放的可能性,以防某些
* 东西使用了稍微过时的catcache项。
*/
if (HeapTupleHasExternal(ntp))
dtp = toast_flatten_tuple(ntp, cache->cc_tupdesc);
else
dtp = ntp;
/* 一次性为catcup和缓存元组分配内存 */
oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
ct = (CatCTup *) palloc(sizeof(CatCTup) +
MAXIMUM_ALIGNOF + dtp->t_len);
ct->tuple.t_len = dtp->t_len;
ct->tuple.t_self = dtp->t_self;
ct->tuple.t_tableOid = dtp->t_tableOid;
ct->tuple.t_data = (HeapTupleHeader)
MAXALIGN(((char *) ct) + sizeof(CatCTup));
/* 元组的内容复制 */
memcpy((char *) ct->tuple.t_data,
(const char *) dtp->t_data,
dtp->t_len);
MemoryContextSwitchTo(oldcxt);
if (dtp != ntp)
heap_freetuple(dtp);
/* 提取键——如果不是按值,它们将指向元组 */
for (i = 0; i < cache->cc_nkeys; i++)
{
Datum atp;
bool isnull;
atp = heap_getattr(&ct->tuple,
cache->cc_keyno[i],
cache->cc_tupdesc,
&isnull);
Assert(!isnull);
ct->keys[i] = atp;
}
}
else
{
Assert(negative);
oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
ct = (CatCTup *) palloc(sizeof(CatCTup));
/* 存储键——它们将指向单独分配的内存,如果不是按值的话。 */
CatCacheCopyKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno,
arguments, ct->keys);
MemoryContextSwitchTo(oldcxt);
}
/* 完成catcup头的初始化,并将其添加到缓存的链表和计数中。 */
ct->ct_magic = CT_MAGIC;
ct->my_cache = cache;
ct->c_list = NULL;
ct->refcount = 0; /* 目前 */
ct->dead = false;
ct->negative = negative;
ct->hash_value = hashValue;
dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
cache->cc_ntup++;
CacheHdr->ch_ntup++;
/* 如果哈希表太满,则扩大bucket数组。当填充因子>2,我们任意放大 */
if (cache->cc_ntup > cache->cc_nbuckets * 2)
RehashCatCache(cache);
return ct;
}
调用RehashCatCache
/* 扩大缓存,使桶的数量加倍。 */
static void
RehashCatCache(CatCache *cp)
{
dlist_head *newbucket;
int newnbuckets;
int i;
//重新散列存储桶的目录缓存id
elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets",
cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets);
/* 分配一个新的、更大的哈希表。 */
newnbuckets = cp->cc_nbuckets * 2;
newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head));
/* 将旧哈希表中的所有条目移动到新的哈希表中。 */
for (i = 0; i < cp->cc_nbuckets; i++)
{
dlist_mutable_iter iter;
dlist_foreach_modify(iter, &cp->cc_bucket[i])
{
CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur);
int hashIndex = HASH_INDEX(ct->hash_value, newnbuckets);//计算出哈希值,根据哈希值找到对应的哈希桶序号
dlist_delete(iter.cur);
dlist_push_head(&newbucket[hashIndex], &ct->cache_elem);
}
}
/* 切换到新阵列。 */
pfree(cp->cc_bucket);
cp->cc_nbuckets = newnbuckets;
cp->cc_bucket = newbucket;
}
需要调用函数CatCacheCopyKeys
/* Helper例程,它将srckeys数组中的键复制到dstkeys数组中,确保数据在当前内存上下文中被充分分配。 */
static void
CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
Datum *srckeys, Datum *dstkeys)
{
int i;
/* 通过将所有键存储在一个分配中,可以提高内存和查找性能。 */
for (i = 0; i < nkeys; i++)
{
int attnum = attnos[i];
Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1);
Datum src = srckeys[i];
NameData srcname;
/* 在调用者传递一个需要NAME的C字符串时必须小心:将给定的参数转换为正确填充的NAME。否则,由datumCopy()完成的memcpy()可能会从内存的末端脱落。 */
if (att->atttypid == NAMEOID)
{
namestrcpy(&srcname, DatumGetCString(src));
src = NameGetDatum(&srcname);
}
dstkeys[i] = datumCopy(src,
att->attbyval,
att->attlen);
}
}
部分查找
部分匹配使用函数SearchCatcacheList,该函数产生一个CatCList结构,其中以链表的方式存放了在Cache中找到的元组。
CatCList中的tuple字段记录的是一个负元组,它仅仅用来存放该CatCList所用到的键值,没有其他用户。CatCList中所包含的元组实际通过members字段表示的变长数据来记录,该数组的实际长度由n_members字段记录。
SearchCatCacheList函数也会先计算查找键的Hash值,不过该函数首先会在CatCache的cc_lists字段中记录的CatCList链表中查找以前是否缓存了该查找键的结果,该查找过程将使用CatCList中tuple字段指向的元组与查找键进行Hash值比较。
如果能够找到匹配该Hash值的CatCList,则cc_lhits加1并将该CatCList移到cc_lists所指向链表的头部,然后返回找到的CatCList。如果在CatCache中找不到CatCList,则扫描物理系统表并构建相应的CatCList并将它加入到cc_lists所指向链表的头部。
主要代码
/*
* SearchCatCacheList
*
* 生成与部分键匹配的所有元组的列表(也就是说,一个键只指定缓存的N个键列中的前K个)。
* 在这里指定缓存的所有键列没有任何意义:因为键是唯一的,最多只能有一个匹配,所以你应该使用SearchCatCache()代替。
* 因此,这个函数比SearchCatCache()少接受一个Datum参数。
*/
CatCList *
SearchCatCacheList(CatCache *cache,
int nkeys,
Datum v1,
Datum v2,
Datum v3)
{
Datum v4 = 0; /* 最后一列值 */
Datum arguments[CATCACHE_MAXKEYS];
uint32 lHashValue;
dlist_iter iter;
CatCList *cl;
CatCTup *ct;
List *volatile ctlist;
ListCell *ctlist_item;
int nmembers;
bool ordered;
HeapTuple ntp;
MemoryContext oldcxt;
int i;
/* 每个缓存的一次性启动开销 */
if (cache->cc_tupdesc == NULL)
CatalogCacheInitializeCache(cache);
Assert(nkeys > 0 && nkeys < cache->cc_nkeys);
#ifdef CATCACHE_STATS
cache->cc_lsearches++;
#endif
/* 初始化局部参数数组 */
arguments[0] = v1;
arguments[1] = v2;
arguments[2] = v3;
arguments[3] = v4;
/* 计算给定键的哈希值,以便更快地进行搜索。我们目前还没有将CatCList项目划分为桶,但这仍然允许我们在大多数时间快速跳过不匹配的项目。 */
lHashValue = CatalogCacheComputeHashValue(cache, nkeys, v1, v2, v3, v4);
/*
*扫描这些项目,直到我们找到匹配的或耗尽我们的列表
*注意:在这里可以使用dlist_foreach,因为即使我们在循环中修改了dlist,之后也不会继续循环。
*/
dlist_foreach(iter, &cache->cc_lists)
{
cl = dlist_container(CatCList, cache_elem, iter.cur);
if (cl->dead)
continue; /* 忽略死亡的元组 */
if (cl->hash_value != lHashValue)
continue; /* 哈希值错误则快速跳过 */
/* 看看缓存列表是否与我们的键匹配。 */
if (cl->nkeys != nkeys)
continue;
if (!CatalogCacheCompareTuple(cache, nkeys, cl->keys, arguments))//如果键值不匹配则跳过
continue;
/*
* 能够执行到这里说明在cache中找到了匹配的元组,将该元组移动到该哈希桶的链表头,这
* 是为了加快后续搜索。
* 这种采取了将最近访问最频繁的元素放在链表前部的做法能够使得频繁访问的元素得到快速
* 访问。
*/
dlist_move_head(&cache->cc_lists, &cl->cache_elem);
/* 增加列表的引用计数并返回它 */
ResourceOwnerEnlargeCatCacheListRefs(CurrentResourceOwner);
cl->refcount++;//将它的引用技术加一
ResourceOwnerRememberCatCacheListRef(CurrentResourceOwner, cl);
CACHE_elog(DEBUG2, "SearchCatCacheList(%s): found list",
cache->cc_relname);
#ifdef CATCACHE_STATS
cache->cc_lhits++;//缓存命中次数加一
#endif
return cl;//返回该元组
}
/* List没有在缓存中找到,所以我们必须通过读取关系来构建它。对于在关系中找到的每个匹配元
* 组,如果可能,使用现有的缓存条目,否则构建一个新的。
* 我们必须临时增加成员的refcounts,以确保它们不会在加载其他成员时从缓存中删除。我们使
* 用一个PG_TRY块来确保如果在构建完CatCList之前出现错误,我们可以撤销这些refcount。
*/
ResourceOwnerEnlargeCatCacheListRefs(CurrentResourceOwner);
ctlist = NIL;
PG_TRY();
{
ScanKeyData cur_skey[CATCACHE_MAXKEYS];
Relation relation;
SysScanDesc scandesc;
/* 需要在关系中进行查找,复制scankey并填写每个调用字段。*/
memcpy(cur_skey, cache->cc_skey, sizeof(ScanKeyData) * cache->cc_nkeys);
cur_skey[0].sk_argument = v1;
cur_skey[1].sk_argument = v2;
cur_skey[2].sk_argument = v3;
cur_skey[3].sk_argument = v4;
relation = table_open(cache->cc_reloid, AccessShareLock);
scandesc = systable_beginscan(relation,
cache->cc_indexoid,
IndexScanOK(cache, cur_skey),
NULL,
nkeys,
cur_skey);
/* 如果我们正在做索引扫描,列表将被排序 */
ordered = (scandesc->irel != NULL);
while (HeapTupleIsValid(ntp = systable_getnext(scandesc)))
{
uint32 hashValue;
Index hashIndex;
bool found = false;
dlist_head *bucket;
/*
* 看看这个元组是否已经有条目了。
*/
ct = NULL;
hashValue = CatalogCacheComputeTupleHashValue(cache, cache->cc_nkeys, ntp);
hashIndex = HASH_INDEX(hashValue, cache->cc_nbuckets);
bucket = &cache->cc_bucket[hashIndex];
dlist_foreach(iter, bucket)
{
ct = dlist_container(CatCTup, cache_elem, iter.cur);
if (ct->dead || ct->negative)
continue; /* 忽略死亡的和负的元组 */
if (ct->hash_value != hashValue)
continue; /* 哈希值错误则快速跳过 */
if (!ItemPointerEquals(&(ct->tuple.t_self), &(ntp->t_self)))
continue; //如果键值不匹配则跳过
/* 找到一个匹配,但不能使用它,如果它已经属于另一个列表 */
if (ct->c_list)
continue;
found = true;
break; /* A-OK */
}
if (!found)
{
/* 我们找不到可用的条目,所以重新做一个 */
ct = CatalogCacheCreateEntry(cache, ntp, arguments,
hashValue, hashIndex,
false);
}
/* 注意:添加条目到ctlist,然后增加它的引用计数 */
/* 如果lappend耗尽内存,这种方法将使状态保持正确 */
ctlist = lappend(ctlist, ct);
ct->refcount++;
}
systable_endscan(scandesc);
table_close(relation, AccessShareLock);
/* 现在我们可以构建CatCList条目。 */
oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
nmembers = list_length(ctlist);
cl = (CatCList *)
palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *));
/* 获取键值 */
CatCacheCopyKeys(cache->cc_tupdesc, nkeys, cache->cc_keyno,
arguments, cl->keys);
MemoryContextSwitchTo(oldcxt);
/*
* 在我们完成CatCList的构建并将其记住在资源所有者中之前,我们现在已经经历了可能触
* 发elog日志管理系统的最后一件事。因此,退出PG_TRY是可以的,实际上,我们最好在开
* 始将成员标记为属于该列表之前这样做。
*/
}
PG_CATCH();
{
foreach(ctlist_item, ctlist)
{
ct = (CatCTup *) lfirst(ctlist_item);
Assert(ct->c_list == NULL);
Assert(ct->refcount > 0);
ct->refcount--;
if (
#ifndef CATCACHE_FORCE_RELEASE
ct->dead &&
#endif
ct->refcount == 0 &&
(ct->c_list == NULL || ct->c_list->refcount == 0))
CatCacheRemoveCTup(cache, ct);
}
PG_RE_THROW();
}
PG_END_TRY();
cl->cl_magic = CL_MAGIC;
cl->my_cache = cache;
cl->refcount = 0; /* 目前 */
cl->dead = false;
cl->ordered = ordered;
cl->nkeys = nkeys;
cl->hash_value = lHashValue;
cl->n_members = nmembers;
i = 0;
foreach(ctlist_item, ctlist)
{
cl->members[i++] = ct = (CatCTup *) lfirst(ctlist_item);
Assert(ct->c_list == NULL);
ct->c_list = cl;
/* 释放成员上的临时引用 */
Assert(ct->refcount > 0);
ct->refcount--;
/* 如果有成员已经死亡,请标记死亡名单 */
if (ct->dead)
cl->dead = true;
}
Assert(i == nmembers);
dlist_push_head(&cache->cc_lists, &cl->cache_elem);
/* 最后,增加列表的参考数并返回它 */
cl->refcount++;
ResourceOwnerRememberCatCacheListRef(CurrentResourceOwner, cl);
CACHE_elog(DEBUG2, "SearchCatCacheList(%s): made list of %d members",
cache->cc_relname, nmembers);
return cl;
}
在SearchCatCacheList途中,也调用了CatalogCacheInitializeCache加载系统表,
调用CatalogCacheComputeHashValue计算给定键的哈希值,
调用CatalogCacheCompareTuple,比较元组和传递的参数,
调用IndexScanOK,扫描物理系统,确认要查找的元组确实不存在,还是没有换存在CatCache中,
调用CatalogCacheCreateEntry,当没有可用的条目时,重新生成一个,
调用RehashCatCache,扩大桶的容量,
调用CatCacheCopyKeys获取键值,
调用CatalogCacheComputeTupleHashValue计算元组哈希值和与要缓存的给定元组关联的哈希值。
然后调用了CatCacheRemoveCTup
/*
* CatCacheRemoveCTup
*
* 解除链接并删除给定的缓存条目
*
*
* NB: 如果它是一个CatCList的成员,该CatCList也被删除。
* 缓存条目和列表最好都没有引用计数。
*/
static void
CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
{
Assert(ct->refcount == 0);
Assert(ct->my_cache == cache);
if (ct->c_list)
{
/*
* 处理这个问题的最干净的方法是调用CatCacheRemoveCList,它将递归回给我,递归调用
* 将完成工作。设置“死”标志以确保它进行递归。
*/
ct->dead = true;
CatCacheRemoveCList(cache, ct->c_list);
return; /* 无事可做 */
}
/* 从链表中断开链接 */
dlist_delete(&ct->cache_elem);
/*
* 当我们处理一个负的条目时,自由键,普通的条目只是指向元组,和catcup一起分配。
*/
if (ct->negative)
CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
cache->cc_keyno, ct->keys);
pfree(ct);
--cache->cc_ntup;
--CacheHdr->ch_ntup;
}
当ct为负时,调用了CatCacheFreeKeys
/*
* 释放存储在键数组中的键的帮助程序。
*/
static void
CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys)
{
int i;
for (i = 0; i < nkeys; i++)
{
int attnum = attnos[i];
Form_pg_attribute att;
/* 在缓存中不支持系统属性 */
Assert(attnum > 0);
att = TupleDescAttr(tupdesc, attnum - 1);
if (!att->attbyval)
pfree(DatumGetPointer(keys[i]));
}
}
调用者不能修改列表对象或被指向的元组,并且必须在处理列表时调用ReleaseCatCacheList()。
/*
* ReleaseCatCacheList
*
* 减少catcache列表的引用计数。
*/
void
ReleaseCatCacheList(CatCList *list)
{
/* 安全检查,确保我们拿到了一个缓存入口 */
Assert(list->cl_magic == CL_MAGIC);
Assert(list->refcount > 0);
list->refcount--;
ResourceOwnerForgetCatCacheListRef(CurrentResourceOwner, list);
if (
#ifndef CATCACHE_FORCE_RELEASE
list->dead &&
#endif
list->refcount == 0)
CatCacheRemoveCList(list->my_cache, list);
}
最后调用CatCacheRemoveCList函数
/*
* CatCacheRemoveCList
* 解除链接并删除给定的缓存列表条目
* 注意:任何失效的成员条目也会被删除。
*/
static void
CatCacheRemoveCList(CatCache *cache, CatCList *cl)
{
int i;
Assert(cl->refcount == 0);
Assert(cl->my_cache == cache);
/* 从成员元组中脱钩 */
for (i = cl->n_members; --i >= 0;)
{
CatCTup *ct = cl->members[i];
Assert(ct->c_list == cl);
ct->c_list = NULL;
/* 如果该成员已死且现在没有引用,则删除它 */
if (
#ifndef CATCACHE_FORCE_RELEASE
ct->dead &&
#endif
ct->refcount == 0)
CatCacheRemoveCTup(cache, ct);
}
/* 从链表中断开链接 */
dlist_delete(&cl->cache_elem);
/* 自由关联列数据 */
CatCacheFreeKeys(cache->cc_tupdesc, cl->nkeys,
cache->cc_keyno, cl->keys);
pfree(cl);
}
部分内容来自:
《PostgreSQL 数据库内核分析》彭智勇、彭煜玮 著
PG高速缓冲区03
内存管理(3)