字典
字典是 Redis 中相当重要的结构,可以说, Redis 数据库的底层就是用字典实现的,而对数据库的增删改查,也是基于对字典的操作实现的.
字典的基本结构如下
4.1.1 哈希表
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
分别记录当前哈希表中总数,已记录数,指针数组,构成基本结构,用哈希算法进行索引计算存放功能
4.1.2 哈希节点
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
类似于 std::pair 的结构设计,拥有 key 和 val 成员变量,额外多出了 next 指针,内容是指向相同 hash 结果的下一个对象指针,也表明了 Redis 的 hash 碰撞用链表来解决,(另一种是拉链式,顺位+1),需要说的是,当相同 hash 结果得出后需要链表插入时,是插入至头部而非尾部,其复杂度从 O(N) 降至 O(1)
4.1.3 字典
/*
* 字典类型特定函数
*/
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 这个结构用于存放特殊类型键值队函数接口调用存放,结构内全部是函数指针
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
核心 dict 结构, 可以看到有两张 dictht 哈希表,正常情况下,只有一张表来进行数据的处理存储,另一张表将在处理 rehash 时进行使用.
rehashidx 表示当前 rehash 执行到哪个步骤
4.2 哈希算法
哈希算法指的是相同的输入会输出一个相同的结果的一个计算函数,评判算法的优劣可以通过两个方面,一个是算法的速度,一个算法算出的结果的一个均匀程度.
从 Redis 来讨论 .
可以认为算法结果越均匀,负载因子就越能表现当前字典中的负载程度,也就能减少 rehash 的发生…
Redis 使用的 hash 算法为 MurmurHash2.
4.3 解决键冲突
当哈希结果冲突时, dictEntry 使用链表的形式放置在第一个指针的位置
4.4. rehash
观察 dictht 的数据结构,在最坏的情况下,也就是所有的输入都输出了相同的 hash 结果时, table 将退化成一张链表结构, Redis 作为高校的数据库肯定是不容许该种情况,当然,正常工作环境中不会出现这种极端情况,但并不妨碍得出大量的链表结构将减慢执行效果的这一结论,所以 Redis 的处理行为为是进行 rehash 操作.
rehash 简单来说就是通过扩容来重新排列元素在 dictht 中所处的位置,是元素更均匀的分布,来减少链表结构的产生,以加快 Redis 的整体运行效率
/* 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. */
// 一下两个条件之一为真时,对字典进行扩展
// 1)字典已使用节点数和字典大小之间的比率接近 1:1
// 并且 dict_can_resize 为真
// 2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 新哈希表的大小至少是目前已使用节点数的两倍
// T = O(N)
return dictExpand(d, d->ht[0].used*2);
}
rehash 的条件较书中略有不同,但不妨碍我们对何时进行 rehash 这个行为的理解.
一下两个条件之一为真时,对字典进行扩展
1)字典已使用节点数和字典大小之间的比率接近 1:1并且 dict_can_resize 为真
2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
4.5 渐进式rehash
rehash 时我们并没有讨论具体场景, 我们在进行 rehash 的时候往往是认为一次性进行所有元素的重新计算, 其实这是不现实的,当我们元素只有几百个几千个时,我们当然可以,但一旦数量超过几个数量级时,达到几亿,几十亿时,我们还可以一口气进行 rehash 吗? 想象一下,如果真的决定一口气进行rehash,那么数据库一定会发生一段时间的无法响应,这显然是不允许的,所以,我们需要进行渐进式rehash.
但是随着 渐进式rehash 这个策略的确定,随之而来的问题会更多,比如,在 rehahs 过程中,如何正确的相应查询,插入,删除等命令就是一件不容易的事,因为此时数据已经发生了迁移.
rehash 的详细步骤
1): 为ht[1]分配空间,让字典同事持有ht[0] 和 ht[1].
2): 将 rehashidx 置为0, 表示开始进行 rehash.
3): 在 rehash 期间,发生查询,删除,更新,添加等行为时,首先去ht[0] 中尝试, 再去 ht[1] 尝试,而添加则直接去 ht[1] 中进行.
4): 在未来某个时间节点上,所有的数据迁移完毕,将 rehashidx 置为 -1, 表示 rehash 完成. ht[1] 设为 ht[0] ,移除 ht[0]
总结
字典用哈希表来进行实现,每个字段有持有两张哈希表,用来伸缩当前的表格达到加快效率的目的.
哈希表通过链表来解决哈希冲突,当负载因子满足条件时,会进行 rehash 操作.
rehash 行为是渐进式的, 会逐步进行数据迁移.