参考《Redis设计与实现》第四章 和 Redis6 源码
参考笔记链接
- Redis的数据库是用字典来作为底层实现的,字典也是哈希键的底层实现之一。字典中每个键都是独一无二的。
- Redis字典使用哈希表作为底层实现。
哈希表
Redis哈希表的结构如下
/* 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指针的一维数组。
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
哈希表节点
typedef struct dictEntry {
// 键
void *key;
// 值
// union是共用体结构,成员之间会相互影响,
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
其中,对键值对的值v赋值时,该值可以是void*指针,也可以是uint64_t整数,又或者是一个int64_t整数,也可以是double类型。
字典
// 字典
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引,指向下一个要rehash的bucket(节点链表)
// 当rehash不在进行时,值为-1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 如果>0暂停rehasing,<0表示编码错误
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;
- Redis会为用途不同的字典设置不同的类型特定函数。
- ht[1]一般只会在对ht[0]哈希表进行rehash时使用。
rehash
具体见《Redis设计与实现》第四章 4.4 rehash。
为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,需要对哈希表的大小进行相应的扩展或收缩。
负载因子的计算:
扩展和收缩哈希表的工作可以通过执行rehash (重新散列) 操作来完成。第一步就是为ht[1]分配内存空间:
例如ht[0].used = 5,如果执行扩展操作, 5 * 2 = 10;因此ht[1]的大小就是16;如果执行收缩操作,ht[1]的大小就是8。
渐进式rehash
- 为了避免rehash对服务器性能造成影响(键值对数量太多),服务器分多次、渐进式地将ht[0]里地键值对慢慢地rehash到ht[1]。
- 渐进式rehash期间,新添加的键值对一律保存到ht[1]中,ht[0]不再进行任何添加操作,保证ht[0]在这期间包含的键值对数量只减不增。
这期间在字典里面查找一个键,程序会先到ht[0]找,再到ht[1]找,具体实现如下:
// 根据键key查找哈希表节点
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL; /* dict is empty */
// 如果字典正在rehash,则执行rehash的步骤。
if (dictIsRehashing(d)) _dictRehashStep(d);
// 得到key对应的hash值
h = dictHashKey(d, key);
/* 先在ht[0]里找,再到ht[1]里去找 */
for (table = 0; table <= 1; table++) {
// 根据hash值得到在表中对应的索引值
idx = h & d->ht[table].sizemask;
// 根据索引值得到表中对应索引值的哈希表头节点
he = d->ht[table].table[idx];
// he为NULL,则该hash值对应的节点不在该表中(已经迁移)
// 否则,访问表中对应索引值的节点链表找到节点
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
/* 如果字典已经停止rehash了,那么就意味着ht[1]为空了,
在ht[0]没找到,也不用去ht[1]中找了*/
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
rehash过程
/* 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. */
// n为rehash的bucket数量,bucket即表索引对应的链表
// dickFind()调用该函数每次都只rehash一个bucket,即n = 1
// 返回0表示Rehash结束,返回1表示还得继续
int dictRehash(dict *d, int n) {
// 允许访问空bucket的最大数量
// 如果n = 1, 最多只能访问10个空bucket,超过就不访问直接返回。
// 即rehashidx最多加10 (n * 10)
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
// 表的节点数量不能为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);
// 找到非空bucket,empty_visits次数后找不到就返回了
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 */
// bucket(链表)节点全部迁移,即一次迁移整个bucket(链表)
while(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
// 得到节点在ht[1]中的索引值
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;
// rehashidx是下一个要rehash的bucket(节点链表)的索引
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
// 如果ht[0]中的节点已经全部迁移
if (d->ht[0].used == 0) {
// 清空ht[0]的哈希表
zfree(d->ht[0].table);
// ht[1]赋给ht[0]
d->ht[0] = d->ht[1];
// 重新初始化一个ht[1]供下次rehash使用
_dictReset(&d->ht[1]);
// rehashidx为-1表示rehash结束
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}