1. 前言
本文的内容是基于 redis-7.0.0
的源码,具体的源码可以参阅 redis 7.0.0。本文也不会对源文件 dict.h/dict.c
中所有的源码进行解释,只会讲解其中的主干部分,如果有兴趣可以参阅 dict.h 和 dict.c。
2. 基础概念
在计算机科学中,关联数组(Associative Array),又称映射(Map)、字典(Dictionary)是一个抽象的 数据结构(下面统称为字典),它包含着类似于(键,值)的有序对。 字典可以由 哈希表(Hash table,也叫散列表)或者 红黑树 (Red–black tree)实现。从 redis 的源码可以看出,redis 采用了哈希表的实现。
散列表由两大元素组成,数组
和哈希函数
。其中哈希函数对键值对的 key
进行 hash
运算,得到其在数组中的位置(index),然后将键值对的 value
存储在数组对应的位置上。其过程示意图如下:
随着数据的增多,就会出现不同的 key
值经过哈希函数计算得出相同的 index
的情况。也就是产生了冲突。解决冲突的方法有很多,比如:
- 双散列
- 再散列
- 开放定址法
- 建立一个公共溢出区。
- 单独链表法:将散列到同一个存储位置的所有元素保存在一个链表中。实现时,一种策略是散列表同一位置的所有冲突结果都是用栈存放的,新元素被插入到表的前端还是后端完全取决于怎样方便。
在 redis 中采用的是 单独链表法
(也叫链地址法)。对于散列到同一位置的键值对,以单链表的形式将它们组织起来。其过程示意图如下:
在上面的图中,Key1 和 Key2 进行散列后的 index 相同,对应的键值对
存储在以 index 为起始地址的单链表中。
3. redis 实现
3.1 结构定义
3.1.1 dict
在 dict.h 定义了结构体 dict。
typedef struct dict dict;
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* Keep small vars at end for optimal (minimal) struct padding */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};
主要字段的含义如下:
- type 类型为
dictType
结构体指针,dictType 中定义了特定类型的相关函数,包括key
和value
的 compare,dup,destructor等。 - ht_table[2] 类型为 dictEntry** 代表每一个键值对。这里的
2
代表每一个dict
类型有两个哈希表,ht_table[0]
是字典主要使用的哈希表,ht_table[1]
是在对ht_table[0]
进行rehash
时才使用。 - ht_used 类型为 unsigned long,表示哈希表中 entry 的数量。
- rehashidx 类型为 long,表示正在哈希表中位置处于 index 的 entry 正在 rehash。
- ht_size_exp[2] 表示 hash 表的大小的指数,
ht_table[0]
的size = 1 << ht_size_exp[0]
- pauserehash 类型为 int16_t,如果其值大于 0,则表示 rehash 暂停的,也就是此时不被允许 rehash。如果小于0,则表示 coding error。
3.1.2 dictType 结构定义
dictType 结构体中定义 key 和 value 的一些相关函数指针,包括 dup,compare,destructor 和 hashFunction等。
dup 函数用于复制,compare 函数用于比较,destructor 用于销毁。
hashFunction 用于获取 key 的哈希值,redis 默认的 hash 函数是 SipHash。
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);
/* Allow a dictEntry to carry extra caller-defined metadata. The
* extra memory is initialized to 0 when a dictEntry is allocated. */
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
3.1.3 dictEntry 结构定义
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* Next entry in the same hash bucket. */
void *metadata[]; /* An arbitrary number of bytes (starting at a
* pointer-aligned address) of size as returned
* by dictType's dictEntryMetadataBytes(). */
} dictEntry;
dictEntry 包括 key 和 value,以及指向下一个 dictEntry 的指针。
- key 类型为 void *。
- v 类型为 union
- next 类型为 dictEntry *。
metadata 字段将在下面进行介绍,从注释来看它的含义是:dictType 的 dictEntryMetadataBytes() 返回的任意大小的字节数。
3.2 dict 创建
到目前为止,我们基本上了解了 dict 的结构定义。接下来可以看下创建一个新的 dict,它的一些字段是如何初始化的。 创建一个 dict 的函数定义如下:
/* Create a new hash table */
dict *dictCreate(dictType *type)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type);
return d;
}
dictCreate
函数的参数类型为一个 dictType *
。首先为 dict 分配内存,调用 zmalloc
函数进行内存空间分配,然后调用 int _dictInit(dict *d, dictType *type)
对 dict 中的字段进行初始化。
_dictInit
函数定义如下:
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
_dictReset(d, 0);
_dictReset(d, 1);
d->type = type;
d->rehashidx = -1;
d->pauserehash = 0;
return DICT_OK;
}
调用 _dictReset
分别初始化 ht_table[0]
和 ht_table[1]
。然后再设置 type
,rehashidx
,pauserehash
三个字段。
d->type = type; 设置 ditc 的 type 字段
d->rehashidx = -1; 设置 ditc 的 rehashidx 字段,表明没有正在 rehash 的键值对。
d->pauserehash = 0;
_dictRest
函数的定义如下:
static void _dictReset(dict *d, int htidx)
{
d->ht_table[htidx] = NULL;
d->ht_size_exp[htidx] = -1;
d->ht_used[htidx] = 0;
}
_dictRest
分别对 ht_tabel[htidx]
, ht_size_exp[htidx]
和 ht_used[htidx]
进行初始化。
可以将一个新建的 dict
表示成如下图所示:
3.3 dict 添加元素
3.3.1 dictAdd
往一个 dict 添加元素的 dictAdd
函数的定义如下,可以看到在其中调用了 dictAddRaw
函数。
/* Add an element to the target hash table */
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
函数定义如下:
/* Low level add or find:
* This function adds the entry but instead of setting a value returns the
* dictEntry structure to the user, that will make sure to fill the value
* field as they wish.
*
* This function is also directly exposed to the user API to be called
* mainly in order to store non-pointers inside the hash value, example:
*
* entry = dictAddRaw(dict,mykey,NULL);
* if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
*
* Return values:
*
* If key already exists NULL is returned, and "*existing" is populated
* with the existing entry if existing is not NULL.
*
* If key was added, the hash entry is returned to be manipulated by the caller.
*/
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
int htidx;
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
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]++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
判断是否正在执行 reshash 的宏定义如下:
#define dictIsRehashing(d) ((d)->rehashidx != -1)
首先判断是否正在 rehash。如果是则执行 _dictRehashStep()
函数。
_dictRehashStep()
的代码如下,可以看出,如果 d->pauserehash == 0
,则调用 dictRehash
函数进行 rehash,这一部分我们下面介绍。
/* This function performs just a step of rehashing, and only if hashing has
* not been paused for our hash table. When we have iterators in the
* middle of a rehashing we can't mess with the two hash tables otherwise
* some elements can be missed or duplicated.
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used. */
static void _dictRehashStep(dict *d) {
if (d->pauserehash == 0) dictRehash(d,1);
}
在判断是否 rehash 之后,则判断要插入到 dict 中 key 是否存在于 dict 中。代码如下:
// Get the index of the new element, or -1 if
// the element already exists.
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
dictHashKey
的宏定义如下,调用的是 dict 的 type 参数中的 hashFunction
函数,来计算 hash
值。
#define dictHashKey(d, key) (d)->type->hashFunction(key)
如果 key 存在于 dict 中,则返回 -1,如果不存在,则返回其在 ht_table 中的 index。此时需要注意的是,在 redsi 7.0 的源码中,默认使用的哈希函数是使用 SipHash 实现的,其源码定义在 siphash.c 中。
/* -------------------------- hash functions -------------------------------- */
static uint8_t dict_hash_function_seed[16];
void dictSetHashFunctionSeed(uint8_t *seed) {
memcpy(dict_hash_function_seed,seed,sizeof(dict_hash_function_seed));
}
uint8_t *dictGetHashFunctionSeed(void) {
return dict_hash_function_seed;
}
/* The default hashing function uses SipHash implementation
* in siphash.c. */
uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t dictGenHashFunction(const void *key, size_t len) {
return siphash(key,len,dict_hash_function_seed);
}
uint64_t dictGenCaseHashFunction(const unsigned char *buf, size_t len) {
return siphash_nocase(buf,len,dict_hash_function_seed);
}
_dictKeyIndex
函数定义如下
/* Returns the index of a free slot that can be populated with
* a hash entry for the given 'key'.
* If the key already exists, -1 is returned
* and the optional output parameter may be filled.
*
* Note that if we are in the process of rehashing the hash table, the
* index is always returned in the context of the second (new) hash table. */
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 & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
/* 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;
}
DICTHT_SIZE 和 DICTHT_SIZE_MASK 宏定义如下:
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
下面的代码是为了防止 idx 的值超过了 ht_table 的大小,数组越界。
idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
ht_table 的大小为 1 << exp,因为 DICTHT_SIZE_MASK(exp) 返回的值只有 0 和 (1<<exp)- 1)。所以 idx 的值永远不会大于或者等于 1 << exp。
if ht_size_exp[0] = 2;
so ht_table[0] 的大小为 8;
假如 hashFunction(key) 的值为 100,超过了ht_table[0] 的大小 8。
DICTHT_SIZE_MASK(2) = 7
那么 idx 的值为:100 & 7 = 4
_dictKeyIndex
函数调用 _dictExpandIfNeeded
函数是否需要扩充散列表(下面再介绍这个函数)。
最后的 for 循环遍历两个 ht_table 查找是否有相应的 key 在散列表中。如果存在,返回相应的 idx
以及获得将相应 dictEntry
。
在dictAddRaw函数中使用头插法插入 entry,并且更新 ht_table_used 的值
entry->next = d->ht_table[htidx][index];
d->ht_table[htidx][index] = entry;
d->ht_used[htidx]++;
最后调用 dictSetKey
函数,插入对应的 key value。
void dictSetKey(dict *d, dictEntry* de, void *key) {
assert(!d->type->no_value);
if (d->type->keyDup)
de->key = d->type->keyDup(d, key);
else
de->key = key;
}
当 addRaw 函数执行完,在执行 dictSetVal
函数,设置新添加的 key 的 value。代码如下:
void dictSetVal(dict *d, dictEntry *de, void *val) {
assert(entryHasValue(de));
de->v.val = d->type->valDup ? d->type->valDup(d, val) : val;
}
下图显示了一个只有 ht_table[0]
中添加了几个元素的 dict 的示意图:
至此向一个 dict 中添加键值对的所涉及的代码已经梳理完了,虽然没有特别详细的解释每一行代码,但是抓住了主干。总结一下就是:
- dict 正在 rehash,则进行 rehash 之后再进行插入键值对。
- 如果需要对 ht_table 进行扩充,则进行扩充。
- 查找添加的 key 是否存在于 ht_table。如果存在,则不进行插入 entry,而是找到 key 值相等 entry,重新设置其 value。
- 如果不存在,则新建entry,计算要插入到 ht_table 中 index。获取到散列到该 index 的所有 entry 组成的单链表的表头,然后将新建的 entry 利用头插法插入到链表中。
- 更新 dict 中 相关的字段。比如 ht_table_used[0]++,表示 ht_table[0] 中的 dictEntry 加 1。
3.3.3 dictAddAndFind
dictAddOrFind()
只是 dictAddRaw()
的一个简单版本,它始终返回指定键的哈希条目,即使该键已经存在并且无法添加(在这种情况下,返回已存在键的条目)。
/* Add or Find:
* dictAddOrFind() is simply a version of dictAddRaw() that always
* returns the hash entry of the specified key, even if the key already
* exists and can't be added (in that case the entry of the already
* existing key is returned.)
*
* See dictAddRaw() for more information. */
dictEntry *dictAddOrFind(dict *d, void *key) {
dictEntry *entry, *existing;
entry = dictAddRaw(d,key,&existing);
return entry ? entry : existing;
}
3.3.4 dictFind
dictFind
查找 dict 中某个 key 的 dictEntry。如果存在,则返回该 entry。如果不存在,则返回 NULL。
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL; /* dict is empty */
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;
}
3.4 dict rehash
在 int dictRehash(dict *d, int n)
中进行 rehash 过程。这个函数的主要目的是将 ht_table[0] 中的键值对 rehash 到 ht_table[1]
中。并且将 ht_table[0]
数据清空,ht_table[1]
替换为新的 ht_table1[0]
。int dictRehash(dict *d, int n)
代码如下:
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
if (dict_can_resize == DICT_RESIZE_AVOID &&
((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
(s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
{
return 0;
}
while(n-- && d->ht_used[0] != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
//找到第一个不为空的 rehashidx。
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
//获取该 idx 对应的单链表头节点。
de = d->ht_table[0][d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
//移动该bucket(也指单链表) 所有的 keys(也指单链表中的节点) 到新的 hash table 中。
while(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
//获取该 entry 在新ht_table[1] 中的 index
h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
//插入新 entry 在 ht_table[1][h] 单链表中
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
//更新ht_table 中的 entry 数量。
d->ht_used[0]--;
d->ht_used[1]++;
de = nextde;
}
//将ht_table[0]中正在 rehash 的 bucket置空
d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
//如果ht_table[0] 不存在任何节点,则将ht_table[0]置空。
if (d->ht_used[0] == 0) {
zfree(d->ht_table[0]);
/* Copy the new ht onto the old one */
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;
}
/* More to rehash... */
return 1;
}
- rehash 第一步首先判断是否满足 rehash 的条件。其中 empty_visits 表示最大的能访问的空的 hash 中空的条目的数量。当满足这个条件,则就不进行 rehash。这也意味着该函数可能不会 rehash 整个 ht_table[0]。
int empty_visits = n*10; /* Max number of empty buckets to visit. */
unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
if (dict_can_resize == DICT_RESIZE_AVOID &&
((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
(s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
{
return 0;
}
- 找到第一个不为空的 rehashidx。
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
- 获取该 rehashidx 对应的单链表头节点。
de = d->ht_table[0][d->rehashidx];
移动该bucket(也指单链表) 所有的 keys(也指单链表中的节点) 到新的 hash table 中。
while(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
//获取该 entry 在新ht_table[1] 中的 index
h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
//插入新 entry 在 ht_table[1][h] 单链表中
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
//更新ht_table 中的 entry 数量。
d->ht_used[0]--;
d->ht_used[1]++;
de = nextde;
}
将 ht_table[0]
中正在 rehash 的 bucket 置空
d->ht_table[0][d->rehashidx] = NULL;
d->rehashidx++;
如果 ht_table[0] 不存在任何节点,则将 ht_table[0] 置空。
if (d->ht_used[0] == 0) {
//释放内存
zfree(d->ht_table[0]);
/* Copy the new ht onto the old one */
//将 ht_table[1] 复制给 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];
//重置 ht_table[1]
_dictReset(d, 1);
//并且标志 dict 为没有正在 rehash
d->rehashidx = -1;
return 0;
}
3.5 dict expand
当需要对 dict 进行扩充容量时,调用 dictExpand
函数,其内部调用的是 _dictExpand
函数,源码如下:
/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
return _dictExpand(d, size, NULL);
}
/* Expand or create the hash table,
* when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
* Returns DICT_OK if expand was performed, and DICT_ERR if skipped. */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
if (malloc_failed) *malloc_failed = 0;
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht_used[0] > size)
return DICT_ERR;
/* the new hash table */
dictEntry **new_ht_table;
unsigned long new_ht_used;
signed char new_ht_size_exp = _dictNextExp(size);
/* Detect overflows */
size_t newsize = 1ul<<new_ht_size_exp;
if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
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;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
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;
}
/* Prepare a second hash table for incremental rehashing */
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;
}
- 判断是否满足 expand 的条件,如果 dict 正在 rehash 或者形参 size 小于 ht_table[0] 中 entry 的个数,则返回 DICT_ERR。
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht_used[0] > size)
return DICT_ERR;
- 初始化新的 hash table, size。判断是否能够满足条件,如果不能,则返回 DICT_ERR。
/* the new hash table */
dictEntry **new_ht_table;
unsigned long new_ht_used;
signed char new_ht_size_exp = _dictNextExp(size);
/* Detect overflows */
size_t newsize = 1ul<<new_ht_size_exp;
if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;
_dictNextExp
函数获取能大于 size 的ht_table_exp 的值 e。
/* TODO: clz optimization */
/* Our hash table capability is a power of two */
static signed char _dictNextExp(unsigned long size)
{
unsigned char e = DICT_HT_INITIAL_EXP;
if (size >= LONG_MAX) return (8*sizeof(long)-1);
while(1) {
if (((unsigned long)1<<e) >= size)
return e;
e++;
}
}
- 重新分配内存
/* Allocate the new hash table and initialize all pointers to NULL */
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;
- 如果是 dict 初始化后的第一次expand,将新创建的 hash table 赋值给 ht_table[0],并且设置 ht_used[0] 和 ht_size_exp[0] 的值。返回 DICT_OK。否则,执行第 5 步。
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
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;
}
- 将新创建的 hash table 赋值给 ht_table[1],供 rehash 过程使用。
/* Prepare a second hash table for incremental rehashing */
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;
redis 中还定义了一个 dictTryExpand
函数,它与 dictExpand
不同的是,dictTryExpand
当内存分配失败时,会返回 DICT_ERR,而 dictExpand
则是当实际 expand 并没有执行则会返回 DICT_ERR。
/* return DICT_ERR if expand failed due to memory allocation failure */
int dictTryExpand(dict *d, unsigned long size) {
int malloc_failed;
_dictExpand(d, size, &malloc_failed);
return malloc_failed? DICT_ERR : DICT_OK;
}
4. dict iterator
4.1 iterator 结构
迭代器 用于访问容器里元素。redis 也为 dict 中的 entry
遍历设计了 dictIterator,在 dict.h 中定义了 dictIterator
结构体。
/* If safe is set to 1 this is a safe iterator, that means, you can call
* dictAdd, dictFind, and other functions against the dictionary even while
* iterating. Otherwise it is a non safe iterator, and only dictNext()
* should be called while iterating. */
typedef struct dictIterator {
dict *d;
long index;
int table, safe;
dictEntry *entry, *nextEntry;
/* unsafe iterator fingerprint for misuse detection. */
unsigned long long fingerprint;
} dictIterator;
- d 迭代器遍历的字典。
- index 遍历的 ht_table 的index 。
- table 当前 dict 遍历的哈希表,ht_table[0] 或者 ht_table[1]。
- safa 表示迭代器是否是 安全遍历。
- entry 表示当前迭代器遍历的 entry。
- nextEntry 表示接下来要遍历的 entry。
- fingerprint 表示字典当前状态的签名(后面有详细的介绍)。
4.2 dict iterator get
其中 dictGetIterator
函数用于获取一个 dict 的迭代器,而 dictGetSafeIterator
函数用于获取一个安全的迭代器,这是通过设置 dictIterator->safe = 1 来实现的。
dictGetIterator
源码如下:
dictIterator *dictGetIterator(dict *d)
{
dictIterator *iter = zmalloc(sizeof(*iter));
iter->d = d;
iter->table = 0;
iter->index = -1;
iter->safe = 0;
iter->entry = NULL;
iter->nextEntry = NULL;
return iter;
}
dictGetSafeIterator
源码如下:
dictIterator *dictGetSafeIterator(dict *d) {
dictIterator *i = dictGetIterator(d);
i->safe = 1;
return i;
}
那么此时就有一个问题,如何保证迭代器的安全是如何实现的呢?在 dictNext(dictIterator *iter)
函数中,通过判断 iter->safe 的值,然后通过 dictPauseRehashing(iter->d)
,来禁止对 dict 进行 rehash,保证迭代器遍历过程,不会发生 rehash。
#define dictPauseRehashing(d) (d)->pauserehash++
4.3 dict iterator next
dict iterator 的遍历,通过 dictNext 方法返回下一个遍历的元素,如果不存在,则返回 NULL。
dictEntry *dictNext(dictIterator *iter)
{
while (1) {
if (iter->entry == NULL) {
if (iter->index == -1 && iter->table == 0) {
if (iter->safe)
dictPauseRehashing(iter->d);
else
iter->fingerprint = dictFingerprint(iter->d);
}
iter->index++;
if (iter->index >= (long) DICTHT_SIZE(iter->d->ht_size_exp[iter->table])) {
if (dictIsRehashing(iter->d) && iter->table == 0) {
iter->table++;
iter->index = 0;
} else {
break;
}
}
iter->entry = iter->d->ht_table[iter->table][iter->index];
} else {
iter->entry = iter->nextEntry;
}
if (iter->entry) {
/* We need to save the 'next' here, the iterator user
* may delete the entry we are returning. */
iter->nextEntry = iter->entry->next;
return iter->entry;
}
}
}
iter->entry == NULL 时,有两种可能,
- iterator 初始化后未进行遍历。
- 上一次迭代的 entry 的 nextentry 为 NULL,所以当前 iter->entry 为 NULL。
当因为是 iterator 初始化后未进行遍历,判断是否为 safe。
- 若 iter->safe 为 1 时,表示是安全的 iter。则调用 dictPauseRehashing 停止 rehashing。
dictPauseRehashing(iter->d);
- 若 iter->safe 为 0 时,表示的是不完全的 iter。需要计算此时 dict 的 fingerprint。
iter->fingerprint = dictFingerprint(iter->d);
判断当前遍历hash table 的 index 是否超过了,当前遍历的 ht_table 的最大长度。
- 如果正在 rehashing,且当前正在遍历 ht_table[0],则更换遍历成 ht_table[1]。且设置 iter->index == 0。
- 如果不满足 1 的条件,则直接 break 跳出 while 循环,函数返回 NULL。
更新 iter->entry 为下一个要遍历的 entry。
- iter->entry == NULL
iter->index++;
iter->entry = iter->d->ht_table[iter->table][iter->index];
- iter->entry != NULL
iter->entry = iter->nextEntry;
最后判断当前 iter->entry 的值。
- iter->entry != NULL,更新 iter->entry 和 iter-nextEntry的值。
if (iter->entry) {
/* We need to save the 'next' here, the iterator user
* may delete the entry we are returning. */
iter->nextEntry = iter->entry->next;
return iter->entry;
}
- iter->entry == NULL,直接返回 NULL。
return NULL;
4.4 dict fingerpinter 计算
计算一个 dict
的 fingeprint
是通过 dictFingerprint
函数来进行介绍算的。从注释可以看出:
fingerprint 是一个 64 位数字,表示在给定的时间点的状态,它的值为 dict 的各个字段的通过异或元算得出。当一个不安全的 iterator 初始化,并且第一次遍历时,我们计算其 fingerprint 值。当 iterator 释放时,我们会再对 dict 的 fingerprint 进行计算,如果此时的 fingerprint 与之前计算的不同,则意味着 dict 的遍历过程中,存在对 dict 进行了某些禁止操作(比如 添加元素)。
/* A fingerprint is a 64 bit number that represents the state of the dictionary
* at a given time, it's just a few dict properties xored together.
* When an unsafe iterator is initialized, we get the dict fingerprint, and check
* the fingerprint again when the iterator is released.
* If the two fingerprints are different it means that the user of the iterator
* performed forbidden operations against the dictionary while iterating. */
unsigned long long dictFingerprint(dict *d) {
unsigned long long integers[6], hash = 0;
int j;
integers[0] = (long) d->ht_table[0];
integers[1] = d->ht_size_exp[0];
integers[2] = d->ht_used[0];
integers[3] = (long) d->ht_table[1];
integers[4] = d->ht_size_exp[1];
integers[5] = d->ht_used[1];
/* We hash N integers by summing every successive integer with the integer
* hashing of the previous sum. Basically:
*
* Result = hash(hash(hash(int1)+int2)+int3) ...
*
* This way the same set of integers in a different order will (likely) hash
* to a different number. */
for (j = 0; j < 6; j++) {
hash += integers[j];
/* For the hashing step we use Tomas Wang's 64 bit integer hash. */
hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
hash = hash ^ (hash >> 24);
hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
hash = hash ^ (hash >> 14);
hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
hash = hash ^ (hash >> 28);
hash = hash + (hash << 31);
}
return hash;
}
4.4 dict iterator release
当对 iterator 进行释放时,首先判断对 iterator 是否进行过遍历,如果进行过遍历,则判断是否为 safe,如果 safe,则需要 resumeRehashing。如果不是 safa,则需要比较 fingerprint。最后调用 zfree(iter) 释放内存。
void dictReleaseIterator(dictIterator *iter)
{
if (!(iter->index == -1 && iter->table == 0)) {
if (iter->safe)
dictResumeRehashing(iter->d);
else
assert(iter->fingerprint == dictFingerprint(iter->d));
}
zfree(iter);
}
5. 总结
- 字典是由键值对构成的抽象数据结构。
- Redis 字典的底层实现为哈希表,每个字典使用两个哈希表,一般情况下只使用 ht_table[0], 只有在 rehash 进行时,才会同时使用 ht_table[0] 和 ht_table[1]。
- 哈希表使用链地址法来解决键冲突的问题。
- 对哈希表的 rehash 是分多次、渐进式地进行的。
- Redis 中安全的 iterator,进行遍历时,会终止 rehash 过程。
- Redis 中不安全的 iterorator 销毁时使用 fingerprint 来验证 dict 是否被更改。
对于 redis 的 rehash 和 expand 等过程,在 Redis设计与实现之字典 都有具体的示意图表示,有兴趣的可以阅读此 blog。引用中内容不是基于 redis 7.0.0,不过原理大致都是相通的。还有许多函数未进行阅读,不过主体的代码都已经介绍了。