结构
typedef struct dict {
dictType *type; //dict类型,内置不同hash函数
void *privdata; //私有数据,在做特殊hash运算时用
dictht ht[2]; //一个dict包含两个哈希表,另一个hash表在rehash时用
long rehashidx; /* rehashing not in progress if rehashidx == -1 */ //rehash进度索引,
每进行一次rehash都会更新
//rehash是否暂停
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
typedef struct dictType {
// 哈希函数,用于计算键的哈希值
uint64_t (*hashFunction)(const void *key);
// 复制键的函数,用于复制键的值
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数,用于复制值的内容
void *(*valDup)(void *privdata, const void *obj);
// 比较键的函数,用于比较两个键的值是否相等
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 键的析构函数,用于释放键的内存
void (*keyDestructor)(void *privdata, void *key);
// 值的析构函数,用于释放值的内存
void (*valDestructor)(void *privdata, void *obj);
// 扩容允许性检查函数,用于判断是否允许进行扩容
int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;
typedef struct dictEntry {
void *key; //键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; //值
//下一个Entry指针
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
//entry数组,数组中保存的是指向entry的指针
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表掩码,总等于size - 1 所以h & sizemask即可运算索引位置
unsigned long sizemask;
//used为哈希表中键值对总和,每做一次add操作used就会加1
unsigned long used;
} dictht;
哈希碰撞时结构图,头插法,新entry插到链表头,next指针指向旧的entry
总结构图
扩容
int _dictExpand(dict *d, unsigned long size, int* malloc_failed) {
// 如果 malloc_failed 不为 NULL,则初始化为 0
if (malloc_failed) *malloc_failed = 0;
/* 如果正在进行 rehash 或者新的大小小于已使用元素的数量,返回错误 */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* 新的哈希表 */
unsigned long realsize = _dictNextPower(size); //这里会获取新的大小
/* 检测溢出 */
if (realsize < size || realsize * sizeof(dictEntry*) < realsize)
return DICT_ERR;
/* 如果新的大小等于当前哈希表的大小,返回错误。收缩时可能发生 */
if (realsize == d->ht[0].size) return DICT_ERR;
/* 分配新的哈希表并将所有指针初始化为 NULL */
n.size = realsize;
n.sizemask = realsize - 1;
if (malloc_failed) {
// 如果 malloc_failed 不为 NULL,使用 ztrycalloc 分配内存
n.table = ztrycalloc(realsize * sizeof(dictEntry*));
*malloc_failed = n.table == NULL;
if (*malloc_failed)
return DICT_ERR;
} else {
// 否则,使用 zcalloc 分配内存
n.table = zcalloc(realsize * sizeof(dictEntry*));
}
n.used = 0;
/* 如果是第一次初始化,只是设置第一个哈希表以便接受键,不算真正的 rehashing */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* 准备第二个哈希表用于增量式 rehashing ,准备rehash*/
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
_dictNextPower函数用于找到大于等于size的最小的 2 的幂。这是为了确保哈希表的大小始终是 2 的幂,这有助于提高哈希函数的性能,因为取模运算可以通过位运算进行优化。- 如果
size已经是 2 的幂,那么realsize的值就等于size,不进行额外的变化。 - 如果
size不是 2 的幂,那么realsize的值就会是大于等于size的最小的 2 的幂。
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE;
if (size >= LONG_MAX) return LONG_MAX + 1LU;
while(1) {
if (i >= size)
return i;
i *= 2; //永远是2的n次
}
}
加入一个键值对时发生了什么
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL); //返回的entry有key无value
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val); //entry返回后再设置值
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
long index;
dictEntry *entry;
dictht *ht;
// 如果正在进行 rehash 操作,则执行一步 rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
// 获取键的索引,如果键已经存在,则返回 -1,存在的情况下可以通过 existing 获取已存在的节点
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
// 获取当前哈希表,如果正在rehash就获取新表
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
// 分配内存并存储新的 entry
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++; //每加入一个元素used就会加1
// 设置 hash entry 的字段
dictSetKey(d, entry, key);
// 返回新添加的 entry
return entry;
}
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; /* 最大访问空桶次数。这里是10*/
if (!dictIsRehashing(d)) return 0; //如果rehash结束了
while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* 确保 rehashidx 不会溢出,因为还有更多元素要重新哈希。*/
assert(d->ht[0].size > (unsigned long)d->rehashidx);
/* 查找下一个非空桶以处理。*/
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1; /* 达到最大空桶访问次数,返回以稍后继续。 */
}
//拿到桶
de = d->ht[0].table[d->rehashidx];
/* 将该桶中的所有键从旧哈希表移动到新哈希表。*/
while (de) {
uint64_t h;
nextde = de->next;
/* 在新哈希表桶的头部插入键。*/
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde; /* 移动到旧哈希表桶中的下一个条目。 */
}
d->ht[0].table[d->rehashidx] = NULL; /* 在旧哈希表中标记该桶为空。*/
d->rehashidx++;
}
/* 检查是否已经完成整个哈希表的 rehash... */
if (d->ht[0].used == 0) {
/* 释放旧哈希表,用新哈希表替换,并重置 rehash 状态。 */
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0; /* Rehash 操作已完成。*/
}
/* 还有更多要 rehash... */
return 1; /* 仍有更多工作要做。 */
}
添加一个元素时会检查是否在rehash,如果在rehash会进行一次rehash,如果连续十次访问到的都是空桶会停止rehash。在rehash中会重新对一个桶中的所有元素进行重新散列,直到清空旧表中此旧桶的元素。每次操作新旧桶都会做一次used++或--的操作,rehash结束后会检查是否全部rehash完毕,如果已经全部完毕会更改rehash状态,并释放旧表内存。
如果没有在rehash,首先检查键是否已经存在,然后创建一个只有键的entry,包装返回后插入value。
如果有一个重复的Key插入,并且插入时发现正在rehash,于是键值对插入在新表中。当旧表中重复的key要迁移到新表,迁移时并没有做检查逻辑,会不会造成数据重复呢?
一番阅读后找到了这块代码,在判断Key是否存在时,如果正在rehash,新表旧表都会判断,所以不会出现数据重复的情况。
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;
/* Expand the hash table if needed */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
for (table = 0; table <= 1; table++) {
idx = hash & d->ht[table].sizemask;
/* Search if this slot does not already contain the given key */
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
if (existing) *existing = he;
return -1;
}
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return idx;
}
删除一个键值对时发生了什么
/* Remove an element, returning DICT_OK on success or DICT_ERR if the
* element was not found. */
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
/* 从字典中删除键为 key 的元素,如果 nofree 为 0,则同时释放键和值的内存。*/
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
/* 如果哈希表为空,直接返回。*/
if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
/* 如果正在进行 rehash 操作,则执行一步 rehash。*/
if (dictIsRehashing(d)) _dictRehashStep(d);
/* 计算键的哈希值。*/
h = dictHashKey(d, key);
/* 遍历两个哈希表,查找并删除元素。*/
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask; /* 计算索引位置。*/
he = d->ht[table].table[idx]; /* 获取链表头。*/
prevHe = NULL;
while (he) {
/* 如果找到匹配的键,则删除元素。*/
if (key == he->key || dictCompareKeys(d, key, he->key)) {
/* 从链表中解除该元素的链接。*/
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
/* 如果需要释放内存,则同时释放键和值的内存。*/
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
//更新used
d->ht[table].used--;
/* 返回被删除的元素。*/
return he;
}
prevHe = he;
he = he->next;
}
/* 如果不是正在进行 rehash 操作,则只遍历第一个哈希表。*/
if (!dictIsRehashing(d))
break;
}
return NULL; /* 未找到匹配的元素。*/
}
所以增加或移除一个元素都会检查是否在rehash,而rehash也许在扩容也许在收缩,如果在收缩会释放掉该释放的内存