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 操作