列存储(hash_search)
- 概述
- hash_search 函数
- hash_search_with_hash_value 函数
- calc_bucket 函数
- get_hash_entry 函数
- 补充知识
声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档。
概述
在对源码的不断学习中,经常可以看到一个名为 hash_search 的函数,该函数用于在哈希表中查找键并执行相应操作。先前一直没有仔细看过该函数的内部逻辑,本文则来详细的学习一下吧。
hash_search 函数
hash_search 函数是用于在哈希表中进行通用的查找、插入和删除操作的实现。通过调用 hash_search_with_hash_value 函数,该函数提供了对哈希表中元素的查找、插入和删除的支持,具体操作由传入的 action 参数决定。action 参数可以取值为 HASH_FIND(查找)、HASH_ENTER(插入)、HASH_ENTER_NULL(插入,返回 NULL 如果内存不足)和 HASH_REMOVE(删除)。函数返回 找到/插入/删除 的元素的指针,或者如果没有找到匹配项则返回 NULL。此函数通过提供通用的哈希表操作,实现了对哈希表中数据的灵活管理,支持不同应用场景的需求。函数源码如下所示:(路径:src/common/backend/utils/hash/dynahash.cpp
)
/** hash_search -- 在哈希表中查找键并执行相应操作* hash_search_with_hash_value -- 在哈希表中查找键并执行相应操作,提供预先计算的哈希值** action 的取值包括:* HASH_FIND: 在表中查找键* HASH_ENTER: 在表中查找键,如果不存在则创建新条目* HASH_ENTER_NULL: 在表中查找键,如果不存在则创建新条目,如果内存不足则返回 NULL* HASH_REMOVE: 在表中查找键,如果存在则删除条目** 返回值是找到/插入/删除的元素的指针,如果没有找到匹配项则返回 NULL。* (注意:在 REMOVE 操作中,结果是一个悬空指针,不应该被解引用!)** 如果 foundPtr 不为 NULL,则 *foundPtr 被设置为 TRUE,表示在表中找到现有条目,为 FALSE 则表示没有找到。* 这对于 HASH_ENTER 操作是必需的,但在其他情况下与返回值重复。** 对于 hash_search_with_hash_value,hashvalue 参数必须先使用 get_hash_value() 计算。*/
void* hash_search(HTAB* hashp, const void* keyPtr, HASHACTION action, bool* foundPtr)
{// 调用 hash_search_with_hash_value 函数,传递哈希值参数为使用哈希函数计算得到的哈希值return hash_search_with_hash_value(hashp, keyPtr, hashp->hash(keyPtr, hashp->keysize), action, foundPtr);
}
函数 hash_search 的入参如下:
- HTAB* hashp:指向哈希表的指针,是哈希表的控制结构。
- const void* keyPtr:指向要查找、插入或删除的键值的指针。
- HASHACTION action:指定要执行的操作,可以取以下值:
- HASH_FIND:查找键值。
- HASH_ENTER:查找键值,如果不存在则创建新条目。
- HASH_ENTER_NULL:查找键值,如果不存在则创建新条目,如果内存不足则返回 NULL。
- HASH_REMOVE:查找键值,如果存在则删除条目。
- bool* foundPtr:指向一个布尔型变量的指针,用于接收是否找到现有条目的信息。如果不关心此信息,可以将其设置为 NULL。
hash_search_with_hash_value 函数
hash_search_with_hash_value 函数是一个通用的哈希表查找、插入和删除元素的核心函数,根据给定的哈希值和键值,它在哈希表中进行查找,如果元素存在则返回指向元素的指针,如果不存在则根据操作类型进行相应的处理。支持的操作类型包括查找(HASH_FIND)、插入(HASH_ENTER)、插入并返回 NULL(HASH_ENTER_NULL)和删除(HASH_REMOVE)。函数内部通过哈希值定位到对应的哈希桶,再在哈希桶的冲突链上进行查找或执行插入和删除操作。在插入时,如果哈希表满了,会进行自动扩展。函数同时考虑哈希表的分区模式、冻结状态和哈希表顺序扫描的情况,确保操作的正确性和高效性。函数源码如下所示:(路径:src/common/backend/utils/hash/dynahash.cpp
)
/** 根据哈希值进行哈希表的查找、插入和删除操作,支持不同的操作和返回结果。** 入参:* - hashp: 指向哈希表的指针,包含哈希表的属性和方法。* - keyPtr: 要查找、插入或删除的键值的指针。* - hashvalue: 已经计算好的键值的哈希值。* - action: 操作类型,可以是 HASH_FIND、HASH_ENTER、HASH_ENTER_NULL 或 HASH_REMOVE。* - foundPtr: 用于接收是否找到元素的指针,如果不关心,可以设置为 NULL。** 返回值:* - 查找操作(HASH_FIND)返回找到的元素的指针,如果未找到则返回 NULL。* - 插入操作(HASH_ENTER 或 HASH_ENTER_NULL)返回新插入的元素的指针,如果出错或内存不足则返回 NULL。* - 删除操作(HASH_REMOVE)返回被删除的元素的指针,如果未找到或出错则返回 NULL。** 注意:该函数对哈希表进行查找、插入和删除操作,同时考虑了哈希表的扩展和元素的内存管理。*/
void* hash_search_with_hash_value(HTAB* hashp, const void* keyPtr, uint32 hashvalue, HASHACTION action, bool* foundPtr)
{HASHHDR* hctl = hashp->hctl; // 哈希表的控制信息Size keysize; // 键值的大小uint32 bucket; // 哈希桶的编号long segment_num; // 哈希段的编号long segment_ndx; // 哈希段内的索引HASHSEGMENT segp; // 哈希段HASHBUCKET currBucket; // 当前哈希桶HASHBUCKET* prevBucketPtr = NULL; // 前一个哈希桶的指针HashCompareFunc match = NULL; // 键值比较函数int freelist_idx = FREELIST_IDX(hctl, hashvalue); // 自由链表的索引// 统计哈希表的访问次数
#if HASH_STATISTICShash_accesses++;hctl->accesses++;
#endif/** 如果是插入操作,检查是否需要拓展哈希表的桶数量。*/if (action == HASH_ENTER || action == HASH_ENTER_NULL) {/** 如果哈希表是分区模式、冻结状态、或者有活跃的哈希表顺序扫描,就不能进行拓展。* 此处顺序检查的次序是为了先检查成本较低的条件。*/if (!IS_PARTITIONED(hctl) && !hashp->frozen &&hctl->freeList[0].nentries / (long)(hctl->max_bucket + 1) >= hctl->ffactor && !has_seq_scans(hashp)) {(void)expand_table(hashp);}}/** 进行初始查找*/bucket = calc_bucket(hctl, hashvalue);segment_num = bucket >> hashp->sshift;segment_ndx = MOD(bucket, hashp->ssize);segp = hashp->dir[segment_num];if (segp == NULL) {hash_corrupted(hashp);}prevBucketPtr = &segp[segment_ndx];currBucket = *prevBucketPtr;/** 跟随冲突链查找匹配的键值*/match = hashp->match; // 保存一次键值比较函数的调用keysize = hashp->keysize; // 保存一次键值的大小while (currBucket != NULL) {if (currBucket->hashvalue == hashvalue && match(ELEMENTKEY(currBucket), keyPtr, keysize) == 0) {break;}prevBucketPtr = &(currBucket->link);currBucket = *prevBucketPtr;
#if HASH_STATISTICShash_collisions++;hctl->collisions++;
#endif}// 将是否找到元素的信息写入 foundPtrif (foundPtr != NULL) {*foundPtr = (bool)(currBucket != NULL);}// 根据不同的操作类型进行处理switch (action) {case HASH_FIND: {// 查找操作,返回找到的元素指针,未找到返回 NULLif (currBucket != NULL) {return (void*)ELEMENTKEY(currBucket);}return NULL;}case HASH_REMOVE: {// 删除操作,返回被删除的元素指针,未找到返回 NULLif (currBucket != NULL) {// 如果是分区模式,需要锁定以修改 nentries 和 freeListif (IS_PARTITIONED(hctl)) {SpinLockAcquire(&(hctl->freeList[freelist_idx].mutex));}Assert(hctl->freeList[freelist_idx].nentries > 0);hctl->freeList[freelist_idx].nentries--;// 从哈希桶链表中移除记录*prevBucketPtr = currBucket->link;// 将记录添加到哈希表的自由链表中currBucket->link = hctl->freeList[freelist_idx].freeList;hctl->freeList[freelist_idx].freeList = currBucket;// 如果是分区模式,释放锁if (IS_PARTITIONED(hctl)) {SpinLockRelease(&hctl->freeList[freelist_idx].mutex);}/** 注意:最好确保调用者在这个元素上同步访问,因为其他代码将会* 在下一次插入时重新使用它。*/return (void*)ELEMENTKEY(currBucket);}return NULL;}case HASH_ENTER_NULL: {// 插入并返回新插入的元素指针,如果失败返回 NULL// ENTER_NULL 无法与基于 palloc 的分配器一起使用Assert(hashp->alloc != DynaHashAlloc || hashp->alloc != DynaHashAllocNoExcept);/* FALL THRU */}case HASH_ENTER: {// 插入并返回新插入的元素指针,如果失败返回 NULL// 如果已经存在元素,则返回找到的元素的指针if (currBucket != NULL) {return (void*)ELEMENTKEY(currBucket);}// 如果是冻结状态,则不能插入if (hashp->frozen) {if (hashp->alloc == DynaHashAllocNoExcept) {write_stderr("cannot insert into frozen hashtable \"%s\"", hashp->tabname);return NULL;}ereport(ERROR,(errcode(ERRCODE_INVALID_OPERATION),errmsg("cannot insert into frozen hashtable \"%s\"", hashp->tabname)));}// 从哈希表的自由链表中获取一个新的哈希桶currBucket = get_hash_entry(hashp, freelist_idx);if (currBucket == NULL) {// 内存不足,返回 NULLif (action == HASH_ENTER_NULL) {return NULL;}// libcomm 永久线程不能使用 elogif (hashp->alloc == DynaHashAllocNoExcept || t_thrd.comm_cxt.LibcommThreadType != LIBCOMM_NONE) {return NULL;}// 报告通用错误消息if (hashp->isshared) {ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of shared memory")));} else {ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory")));}}// 将新元素链接到哈希桶链表中*prevBucketPtr = currBucket;currBucket->link = NULL;// 将键值复制到记录中currBucket->hashvalue = hashvalue;if (hashp->keycopy == memcpy) {errno_t errorno = EOK;errorno = memcpy_s(ELEMENTKEY(currBucket), keysize, keyPtr, keysize);securec_check(errorno, "\0", "\0");} else {hashp->keycopy(ELEMENTKEY(currBucket), keyPtr, keysize);}/** 调用者应该在返回后填充数据字段。* 不要在这里插入可能引发错误的代码,因为这样会导致记录不完整,* 从而损坏调用者的数据结构。*/return (void*)ELEMENTKEY(currBucket);}default:break;}// 未识别的操作类型,报错if (hashp->alloc == DynaHashAllocNoExcept) {write_stderr("unrecognized hash action code: %d", (int)action);} else {ereport(ERROR, (errcode(ERRCODE_INVALID_OPERATION), errmsg("unrecognized hash action code: %d", (int)action)));}return NULL; // 保持编译器静默
}
calc_bucket 函数
内联函数 calc_bucket 用于将给定的哈希值转换为哈希表中的桶号。通过使用哈希表控制结构中的 high_mask 和 low_mask 进行按位与运算,确保计算得到的桶号在哈希表的有效范围内。该函数旨在实现将哈希值映射到哈希表桶的过程,以便在哈希表中查找、插入或删除元素时能够有效地定位到对应的桶。函数源码如下所示:(路径:src/common/backend/utils/hash/dynahash.cpp
)
/* * 描述: 将哈希值转换为桶号的内联函数。* 参数: * @in hctl: 哈希表控制结构。* @in hash_val: 待转换为桶号的哈希值。* 返回值: uint32 - 计算得到的桶号。*/
static inline uint32 calc_bucket(HASHHDR* hctl, uint32 hash_val)
{uint32 bucket;// 使用 high_mask 获取初始桶号。bucket = hash_val & hctl->high_mask;// 如果初始桶号超过最大桶号,使用 low_mask 进行调整。if (bucket > hctl->max_bucket) {bucket = bucket & hctl->low_mask;}// 返回最终计算得到的桶号。return bucket;
}
get_hash_entry 函数
get_hash_entry 函数用于从哈希表的自由列表中获取一个新的哈希表元素。函数根据给定的自由列表索引和哈希表控制结构,尝试从自由列表中获取一个元素。如果自由列表中没有空闲元素,则根据哈希表的分区策略尝试从其他分区借用元素。如果所有尝试都失败,函数返回 NULL。如果成功获取到元素,函数会从自由列表中移除该元素,并增加相应的计数。如果是分区哈希表,函数在操作自由列表时会使用自旋锁确保线程安全。函数源码如下所示:(路径:src/common/backend/utils/hash/dynahash.cpp
)
/** 描述:如果可能,创建一个新的哈希表元素。* 参数:* @in hashp: 哈希表的控制结构。* @in freelist_idx: 自由列表的索引,用于确定从哪个自由列表中获取元素。* 返回值:HASHBUCKET - 新创建的哈希表元素,如果创建失败返回NULL。*/
static HASHBUCKET get_hash_entry(HTAB* hashp, int freelist_idx)
{HASHHDR* hctl = hashp->hctl; // 获取哈希表的控制结构HASHBUCKET newElement; // 新的哈希表元素指针int borrow_from_idx; // 用于在分区哈希表中从其他分区借用元素的自由列表索引for (;;) {// 如果使用分区哈希表,必须锁定以操作 nentries 和 freeListif (IS_PARTITIONED(hctl)) {SpinLockAcquire(&hctl->freeList[freelist_idx].mutex);}// 尝试从自由列表中获取一个元素newElement = hctl->freeList[freelist_idx].freeList;if (newElement != NULL) {break; // 成功获取元素,退出循环}// 如果使用分区哈希表,释放锁并尝试从其他分区借用元素if (IS_PARTITIONED(hctl)) {SpinLockRelease(&hctl->freeList[freelist_idx].mutex);}// 自由列表中没有空闲元素,分配一个新的元素块if (!element_alloc(hashp, hctl->nelem_alloc, freelist_idx)) {if (!IS_PARTITIONED(hctl)) {return NULL; // 内存不足,返回NULL}// 尝试从其他分区借用元素borrow_from_idx = freelist_idx;for (;;) {borrow_from_idx = (borrow_from_idx + 1) % NUM_FREELISTS;// 尝试获取其他分区的元素SpinLockAcquire(&(hctl->freeList[borrow_from_idx].mutex));newElement = hctl->freeList[borrow_from_idx].freeList;if (newElement != NULL) {// 成功从其他分区获取元素,更新自由列表和 nentrieshctl->freeList[borrow_from_idx].freeList = newElement->link;SpinLockRelease(&(hctl->freeList[borrow_from_idx].mutex));SpinLockAcquire(&hctl->freeList[freelist_idx].mutex);hctl->freeList[freelist_idx].nentries++;SpinLockRelease(&hctl->freeList[freelist_idx].mutex);break; // 成功获取元素,退出循环}SpinLockRelease(&(hctl->freeList[borrow_from_idx].mutex));}return newElement;}}// 从自由列表中移除元素,增加 nentrieshctl->freeList[freelist_idx].freeList = newElement->link;hctl->freeList[freelist_idx].nentries++;// 如果使用分区哈希表,释放锁if (IS_PARTITIONED(hctl)) {SpinLockRelease(&hctl->freeList[freelist_idx].mutex);}return newElement; // 返回新创建的哈希表元素指针
}
补充知识
- 哈希桶(Bucket):
定义: 哈希桶是哈希表中存储数据的地方,通常是一个数组。每个桶都有一个唯一的索引,通过哈希函数计算得到。在一个桶中,可以存储一个或多个数据元素,这取决于哈希冲突的情况。
作用: 哈希桶用于存储哈希表中的数据,通过索引快速定位数据。
- 哈希函数:
定义: 哈希函数是将关键字映射为整数值的函数。良好设计的哈希函数应该具备以下特性:
一致性: 对于相同的输入,哈希函数应始终产生相同的输出。
高效性: 哈希函数计算速度应该快。
离散性: 关键字的微小变化应导致哈希值的显著变化。
- 哈希冲突:
定义: 哈希冲突是指两个不同的关键字经过哈希函数映射后得到相同的哈希值。为了解决哈希冲突,通常有两种主要的方法:- 链地址法(Separate Chaining): 每个哈希桶都是一个链表,哈希表中的每个桶都存储指向链表头部的指针。当发生哈希冲突时,新的数据元素被插入到对应桶的链表中。
- 开放地址法(Open Addressing): 当发生哈希冲突时,通过一定的规则,寻找另一个可用的桶来存储冲突的数据。常见的开放地址法包括线性探测、二次探测等。
- 自由列表:
定义: 自由列表是一种数据结构,用于存储哈希表中的空闲元素。当需要插入新的数据元素时,从自由列表中获取一个空闲元素。在哈希表中,由于数据的插入和删除操作,会导致哈希桶中的元素数量动态变化,因此需要维护一个自由列表,用于存储未被使用的桶。