四个数据结构
dictEntry
dictEntry 的结构如下(Redis 7.0):
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
void *metadata[];
} dictEntry;
可以对比 《Redis 设计与实现》中的 dictEntry 结构,发现联合结构 v 中多了一个 double 的浮点数表示,metadata 是一块任意长度的数据,具体的长度由 dictType 中的 dictEntryMetadataBytes() 返回,作用相当于 privdata
dictType
dictType 是一系列操作字典的键和值的操作:
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(dict *d, const void *key);
void *(*valDup)(dict *d, const void *obj);
int (*keyCompare)(dict *d, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
void (*valDestructor)(dict *d, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
该结构是为了实现字典的多态。
dict
7.0 版本的字典结构如下:
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx;
int16_t pauserehash;
signed char ht_size_exp[2];
};
相比于 《Redis 设计与实现》中的字典实现,改动较大,7.0中去掉了 dictht 结构,即去掉了哈希结构。接下来介绍每个成员:
我们可以看到一行注释:/* Keep small vars at end for optimal (minimal) struct padding */ ,将小变量放在结构体的后面,为了最佳或最小的填充,即节省空间。
dictIterator
dictIterator 是字典的迭代器
typedef struct dictIterator {
dict *d;
long index;
int table, safe;
dictEntry *entry, *nextEntry;
unsigned long long fingerprint;
} dictIterator;
解释其成员:
常量与一系列宏
首先是关于哈希表初始化的两个常量:
#define DICT_HT_INITIAL_EXP 2
#define DICT_HT_INITIAL_SIZE (1<<(DICT_HT_INITIAL_EXP))
很明显,哈希表的初始大小为 4
dictFreeVal
释放 val
#define dictFreeVal(d, entry) \
if ((d)->type->valDestructor) \
(d)->type->valDestructor((d), (entry)->v.val)
调用 dictType 中提供的 valDestructor 函数释放 val
dictSetVal
设置 val 的值
#define dictSetVal(d, entry, _val_) do { \
if ((d)->type->valDup) \
(entry)->v.val = (d)->type->valDup((d), _val_); \
else \
(entry)->v.val = (_val_); \
} while(0)
如果 dictType 提供了设置 val 值的方法则调用,没有则直接赋值
dictSetSignedIntegerVal
设置有符号的整型 val 值
#define dictSetSignedIntegerVal(entry, _val_) \
do { (entry)->v.s64 = _val_; } while(0)
当然还有设置 无符号的整型值、double 值
dictFreeKey
释放 key
#define dictFreeKey(d, entry) \
if ((d)->type->keyDestructor) \
(d)->type->keyDestructor((d), (entry)->key)
dictSetKey
设置 key 的值
#define dictSetKey(d, entry, _key_) do { \
if ((d)->type->keyDup) \
(entry)->key = (d)->type->keyDup((d), _key_); \
else \
(entry)->key = (_key_); \
} while(0)
dictCompareKeys
key 的比较
#define dictCompareKeys(d, key1, key2) \
(((d)->type->keyCompare) ? \
(d)->type->keyCompare((d), key1, key2) : \
(key1) == (key2))
如果 dictType 中的 keyCompare 指针不为空,则调用它进行比较,否则直接用 == 比较
dictMetadata
获取用户提供的元数据
#define dictMetadata(entry) (&(entry)->metadata)
dictMetadataSize
获取用户提供的元数据的长度
#define dictMetadataSize(d) ((d)->type->dictEntryMetadataBytes \
? (d)->type->dictEntryMetadataBytes(d) : 0)
获取 key 和 val 的宏
#define dictHashKey(d, key) (d)->type->hashFunction(key)
#define dictGetKey(he) ((he)->key)
#define dictGetVal(he) ((he)->v.val)
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
#define dictGetDoubleVal(he) ((he)->v.d)
关于哈希表大小的宏
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
dictSlots
#define dictSlots(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))
dictSize
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])
关于 rehash 的宏
#define dictIsRehashing(d) ((d)->rehashidx != -1)
#define dictPauseRehashing(d) (d)->pauserehash++
#define dictResumeRehashing(d) (d)->pauserehash--
创建/销毁/修改字典
创建字典
创建字典的接口是 dictCreate
dictCreate 为字典结构分配空间,并调用 _dictInit 进行初始化
在 _dictinit 中,又调用了 _dicrReset 进行成员设置
dict *dictCreate(dictType *type)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type);
return d;
}
int _dictInit(dict *d, dictType *type)
{
_dictReset(d, 0);
_dictReset(d, 1);
d->type = type;
d->rehashidx = -1;
d->pauserehash = 0;
return DICT_OK;
}
static void _dictReset(dict *d, int htidx)
{
d->ht_table[htidx] = NULL;
d->ht_size_exp[htidx] = -1;
d->ht_used[htidx] = 0;
}
在 _dictInit 中有一个宏 DICT_OK,它的定义为:
#define DICT_OK 0
#define DICT_ERR 1
用来标识操作是否成功完成
修改字典
一般修改字典都是为 rehash 做准备,或者扩大容量或者缩小容量。
dictResize
缩小字典大小,前提是当前没有在进行 rehash 以及 dict_can_resize 变量不为 0
dict_can_resize 是 dict.c 中定义的一个全局变量,用来指示 resize 能否进行
static int dict_can_resize = 1;
什么时候 dict_can_resize 会为0呢?
当 redis 在后台进行持久化时, 为了最大化地利用系统的 copy on write 机制,会暂时地将 dict_can_resize 设置为0,避免执行自然 rehash,从而减少程序对内存的碰撞。持久化任务完成后,dict_can_resize 又会设置为1
int dictResize(dict *d)
{
unsigned long minimal;
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht_used[0];
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
缩小哈希表的大小,新的容量刚好能容纳已有元素。minimal 等于已有元素的数量,然后在 _dictExpand 中,会将新容量的大小设置为刚好大于等于 minimal 的 2 的幂。
dictExpand
再来看 dictExpand,调用 _dictExpand 进行实际的扩容。
int dictExpand(dict *d, unsigned long size) {
return _dictExpand(d, size, NULL);
}
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
if (malloc_failed) *malloc_failed = 0;
if (dictIsRehashing(d) || d->ht_used[0] > size)
return DICT_ERR;
dictEntry **new_ht_table;
unsigned long new_ht_used;
signed char new_ht_size_exp = _dictNextExp(size);
size_t newsize = 1ul<<new_ht_size_exp;
if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
return DICT_ERR;
if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;
if (malloc_failed) {
new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
*malloc_failed = new_ht_table == NULL;
if (*malloc_failed)
return DICT_ERR;
} else
new_ht_table = zcalloc(newsize*sizeof(dictEntry*));
new_ht_used = 0;
if (d->ht_table[0] == NULL) {
d->ht_size_exp[0] = new_ht_size_exp;
d->ht_used[0] = new_ht_used;
d->ht_table[0] = new_ht_table;
return DICT_OK;
}
d->ht_size_exp[1] = new_ht_size_exp;
d->ht_used[1] = new_ht_used;
d->ht_table[1] = new_ht_table;
d->rehashidx = 0;
return DICT_OK;
}
_dictExpand 函数不是只为了 rehash,还可以初始化字典。 _dictExpand 函数判断是否是 rehash 是通过判断 ht_table[0] 是否为空来判断的。也就是说如果调用 _dictExpand 的字典是非空的,则增容后的哈希表是放在 ht_table[1] 中的,所以需要调用者手动释放 ht_table[0],将 ht_table[1] 放到 ht_table[0] 位置上。
Rehash
rehash 扩容分为两种:
- 自然 rehash:used / size >= 1 时且
dict_can_resize 为1 - 强制 rehash:used / size >
dict_force_resize_ratio 该版本中 dict_force_resize_ratio 的值为5
另外,当哈希表的负载因子小于 0.1 时,程序自动开始对哈希表执行收缩操作。
Rehash 过程:创建一个新的哈希表 ht[1],rehashidx 变成0,根据 rehashidx 指向的桶进行数据的迁移。当所有数据迁移完毕时,释放 ht[0],将 ht[1] 迁移到 ht[0],ht[1] 置为空。
下面是 Rehash 的一个简单示例:
Rehash 之前:
Rehash 开始:
所有元素重新映射到 ht_table[1] :
表的转移,完成 Rehash:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOmONTWE-1652675352252)(C:\Users\晏思俊\AppData\Roaming\Typora\typora-user-images\image-20220516122546788.png)]
Rehash 不是一步就完成的,否则数据量特别大时,迁移数据会是一个很大的工程,可能会导致服务器暂停服务来迁移数据,Rehash 是渐进式的,数据的迁移发生在对数据的增删改查时,这样就将迁移平摊在每个增删改查的操作上。
如果在 rehash 期间要执行添加键的操作,都是在 ht[1] 中进行的,而进行删改查等操作,会同时在两张表中进行。
每次 rehash 都是以 n 个桶为单位的,将每个桶上的链都移到新的哈希表上。一次 rehash 完成以后,如果之后还有桶要 rehash,则返回1,如果 rehash 完成,则返回0。但是实际上,rehashidx 指向的桶可能是空桶,所以为了效率,一次 rehash 最多要遍历 10*n 个空桶,遍历完了 10 * n 个空桶就会返回。
int dictRehash(dict *d, int n) {
int empty_visits = n*10;
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht_used[0] != 0) {
dictEntry *de, *nextde;
assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht_table[0][d->rehashidx];
while(de) {
uint64_t h;
nextde = de->next;
h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
d->ht_used[0]--;
d->ht_used[1]++;
de = nextde;
}
d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;
}
if (d->ht_used[0] == 0) {
zfree(d->ht_table[0]);
d->ht_table[0] = d->ht_table[1];
d->ht_used[0] = d->ht_used[1];
d->ht_size_exp[0] = d->ht_size_exp[1];
_dictReset(d, 1);
d->rehashidx = -1;
return 0;
}
return 1;
}
销毁字典
销毁字典的接口是dictRelease
dictRelease 调用_dictClear 来释放两个哈希表,并最后释放字典空间
int _dictClear(dict *d, int htidx, void(callback)(dict*)) {
unsigned long i;
for (i = 0; i < DICTHT_SIZE(d->ht_size_exp[htidx]) && d->ht_used[htidx] > 0; i++) {
dictEntry *he, *nextHe;
if (callback && (i & 65535) == 0) callback(d);
if ((he = d->ht_table[htidx][i]) == NULL) continue;
while(he) {
nextHe = he->next;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
d->ht_used[htidx]--;
he = nextHe;
}
}
zfree(d->ht_table[htidx]);
_dictReset(d, htidx);
return DICT_OK;
}
void dictRelease(dict *d)
{
_dictClear(d,0,NULL);
_dictClear(d,1,NULL);
zfree(d);
}
_dictClear 的第三个参数是一个函数指针,但 dictRelease 在这里都传了空指针,所以暂且不管。 _dictClear 遍历所有的索引,当该索引上有键值对时,则释放该索引上的整条链表。释放完所有键值对之后,再释放该哈希表并重置其成员。
键的操作
dictAdd
添加一个键值对到指定哈希表中
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
该函数调用了 dictAddRaw 函数,下面是 dictAddRaw 函数的定义:
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
int htidx;
if (dictIsRehashing(d)) _dictRehashStep(d);
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
htidx = dictIsRehashing(d) ? 1 : 0;
size_t metasize = dictMetadataSize(d);
entry = zmalloc(sizeof(*entry) + metasize);
if (metasize > 0) {
memset(dictMetadata(entry), 0, metasize);
}
entry->next = d->ht_table[htidx][index];
d->ht_table[htidx][index] = entry;
d->ht_used[htidx]++;
dictSetKey(d, entry, key);
return entry;
}
该函数上面有一串注释,意思是:该函数添加一个 dictEntry 到哈希表中,但并不会设置值,将该结构返回给用户,将值的设置交给用户。 这个函数也直接暴露给要调用的用户API,主要是为了在哈希值内部存储非指针。
返回值:如果键已经存在,则返回 NULL,并填充“*existing” 如果 existing 不为 NULL,则使用现有条目。如果成功添加了键,则返回哈希条目(dictEntry)以供调用者操作
我们分析函数的一个实现细节:可以看到函数首先会判断当前的哈希表是否在 rehash,如果在 rehash,则进行一次只迁移一个桶的 rehash。_dictRehashStep 是完成一次只迁移一个桶的 rehash。
所以现在我们就知道。dictAdd 调用 dictAddRaw,获取一个已插入的 dictEntry,然后完成值的设置。
在该函数中还调用了 _dictKeyIndex 函数,该函数实现如下:
函数返回 key 在哈希表中的索引,如果哈希表中存在该 key,则返回 -1,可选的输出参数 existing 会被填充成对应的 dictEntry。
需要注意的是,如果当前哈希表正在 rehash,则返回的索引始终是第二张哈希表的索引。
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
for (table = 0; table <= 1; table++) {
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
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;
}
dictReplace
如果 key 不存在,则添加 key,并设置 value,函数返回1。如果 key 已经存在,则更新 value 值,函数返回0
int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, *existing, auxentry;
entry = dictAddRaw(d,key,&existing);
if (entry) {
dictSetVal(d, entry, val);
return 1;
}
auxentry = *existing;
dictSetVal(d, existing, val);
dictFreeVal(d, &auxentry);
return 0;
}
首先调用 dictAddRaw 函数,根据返回值判断 key 是否已经在哈希表中,如果不存在,则 entry 不为空,设置 val 值并返回1。如果 entry 为空,则进行值的更新。但要遵循先设置新值再释放旧值的顺序。因为新值和旧值可能相同,这种情况就需要考虑引用计数,我们应该先增加计数,再减少计数。
dictAddOrFind
该函数总是返回一个 指定key 的 dictEntry。如果 key 已经存在,则将其 dictEntry 返回;如果不存在,则将其添加并返回。
dictEntry *dictAddOrFind(dict *d, void *key) {
dictEntry *entry, *existing;
entry = dictAddRaw(d,key,&existing);
return entry ? entry : existing;
}
dictDelete
删除元素
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
dictDelete 通过 dictGenericDelete 来删除哈希表中的元素。
如果没找到要删除的元素则返回 NULL,找到了则返回已经释放的 dictEntry。
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
if (dictSize(d) == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
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) {
dictFreeUnlinkedEntry(d, he);
}
d->ht_used[table]--;
return he;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return NULL;
}
dictUnlink 与 dictFreeUnlinkedEntry
dictUnlink 函数是从哈希表中删除 key,但 key,value 和 dictEntry 并没有被释放,需要调用 dictFreeUnlinkedEntry 函数来释放这些资源。如果 key 在哈希表中找到了,则返回对应的 dictEntry,如果没找到则返回 NULL
dictEntry *dictUnlink(dict *d, const void *key) {
return dictGenericDelete(d,key,1);
}
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
if (he == NULL) return;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
dictFind
查找 key,找到了返回其 dictEntry;否则返回 NULL
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
he = d->ht_table[table][idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
|