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,这个过程分为三步:
这个过程看起来跟简单,但是数据迁移存在很大的问题,如果ht1的数量非常大,那么在迁移至ht2的时候,会因为大量数据的拷贝造成redis阻塞,无法服务其他请求。
5. 渐进式rehash
为了避免 rehash 在数据迁移过程中,因拷⻉数据的耗时,影响 redis 性能的情况,所以 redis 采⽤了渐进式 rehash,也就是将数据的迁移的⼯作不再是⼀次性迁移完成,⽽是分多次迁移。
6. rehash触发条件
rehash 的触发条件跟负载因⼦(load factor)有关系 负载因⼦可以通过下⾯这个公式计算负载因子 = 哈希表已保存的节点数量/哈希表大小
触发 rehash 操作的条件,主要有两个