redis源码分析 - 字典结构
redis 字典对象分析
字典,又称为符号表 (symbol table)、关联数组 (associated array)或映射 (map),是一种用于保存键值对 (key-value pair)的抽象数据结构。在 redis 中,哈希键和数据库都是通过字典作为底层实现的。
结构体
redis 中字典的结构如下:
typedef struct dictEntry {
void *key; //键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; //值,使用联合体,可以是指针,也可以是其他类型的值
struct dictEntry *next; //使用拉链法解决哈希冲突问题
} dictEntry;
对两个64位类型解释如下
typedef unsigned __int64 uint64_t;
typedef signed __int64 int64_t;
可以将 __int64
理解成 long long
typedef struct dictType {
unsigned int (*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);
} dictType;
dictType
结构体为一簇操作特定类型键值对的函数,privdata
为传给这些函数的可选参数
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
dictht 为哈希表结构,哈希表结构有一个 dictEntry 的双重指针(可理解成 dictEntry 指针数组),size 为哈希表大小,sizemask 为哈希表大小掩码,用于计算哈希索引值,总是等于 size-1
,used 为哈希表元素个数,即已有的节点的数量。
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
dict 为字典结构,包括两个哈希表ht[2], 一个用于正常使用,另一个,当需要扩大哈希表时,将ht[0]中的节点rehashed 到 ht[1] 上,然后再将 ht[1] 设置为ht[0], ht[1]置空,准备下一次 rehashed。 rehashidx 记录了 rehash 的进度,当没有做 rehash 时,它的值为 -1, 当在做 rehash 的值时,它的值表示的是当前 rehash 到了 ht[0] 中的哪一个位置了(可以理解成 ht[0].table[rehashidx])。 redis 的 rehash 是渐进式的,即通过 rehashidx 一个一个递增的形式 rehash 的。
字典的详细实现
计算哈希值和索引值
redis计算哈希值和索引值,是根据键值来计算的,先计算出哈希值,然后根据哈希值和 sizemask 计算索引值。
/* 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.
*
* 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 int _dictKeyIndex(dict *d, const void *key)
{
unsigned int h, idx, table;
dictEntry *he;
/* Expand the hash table if needed */
if (_dictExpandIfNeeded(d) == DICT_ERR) //判断是否需要扩大哈希表的大小,如果正在 rehash,返回 true
//如果used/size 大于安全阀值5时,将需要扩大哈希表大小
return -1;
/* Compute the key hash value */
h = dictHashKey(d, key); //计算哈希值
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask; //计算索引值,在哈希表中查找是否存在,存在返回-1,不存在,返回索引值
/* Search if this slot does not already contain the given key */
he = d->ht[table].table[idx];
while(he) {
if (dictCompareKeys(d, key, he->key))
return -1;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return idx;
}
上面这个函数,为获取哈希值和索引值的过程, 首先通过 dictHashKey(d, key)
获取哈希值,这是一个宏函数
#define dictHashKey(d, key) (d)->type->hashFunction(key)
然后根据哈希值求索引值,idx = h & d->ht[table].sizemask
,如果此元素在哈希表中已存在,返回-1,不需要再次插入,否则返回一个可以用的位置。搜索哈希表时,需要在两个哈希表中都要搜索。
那么,当 _dictExpandIfNeed (d)
判断出需要扩大哈希表时,是如何扩大哈希表的呢?_dictExpandIfNeed (d)
这个函数在判断时:
- 如果
rehashidx != -1
说明,哈希表在rehash
,此时表明就是正在扩大哈希 - 如果
used/size
(这两个都是dictht
的成员)大于阀值dict_force_resize_ratio
(为5)时,就需要扩大哈希表。
扩大哈希表的函数如下
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size) //如果rehashidx不是-1,说明正在扩,不需重复操作;而且扩大后,used 应该小于 size
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.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[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n; // 将 ht[1] 设置为扩大后的哈希表,然后将 rehashidx 置为0,表明从 0 开始 rehash
d->rehashidx = 0;
return DICT_OK;
}
此函数需要注意最后一段代码
d->ht[1] = n;
d->rehashidx = 0;
将扩大后的哈希设置为 ht[1],然后设置 rehashidx 为0,启动 rehash,将 ht[0] 都 从 ht[0].table[0] 开始全部 rehash 到 ht[1].table 中,后面将详细介绍 redis 的 rehash 的过程。
计算哈希值
字典在计算哈希值时,是通过调用宏函数 (d)->type->hashFunction (key)
得到的,这只是一个函数指针,在 redis 中,有两个方法计算哈希值。
/* MurmurHash2, by Austin Appleby
* Note - This code makes a few assumptions about how your machine behaves -
* 1. We can read a 4-byte value from any address without crashing
* 2. sizeof(int) == 4
*
* And it has a few limitations -
*
* 1. It will not work incrementally.
* 2. It will not produce the same results on little-endian and big-endian
* machines.
*/
unsigned int dictGenHashFunction(const void *key, int len) {
/* 'm' and 'r' are mixing constants generated offline.
They're not really 'magic', they just happen to work well. */
uint32_t seed = dict_hash_function_seed;
const uint32_t m = 0x5bd1e995;
const int r = 24;
/* Initialize the hash to a 'random' value */
uint32_t h = seed ^ len;
/* Mix 4 bytes at a time into the hash */
const unsigned char *data = (const unsigned char *)key;
while(len >= 4) {
uint32_t k = *(uint32_t*)data;
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
/* Handle the last few bytes of the input array */
switch(len) {
case 3: h ^= data[2] << 16;
case 2: h ^= data[1] << 8;
case 1: h ^= data[0]; h *= m;
};
/* Do a few final mixes of the hash to ensure the last few
* bytes are well-incorporated. */
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return (unsigned int)h;
}
当字典被用作数据库的底层实现或者哈希键的底层实现时, redis
使用 Murmurhash2
算法来计算哈希的值。 Murmurhash
哈希算法是有 Austin Appleby
与 2008 年发明的,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。
/* And a case insensitive hash function (based on djb hash) */
// 这是一个比较简单的计算哈希值的方法,字符串哈希,就是不断乘以33
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
unsigned int hash = (unsigned int)dict_hash_function_seed; //哈希方法的种子
while (len--)
hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
//hash << 5 + hash = hash * 2^5 + hash = hash * 32 + hash = hash * 33
return hash;
}
解决键冲突
当不同的 key 利用哈希算法得到相同的 hash 值时,哈希表时如何解决冲突问题的呢?通过前面的哈希节点的结构可以看到dictEntry
结构是一个链表的节点,有一个指向 dictEntry
节点的指针成员,确实,哈希表就是通过拉链法(或者链地址法)来解决冲突问题的,每一个哈希节点都有一个next指针,相同哈希索引的多个节点可以通过next指针相连构成一个单链表,这就解决了冲突问题。
因为dictEntry 组成的单链表是没有尾节点的,每次插入一个节点都是从头部插入。
/* Low level add. 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 he wishes.
*
* 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);
* if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
*
* Return values:
*
* If key already exists NULL is returned.
* If key was added, the hash entry is returned to be manipulated by the caller.
*/
/*如果key已经在哈希表中已经存在,返回NULL;否则返回该哈希节点*/
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
/* 判断是否正在 rehash,正在 rehash 中的哈希表与没有 rehash 的哈希表插入操作不同
1. 当 rehash 时,需要将ht[0] 中的所有节点 rehash 到 ht[1]中,这时,为了保证ht[0]中的节点是一直减少的,插入时插入到ht[1]中
2. 没有 rehash ,直接将节点插入到 ht[0]中
*/
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
/* Allocate the memory and store the new entry */
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index]; //从链表头插入
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
从链表头插入,不需要单向遍历到链表尾部在插入节点,降低了插入节点时的时间复杂度 O(1)
rehash
哈希表的一个重要的比例参数为_负载因子_(load factor),它的计算公式为
load_factor = used / size; //代码中的注释为 USED/BUCKETS ratio
随着操作的不断进行,哈希表中的节点会逐渐的增加或者减少,为了维持 load factor 在合适的范围之内,程序需要对哈希表进行扩展 (expand) 或者收缩,而这,是通过 rehash 来完成的。
redis 对字典的哈希表进行 rehash 操作的步骤如下:
1) 为 ht[1] 分配空间,这个空间的大小取决于要执行的操作(扩展还是收缩),以及 ht[0] 当前包含的键值对的数量(ht[0].used):
a. 如果是扩展操作,ht[1] 的大小为不小于 ht[0].used*2 的 2^n;
b. 如果是收缩操作,ht[1] 的大小为不小于 ht[0].used 的 2^n
2) 将 ht[0] 保存的所有键值对 rehash 到 ht[1] 中, rehash 是指重新计算哈希值和索引值,然后保存到 ht[1] 中;
3) 当 ht[0] 上的所有键值对都迁移到了 ht[1] 上之后(此时ht[0]是一张空表),释放 ht[0],同时将 ht[1] 设置为 ht[0],在为 ht[1] 创建一个空表哈希,为下一次 rehash 做准备。
引用中的内容摘自 黄健宏的《Redis设计与实现》4.4节 rehash
扩展
/* Using dictEnableResize() / dictDisableResize() we make possible to
* enable/disable resizing of the hash table as needed. This is very important
* for Redis, as we use copy-on-write and don't want to move too much memory
* around when there is a child performing saving operations.
*
* Note that even when dict_can_resize is set to 0, not all resizes are
* prevented: a hash table is still allowed to grow if the ratio between
* the number of elements and the buckets > dict_force_resize_ratio. */
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
以上两个 static 变量说明的是允许哈希表进行扩展或者收缩的前提条件。
redis 使用了写时复制的原则(COW),打个比方,当父进程创建子进程时,这时,采用写时复制的原则,父子进程访问的数据都是同一份拷贝,只有当其中一个需要对数据进行修改时,才会将父进程的内容拷贝一份给子进程,这样能够极大的节约内存。所以,当 redis 创建服务器子进程时,通过写时复制原则节约内存,就希望在子进程存在期间,尽可能的不要对哈希表进行扩展操作,服务器通过增大负载因子来达到这一目的,从而最大限度的节约内存。
dict_can_resize
设置为1时,表示哈希表可以扩展或者收缩,设置为0时,表示不能操作,但是,当负载因子超过阈值 dict_forece_resize_ratio
时,会强制进行扩展操作。上文也介绍了 _dictExpandIfNeeded
这个函数
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
从代码中可以看出:
- 当负载因子(load factor)达到 1 时,如果允许扩展(
dict_can_resize
设置为1),就进行扩展操作 - 当负载因子超过安全阈值
dict_force_resize_ratio
时,就会强制进行扩展
收缩
当哈希表节点数减少时,为了保证 load factor 在一个合理范围内,需要对哈希表进行收缩,当负载因子小于等于 1 时,就需要做这种操作。
/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{
int minimal;
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
哈希表调整大小的计算
哈希表进行扩展或者收缩,新的哈希大小是如何计算的呢
/* 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;
while(1) {
if (i >= size)
return i;
i *= 2;
}
} `DICT_HT_INITAL_SIZE`的值为4,即 2^2,所以每次设置的新哈希表的大小均为 2 的 n 次幂。
渐进式 rehash
哈希表 rehash 操作时将 ht[0] 中的所有键值对 rehash 到 ht[1] 中,但是这个动作并不是一次性、集中式的完成的,而是渐进式的、分多次完成的。因为当键值对数量很大时,集中式的 rehash 操作,需要庞大的计算量,可能会导致服务器在一段时间内停止服务。为了避免 rehash 对服务器性能造成影响,所以将 rehash 操作分多次执行,渐进式的操作。
1) 当需要 rehash 时,将 rehashidx 设置为0
2) 为 ht[1] 分配空间,此时字典同时拥有 ht[0] 和 ht[1] 两个哈希表
3) 在 rehash 期间,通过判断 rehashidx 是否等于 -1,每次对字典进程添加、删除、查找和更新操作时,出了执行指定操作之外,还会将 ht[0].table[rehashidx]
这个单链表上的所有键值对 rehash 到 ht[1] 中,操作完成后,将 rehashidx 加 1
4) 当 rehash 操作完成后,rehashidx 的值被设置为 -1
/* 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. */
// 返回0表示 rehash 结束,返回1 表示还有键值对需要继续 rehash
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
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];
/* Move all the keys in this bucket from the old to the new hash HT */
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
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++; //操作完将 rehashidx 加1
}
/* Check if we already rehashed the whole table... */
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;
}
/* More to rehash... */
return 1;
}
哈希操作
查找操作
查找操作,当 rehashidx 的值不等于 -1 时,说明正在 rehash,那么进行一次rehash操作,在 _dictRehashStep(d)
中调用 dictRehash(d, 1)
执行一次。
查找时,在 ht[0] 和 ht[1] 中都需要查找
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
unsigned int h, idx, table;
if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */
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];
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
添加
添加操作在上文中解决键冲突一节已经介绍了。
删除
删除操作,先在哈希表中查找指定 value 的键值对,然后删除。
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0);
}
/* Search and remove an element */
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
unsigned int h, idx;
dictEntry *he, *prevHe;
int table;
if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */
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)) { //找到了
/* Unlink the element from the list */
if (prevHe) //从单链表中删除该节点
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(he);
d->ht[table].used--;
return DICT_OK;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return DICT_ERR; /* not found */
}
更新
/* Add an element, discarding the old if the key already exists.
* Return 1 if the key was added from scratch, 0 if there was already an
* element with such key and dictReplace() just performed a value update
* operation. */
int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, auxentry;
/* Try to add the element. If the key
* does not exists dictAdd will suceed. */
if (dictAdd(d, key, val) == DICT_OK)
return 1;
/* It already exists, get the entry */
entry = dictFind(d, key);
/* Set the new value and free the old one. Note that it is important
* to do that in this order, as the value may just be exactly the same
* as the previous one. In this context, think to reference counting,
* you want to increment (set), and then decrement (free), and not the
* reverse. */
auxentry = *entry;
dictSetVal(d, entry, val);
dictFreeVal(d, &auxentry);
return 0;
}
参考文献:
- Redis设计与实现,黄健宏
- 博客文章[http://blog.csdn.net/androidlushangderen/article/details/39860693]