Skip to content

Redis设计与实现之重哈希原理

1910字约6分钟

哈希

2021-02-28

为什么会产生rehash?

随着操作的不断执行, 哈希表保存的 键值对会逐渐地增多或者减少, 为了让哈希表的 负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的 扩展或者收缩

触发rehash

触发时机

触发自动扩展

  • 服务器目前 没有 在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的 负载因子大于等于 1
  • 服务器目前 正在 执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的 负载因子大于等于 5

触发自动收缩

  • 当哈希表的 负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作

触发扩展负载因子不同原因

根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用 写时复制(copy-on-write) 技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。

【知识点】 负载因子 = 哈希表已保存节点数量 / 哈希表大小,即 : load_factor = ht[0].used / ht[0].size

rehash的过程

rehash基本过程原理

    1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于 要执行的操作 ,以及 ht[0]当前包含的键值对数量 (也即是 ht[0].used 属性的值)
    1. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
    1. 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

【知识点】 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂) 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n

rehash的一个例子

执行rehash之前的字典

执行rehash之前的字典

ht[0].used 当前的值为 4 , 4 * 2 = 8 , 而 8 (2^3)恰好是第一个大于等于 4 的 2 的 n 次方, 所以程序会将 ht[1] 哈希表的大小设置为 8 。

h1字典分配空间后ht1字典分配空间后

ht[0] 包含的四个键值对都 rehash 到 ht[1]

ht0字典迁移到ht1后

完成rehash之后的状态

释放 ht[0] ,并将 ht[1] 设置为 ht[0] ,然后为 ht[1] 分配一个空白哈希表

完成rehash之后

渐进式rehash

为什么是渐进式rehash

如果哈希表里保存的键值对数量 比较大 ,要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会 导致服务器在一段时间内停止服务。为了避免 rehash 对服务器性能造成影响,多次、渐进式地将 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 操作已完成。

渐进式rehash的一个例子

注意在整个 rehash 过程中, 字典的 rehashidx 属性是如何变化的。

准备开始rehash

准备开始rehash

rehash索引0上的键值对

rehash索引0上的键值对

rehash索引1上的键值对

rehash索引1上的键值对

rehash索引2上的键值对

rehash索引2上的键值对

rehash索引3上的键值对

rehash索引3上的键值对

rehash执行完毕

rehash执行完毕

[!TIP|style:flat|label:知识点] 渐进式 rehash 的好处在于它 采取分而治之的方式 , 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。 在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等 操作会在两个哈希表上进行 , 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找. 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会 只减不增, 并随着 rehash 操作的执行而最终变成空表。

参考资料

  • 书籍: 《Redis设计与实现》