[redis]哈希表怎么实现rehash


1. 哈希表的结构设计

redis的哈希表结构如下:

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

可以看到,哈希表是一个数组,数组的每个元素是一个指向哈希表节点的指针。

2. 哈希冲突

哈希表实际上是一个数组,数组里每一个元素就是一个哈希桶

当一个键值对的键经过hash函数计算后得到哈希值,再将哈希值进行取模运算,得到的结果就是该key对应的数组元素位置,也就是第几个哈希桶

什么时候会产生哈希冲突呢?

举个例子,有一个可以存放8个哈希桶的哈希表。key1经过哈希函数计算后,再将哈希值%8进行取模运算,结果值为1,那么就对应哈希桶1,类似地,key9和jey10分别对应哈希桶1和6

当key1和key9分配到了相同的哈希桶中,就发生了哈希冲突

因此,当有两个以上数量的key被分配到了一个哈希桶中,此时称这些key发生了冲突

3. 链式哈希

redis采用链式哈希来解决哈希冲突

链式哈希局限性也很明显,随着链表?度的增加,在查询这?位置上的数据的耗时就会增加,毕竟链表的查询的时间复杂度是 O(n)  

4. rehash

在哈希表实际使用时,redis定义了一个dict结构体,这个结构体定义了两个哈希表

typedef struct dict {
  ...
  // 两个哈希表,交替使用,用于rehash
  dictht ht[2];
  ...
} dict;

在正常服务请求阶段,插入的数据都会写到ht1中,此时ht2h还没有被分配空间。

随着数据的逐步增多,触发了rehash,这个过程分为三步:

  • 给ht2分配空间,一般会比ht1大2倍
  • 将ht1的数据迁移到ht2中
  • 迁移完成后,ht1将会被释放,并把ht2设置成ht1,然后ht2为下次rehash作准备

这个过程看起来跟简单,但是数据迁移存在很大的问题,如果ht1的数量非常大,那么在迁移至ht2的时候,会因为大量数据的拷贝造成redis阻塞,无法服务其他请求。

5. 渐进式rehash

为了避免 rehash 在数据迁移过程中,因拷?数据的耗时,影响 redis 性能的情况,所以 redis 采?了渐进式 rehash,也就是将数据的迁移的?作不再是?次性迁移完成,?是分多次迁移。
  • 给ht2分配空间
  • 在 rehash 进?期间,每次哈希表元素进?新增、删除、查找或者更新操作时,redis 除了会执?对应的操作之外,还会顺序将ht1中索引位置上的所有 key-value 迁移到ht2上
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间,会把ht1的所有key-value 迁移到ht2,从?完成 rehash 操作

6. rehash触发条件

rehash 的触发条件跟负载因?(load factor)有关系   负载因?可以通过下?这个公式计算
负载因子 = 哈希表已保存的节点数量/哈希表大小
  触发 rehash 操作的条件,主要有两个
  • 当负载因??于等于 1 ,并且 redis 没有在执? bgsave 命令或者 bgrewiteaof 命令,也就是没有 执? RDB 快照或没有进? AOF 重写的时候,就会进? rehash 操作
  • 当负载因??于等于 5 时,此时说明哈希冲突?常严重了,不管有没有有在执? RDB 快照或 AOF 重写,都会强制进? rehash 操作