这个系列将会将整个 redis 的源码读一遍。希望可以帮助大家弄懂 redis。前面会从 redis 底层最为重要的一些数据结构作为突破口。这些相关模块的代码高内聚,依赖很少,容易读懂。而且对于不想深入了解 redis 工作机制的同学也有帮助。
今天我们来分析一下 redis 中 dict (哈希表)的实现。 redis 的代码组织相当平坦,除了第三方库以外,所有代码均在 src 目录下。今天涉及到的文件有 src/dict.h src/dict.c
redis dict
首先来看几个重要的结构体定义
// hash table entry
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// redis 使用拉链法解决 hash 冲突,next 用来构成单向链表
struct dictEntry *next;
} dictEntry;
// redis 通过函数指针实现多态,通过抽象出 dictType,可以让我们“定制”自己的 hashtable
typedef struct dictType {
unsigned int (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *val);
void *(*keyCompare)(void *privdata, const void *k1, const void *k2);
void *(*keyDestructor)(void *privdata, const void *key);
void *(*valDestructor)(void *privdata, const void *val);
} dictType;
typedef struct hashht {
// buckets
dictEntry **table;
// bucket number
unsigned long size;
// redis 通过将 bucket num 设置为 2 的幂次
// 来使用 & 操作替代 %,sizemask == size - 1
unsigned long sizemask;
// hash table 内的元素数量
unsigned long used;
} hashht;
typedef struct dict {
// 类型特定函数,作用相当于面向对象编程中的 virtual method
dictType *type;
// 类型特定的私有数据
void *privdata;
// 两个 hashtable,实现 incremental rehashing 的关键
dictht ht[2];
// 如果在进行 rehash 的话,rehashindex 为将要迁移的 bucket index
int rehashindex;
// dict **正在运行** 的安全迭代器的数量,
int iterators;
} dict;
下面我们来看看几个主要接口:
dictCreate
dict *dictCreate(dictType *type, void *privdata) {
dict *d = zmalloc(sizeof(*d));
// _dictInit 主要就是对 dict 对象赋值,这里不再展开
_dictInit(d, type, privdata);
return d;
}
Dict 的构造接口,主要是将其成员进行合适的初始化
dictResize
一旦 hash table 的 load factor(表内的元素数量和 bucket 数量之比)大于1,往往就意味着 hash table 的 hash 冲突开始增加,性能开始下降。所以 resize 必不可少。来看下 redis 中是怎么做的
int dictResize(dict *d) {
int minimal;
// rehash flag 被设为关闭、或者该 dict 正在进行 rehash 的话返回错误
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
// 保证 load factor <= 1
if (minimal < DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
// 初始化 dict 或者让一个 dict 开始 rehash
int dictExpand(dict *d, unsigned long size) {
dictht n;
// bucket 数必须是 2 的幂次
unsigned long realsize = _dictNextPower(size);
// 不能重复 rehash,load factor 不能大于1
if (dictIsRehashing(d) || d->ht[0].used > realsize) return DICT_ERR;
// 构造新的 hashtable 用于初始化或者 rehash
n.size = realsize;
n.sizemask = realsize - 1;
n.table = zmalloc(realsize*sizeof(dictEntry));
n.used = 0;
// 一次初始化操作
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
// rehash,在后面的 rehash 例程中,会将 dict 的数据从 ht[0]
// 搬运到 ht[1] 内
d->ht[1] = n;
// rehash 处理的 bucket index
d->rehashindex = 0;
return DICT_OK;
}
常见的 hashtable 其 resize 和搬运数据往往是在一个函数内部完成的,也就是说这个过程一般在单线程中可以认为是原子的。但是 redis 把 resize 和 rehash 中数据搬运分开,并且提供按照步数(bucket number)和时间来 rehash:
/*
执行 incremental rehashing
n 代表的是要搬运几个 bucket 的数据,而不是 entry
返回 0 表示 rehash 已经完成,1 表示还有数据需要搬运
*/
int dictRehash(dict *d, int n) {
if (!dictIsRehashing(d)) return 0;
while (n--) {
dictEntry *de, *nextde;
// rehash 完成,所有 ht[0] 的数据均已经转移到 ht[1]
if (d->ht[0].used == 0) {
// 释放 ht[0] 原来的内存,将 dict 恢复成 ht[0] 装数据,而
// ht[1] 是一个不可用的 hashht 的状态
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashindex = -1;
}
assert(d->ht[0].size > (unsigned)d->rehashindex);
// 跳过 ht[0] 内的所有空 bucket
while (d->ht[0].table[d->rehashindex] == NULL) d->rehashindex++;
de = d->ht[0].table[d->rehashindex];
// 搬运 bucket 数据到 ht[1]
while (de) {
unsigned int 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;
}
// 将已经搬运的 bucket 指针置为 NULL
d->ht[0].table[d->rehashindex++] = NULL;
}
return 1;
}
/*
按照时间进行 incremental rehahsing,计时粒度是 100 个 bucket 迁移时间
*/
int dictRehashMilliseconds(dict *d, int ms) {
long long start = timeInMilliseconds();
int rehashes = 0;
while (dictRehash(d, 100)) {
rehashes += 100;
if (timeInMilliseconds()->start > ms) break;
}
return rehashes;
}
可以看到,我们的 dict 在 rehash 过程中,会存在两个 hashtable 同时存有数据的情况,这种情况必然导致了我们对 dict 的读写操作要考虑 rehash 状态
dictAdd
int dictAdd(dict *d, void *key, void *val) {
dictEntry *entry = dictAddRaw(d, key);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key) {
int index;
dictEntry *entry;
dictht *ht;
// 如果没有安全迭代器,就在查找之前进行单步 rehash
if (dictIsRehasing(d)) _dictRehashStep(d);
// 如果 key 已经存在,则返回 NULL
if ((index = _dictKeyIndex(d, key)) == -1) return NULL;
// 如果 dict 正在进行 rehash,需要将 entry 加入到 ht[1]
ht = dictIsRehashing(d) ? &d->ht[1] : d->ht[0];
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
// dictSetKey 是一个简单的宏,如果 dictType 中 dupKey 函数设置
// 为非 NULL,则调用其复制 key 的值
dictSetKey(d, entry, key);
return entry;
}
dictAdd 中,如果发现 dict 正在进行 rehash,需要将新 entry 加入到 ht[1],否则可能出现数据丢失。
dictFind
dictEntry *dictFind(dict *d, const void *key) {
dictEntry *he;
unsigned int h, idx, table;
// 不管是不是在 rehash,一个非空 dict 的 ht[0].size 都不应该是0
if (d->ht[0].size == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
// 计算 hash 值
h = dictHashKey(d, key);
// 分别查找 ht[0], ht[1]
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
while (he) {
if (dictCompareKeys(d, key, he->key)) return he;
he = he->next
}
// 如果 ht[0] 里面没找到,而且没有 rehash 的话,可以直接返回 NULL
// 因为我们在 rehash 结束以后,将 ht[1] 的 table 置为 NULL,如果
// 不进行判断,会导致 core dump,也可以在取 idx 前判断 ht[table].size != 0
if (!dictIsRehashing(d)) return NULL;
}
// key 不存在
return NULL;
}
dictFind 中,在 rehash 正在进行的情况下,我们需要分别查找 ht[0],ht[1]。同时还要注意因为如果没有进行 rehash,d->ht[1].table 是 NULL,我们要小心访问空指针。
dictDelete
int dictGenericDelete(dict *d, const void *key, int nofree) {
unsigned int h, idx;
dictEntry *he, prevHe;
int table;
// 不能在空 dict 上进行删除
if (d->ht[0].size == 0) return DICT_ERR;
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 (dictCompareKeys(d, key, he->key)) {
// 从链表中删除
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
// 需要释放 key,val 的资源
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(de);
d->ht[table].used--;
return DICT_OK;
}
prevHe = he;
he = he->next;
}
// 没有在 rehash 的话,不要继续查找 ht[1]
if (!dictRehasing(d))
break;
}
// 没有对应的 key
return DICT_ERR;
}
int dictDelete(dict *d, const void *key){
// dictDelete 删除 key,val 对应的资源
return dictGenericDelete(d, key, 0);
}
如果 dict 在 rehash 那么删除时我们也需要查找 ht[0],ht[1]。
dictRelease
int _dictClear(dict *d, dictht *ht, void(*callback)(void*)) {
unsigned long i;
// 释放所有 entry
for (i = 0; i < ht->size && ht->used > 0; i++) {
dictEntry *he, *nextHe;
// ???
if (callback && (i & 65535) == 0) callback(d->privdata);
if ((he = ht->table[i]) == NULL) continue;
while (he) {
nextHe = he->next;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
ht->used--;
he = nextHe;
}
}
// 释放 bucekts
zfree(ht->table);
_dictReset(ht);
return DICT_OK;
}
void dictRelease(dict *d) {
_dictClear(d, &d->ht[0], NULL);
_dictClear(d, &d->ht[1], NULL);
zfree(d);
}
在 dict 析构时,我们需要释放两个底层 hashtable 的资源。
上面我们分析了 dict 的构造、析构接口以及增、删、查接口。相信大家已经明白了 incremental rehashing 是什么意思:将 hashtable 的 rehash 不再是一个原子函数,我们可以在搬运了部分数据以后暂停 rehash 流程。
当 rehash 尚未完成时,dict 中的两个 hashtable 成员中均有数据,dict 的增、删、查接口均需要作出相应的改变。
why incrent rehashing
那么为什么 redis 要不辞新劳的将一个 dict rehash 的过程拆分成开呢?我认为有如下原因:
- 因为 redis 的 rdb 和 aof 机制都是通过子进程 dump 数据实现的。而在常见的操作系统中,父子进程的内存都有 cow(copy on write 机制来进行优化)。通过尽量避免父进程修改数据,可以让父子进程共享相同的物理页而无需为子进程拷贝物理页。所以 redis 在启动 rdb 或者 aof 子进程以后,就会 disable resize(相关代码见 redis.c: updateDictResizePolicy)。
- redis 是单进程架构,其应该尽量避免在一次 eventloop 进行任何耗时过长的操作。通过将 resize 过程分拆,可以避免一个超大 dict 进行 resize 的时候导致其他请求无法得到响应。
最后需要注意的是,及时我们已经 disable resize,如果一个 dict 的 load factor 大于 5,redis 仍然会对这个 dict 进行 resize。