字段是以 key-value 形式来存储数据的,c 语言中没有字典的数据结构,所以 Redis 构建了自己的字典实现
1. Redis 字典的用处
- Redis 数据库就是以字典来存储数据
- Redis 的散列表(哈希键) 使用字典来实现的
2. 字典的实现
哈希表的实现
Redis 字典所使用的哈希表由 dict.h/dictht 结构定义
typedf struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}

哈希表节点的实现
哈希表节点使用 dictEntry 结构表示,每个 dictEntry 结构都保存着一个键值对
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int 64_t s64;
}v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;

字典的实现
Redis 中的字典由 dict.h/dict 结构表示
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privatedata;
//哈希表
dictht ht[2];
//rehash 索引
//当 rehash 不在进行时,值为 -1
int trehashidx;
}dict;
- type 属性和 private 属性是针对不同类型的键值对,为创建多态字典而设置的。
- type 属性指向一个 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数
- private 属性保存了需要传给那些类型特定函数的可选参数
- ht[2] 保存了两张哈希表, 0 号哈希表一般正常使用,1 号哈希表只有在 rehash 的时候才会用到
- trehashidx 记录当前 rehash 的进度,如果没有开始 rehash,则 trehashidx 为 -1
3. 哈希算法
将一个键值对添加到字典里面时,需要经过两个步骤计算出键值对应该放置的位置
- 使用 dict 中的 type 里面的 HashFunction 计算出 key 的哈希值
- 用计算出的哈希值与 dict 里面的 ht(散列表) 中的 sizemask 做 &(与)操作,得出键值对应该放的位置(index)
4. 解决键冲突
Redis 用链地址法来解决键冲突。
程序总是将新节点添加到链表的表头位置(头插法)
5. rehash
随着操作的不断进行,哈希表保存的键值对会逐渐增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表大小进行相应 的扩展或者收缩
扩展和搜索通过 rehash 来完成
rehash 步骤如下:
- 为字典 ht[1] 分配空间,分配空间的大小取决于要执行的操作以及当前键值对的数量(ht[0].used 属性的值)
- 如果是扩展操作,那么 ht[1] 分配的空间为 2n, 使 2n >= ht[0].used * 2, 且 n 要最小
- 如果是收缩操作,那么 ht[1] 分配的空间为 2n, 使 2n >= ht[0].used, 且 n 要最小
- 将保存在 ht[0] 上面的键值对 rehash 到 ht[1] 上,rehash 是指重新计算键值对在 ht[1] 上的哈希值和索引值,然后将键值对放到 ht[1] 上
- rehash 完成之后,释放 ht[0] 的空间,将 ht[1] 设置为 ht[0], 并将 ht[1] 的位置设置为一个空的哈希表,为下次 rehash 做准备
6. 哈希表扩展与收缩的条件
哈希表的负载因子 = ht[0].used / ht[0].size
换种方式就是说:哈希表的负载因子等于哈希表平均每个桶下边挂载的节点数量
哈希表的扩展条件:
- 当服务器没有进行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1
- 当服务器在进行 BGSAVE 或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5
哈希表的收缩条件::
- 当哈希表的负载因子小于等于 0.1
7. 渐进式 rehash
rehash 不是一次性执行完的,而是在每次对字典进行 增删改查 的时候就会进行一次 rehash
这样做是为了将 rehash 操作均摊到每一次的增上改查上,避免集中式 rehash 带来的庞大计算量
rehash 的步骤:
- 为 ht[1] 分配空间
- 将字典中的 rehashidx 设置为 0, 表示 rehash 开始
- 在每次对字典进行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表的 rehashidx 索引上的所有键值对 rehash 到 ht[1] 上,之后将 rehashidx 加一
- 当 rehash 执行完的时候,也就是 ht[0] 上的键值对全部转移到 ht[1] 上的时候,程序将 rehashidx 设置为 -1, 表示 rehash 工作已经完成
渐进式 rehash 执行期间的哈希表的操作:
- 字典的删除、查找、更新等操作会在两个哈希表上进行
- 字典的添加操作一律在 ht[1] 哈希表上操作
参考资料
[1].《Redis 设计与实现》