文章参考 《Redis 设计与实现》黄建宏
字典
在字典中,每个键都是独一无二的,程序可以在字典中根据键查找与之相关联的值,或者通过键来更新和删除值。
字典在 Redis 中的应用相当广泛,比如 Redis 的数据库就是使用字典来作为底层实现的,例如:
redis> SET msg "hello world"
OK
在数据库中创建一个键为 “msg” 值为 “hello world” 的键值对, 这个键值对就保存在数据库的字典里面。
哈希键的底层实现之一也是字典
哈希表
Redis 字典所使用的是哈希表
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size-1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
- table 属性是一个数组, 数组中的每个元素都是指向 dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对
- size 属性记录了 table 数组的大小
- used 属性记录了哈希表目前已有节点的数量
- sizemask 属性的值总是等于 size - 1, 这个属性和哈希值一起决定一个键该被放到 table 数组的哪个索引上面
哈希表节点 dictEntry
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_tu64;
int64_ts64;
} v;
// 指向下个 dictEntry, 形成链表
struct dictEntry *next;
} dictEntry;
- key 属性保存键值对的键, v 属性保存键值对的值,其中键值对的值可以是一个指针,或者是一个 uint64_t 整数 或 int64_t 整数
- next 指向另一个哈希表节点的指针,这个指针将多个哈希值相同的键值对连接在一起, 以此来解决哈希冲突(拉链法)
字典
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为-1
in trehashidx;
} dict;
type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。
ht 属性时一个包含两个项的数组,数组中的每个项都是一个 dictht 哈希表,一般情况下, 字典只使用 ht[0] 的哈希表,ht[1] 哈希表只会在对 ht[0] 进行 rehash 时使用。
rehashidx 属性记录了 rehash 目前的进度,如果目前没有在进行的 rehash, 那么它的值为 -1。
rehash
随着操作的不断执行,哈希表保存的键值对会逐渐增多或减少,为了让哈希表的负载因子 load factor 维持在一个合理的范围内,需要对哈希表进行合理的扩展或者收缩,即通过 rehash (重新散列) 操作来完成
- 为字典 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对数量( ht[0].used 的值 )
- 如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n, 举例, 如果当前 used 为 5, 那么 ht[1] 的大小是16 即 2^4
- 如果执行的是收缩操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 次方
- 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面:rehash 指的是重新计算键的哈希值和索引值然后放到 ht[1] 中
- 当 ht[0] 所有的键值对都迁移到 ht[1] 之后, 释放 ht[0] , 将 ht[1] 设置为 ht[0], 并在 ht[1] 上新创建一个空白哈希表,为下一次 rehash 做准备。
什么情况下会触发 rehash
服务器目前没有在执行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的负载因子 >= 1
服务器目前正在执行 BGSAVE 或 BGREWRITEAOF 命令,并且哈希表负载因子 >= 5
负载因子 = used / size
当负载因子 < 0.1 时,程序自动开始对哈希表执行收缩操作
渐进式 rehash:
rehash动作并不是一次性完成的,防止庞大的计算可能导致服务器在一定时间内停止服务。所以在渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈比表,其中查找会先查找 ht[0], 再查找 ht[1], 添加操作则只对 ht[1] 添加,这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 结束变成空表。