内容来源于《Redis设计与实现》 http://redisbook.com/preview/dict/incremental_rehashing.html
Redis字典数据结构如下:
typedef struct dict{
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //哈希表数组,类型为dictht,ht[0]为实际存储数据使用,ht[1] 为rehash时使用
int rehashidx; //rehash进度标志,-1 代表当前不在 rehash
} dict
执行 rehash 的步骤如下:
1.为字典的 ht[1]
哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0]
当前包含的键值对数量 (也即是 ht[0].used
属性的值):
- 如果执行的是扩展操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used * 2
的 2^n (2
的n
次方幂); - 如果执行的是收缩操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used
的 2^n
举例:如果ht[0].used
当前的值为 4
, 4 * 2 = 8
, 而 8
(2^3)恰好是第一个大于等于 4
的 2
的 n
次方, 所以程序会将 ht[1]
哈希表的大小设置为 8
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 做准备。
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于
1
; - 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于
5
;
其中哈希表的负载因子可以通过公式:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小 load_factor = ht[0].used / ht[0].size
为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]
里面的所有键值对全部 rehash 到 ht[1]
, 而是分多次、渐进式地将 ht[0]
里面的键值对慢慢地 rehash 到 ht[1]
。
以下是哈希表渐进式 rehash 的详细步骤:
- 为
ht[1]
分配空间, 让字典同时持有ht[0]
和ht[1]
两个哈希表。 - 在字典中维持一个索引计数器变量
rehashidx
, 并将它的值设置为0
, 表示 rehash 工作正式开始。 - 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将
ht[0]
哈希表在rehashidx
索引上的所有键值对 rehash 到ht[1]
, 当 rehash 工作完成之后, 程序将rehashidx
属性的值增一。 - 随着字典操作的不断执行, 最终在某个时间点上,
ht[0]
的所有键值对都会被 rehash 至ht[1]
, 这时程序将rehashidx
属性的值设为-1
, 表示 rehash 操作已完成。