Redis中字典dict的rehash和渐进式rehash

上篇: redis底层是怎样保存一个key和value的?

字典的作用

redis的字典是一个用于维护key和value映射关系的数据结构,与Java中的Map类似。在字典中维护了两个存放数据指针的hash表。同时字典中维护了对hash表的扩容,缩容,key的hash算法等操作。

typedef struct dict {
    dictType *type;  
    void *privdata;   //私有数据指针(privdata)由调用者在创建dict的时候传进来,在dictType的某些操作被调用时会传回给调用者, 
    dictht ht[2];	  //底层用来存数据的hash表 2个
    long rehashidx;   // rehash的索引。 渐进式rehash时用到	
    int16_t pauserehash; //暂停rehash的标志。 >0表示已暂停。 <0表示编码错误 =0表示正在rehash
} dict;

在dict中,ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。

dictht的定义:

typedef struct dictht {  
      // 哈希表数组
      dictEntry **table;
  
      // 哈希表大小
      unsigned long size;
  
      // 哈希表大小掩码,用于计算索引值
     // 总是等于 size - 1
     unsigned long sizemask;
 
     // 该哈希表已有节点的数量
     unsigned long used;
 
} dictht;

table 属性是一个数组, 数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。

size 属性记录了哈希表的大小, 也即是 table 数组的大小

sizemask 属性的值总是等于 size-1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。(不是很清楚,为什么要单独定义一个mask,而不直接size-1);

而 used 属性则记录了哈希表目前已有节点(键值对)的数量。

字典的rehash

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

也就是我们常说的,扩容,再次hash。

Redis rehash过程:

为字典的 ht[1] 哈希表分配空间。一般为原字典的两倍,即 ht[0] * 2;
将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面
当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
但其实rehash是非常的耗时间的。假设ht[0]非常的大呢? 40W,400W,甚至4000W呢?

一次rehash甚至可能导致redis宕机,所以出现了渐进式hash。

渐进式Rehash

这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1] 。

为 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 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0]里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

关于网上对这部分讨论更详细的文章参考:Redis系列:底层数据结构之字典

你可能感兴趣的:(#,redis系列,redis)