0
点赞
收藏
分享

微信扫一扫

Redis数据结构8:REDIS_HASH

REDIS_HASH

Hash本质上就是一个保存若干键值对的数据结构,类似于Java中的HashMap。

同样的,hash中只能存在一个独一无二的key,所有的操作都围绕key展开。

hash的最大优点在于其可以提供最佳O(1)的查询时间复杂度。

通过一段原始数据key,通过特定算法将其哈希值转化为数组下标,通过相同的算法处理相同的值可以计算相同的索引,所以只需要O(1)时间复杂度就可以查询到key。

但有一片阴云一直笼罩在哈希表之上:哈希冲突

哈希冲突

前文提到,哈希表通过算法将key转化为下标,可以做到相同key,一定能有相同的下标值。

但key是无限的,下标值是有限的。无限对有限的映射,则必定有不同的key指向相同的下标,两个key抢占一个下标,就造成了哈希冲突

哈希冲突一般有多重解决方法,Redis采用链式哈希解决。

简单来说,链式哈希遇到哈希冲突,则将数组元素转化为一个链表,通过链表将冲突的元素连接起来。

在链表中,查询时间复杂度为O(n),这也是为什么要说hash最佳时间复杂度为O(1)。

结构设计

typedef struct dictht {
    // hash数组
    dictEntry **table;
    // hash大小
    unsigned long size;  
    // 大小掩码,用于计算索引值
    unsigned long sizemask;
    // 已有的节点数量
    unsigned long used;
} dictht;
typedef struct dictEntry {
    // 键值对中的键
    void *key;
  
    // 键值对中的值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下一个哈希表节点
    struct dictEntry *next;
} dictEntry;

在dictEntry的v中,是一个联合体union,里面存放一个指针,int和double。

这样的好处是如果值是一个整数或者小数,可以直接嵌入在dictEntry中而不需要指针。

rehash操作

typedef struct dict {
    …
    // 两个Hash表,交替使用,用于rehash操作
    dictht ht[2]; 
    …
} dict;

在正常服务请求阶段,插入的数据,都会写入到哈希表 1,此时的哈希表 2 并没有被分配空间。

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

  • 给表 2 分配空间,一般会是表 1 的 2 倍;
  • 将表 1 的数据迁移到表 2 中;
  • 迁移完成后,释放表 1 的空间,并把表 2设置为表 1,然后在表 2 新创建一个空白的表,为下次 rehash 做准备。

但如果表 1 中数据量非常大,那么在迁移操作时会耗费大量计算资源,可能会阻塞Redis正常业务。

为了避免这种情况,Redis提出 渐进式哈希 作为解决方案。

渐进式哈希的核心是将数据迁移操作分步进行,而不是一口气完成。

在给表 2 分配空间后,每次哈希表的元素进行操作(增删改查)时,Redis会将被操作元素索引上的所有元素(因为可能是链表)迁移到表2上

随着操作越来越多,终有一刻表 1 能把所有数据都迁移到表 2 上,完成渐进式哈希操作。

何时处罚rehash

我们首先要明白一个概念:Load Factor(负载因子)。

负载因子 = 哈希表已存节点数 / 哈希表大小

以下两种条件满足一种,哈希表就会进行rehash操作:

  • 当负载因子大于等于 1 ,并且 Redis 没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
举报

相关推荐

0 条评论