Redis底层数据结构之Hash

1.概述

Redis当中的hash类似于java当中的HashMap,但也存在着很多的区别。在Redis当中,hash有两种底层实现方式。

  • 当数据量较小的时候,采用zipList作为hash的底层实现,在上一篇该系列的博客当中详细介绍了zipList的实现。
  • 另一种方式是使用字典dict来实现的。

本篇博客主要介绍dict的实现方式。

2.数据结构定义

2.1 字典dict

dict的结构定义如下:

typedf struct dict{
    
    
    dictType *type;//和特定类型键值对相关的函数;
    void *privdata;//上述特定函数的可选参数;
    dictht ht[2];//两张hash表 
    int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
    unsigned long iterators; //正在迭代的迭代器数量
}dict;
  • type 是一个指向 dict.h/dictType 结构的指针,保存了一系列用于操作特定类型键值对的函数;
  • privdata 保存了需要传给上述特定函数的可选参数;
  • ht[2] 两个hash表,使用两个hash表的作用之后会说明。
  • rehashidx 用于标记rehash的进度,若当前这个值为-1,则表示字典没有在执行rehash操作。
  • Iterators 记录正在迭代的迭代器的数量。

对于字典的结构有了初步印象之后,里面比较关键的dictht hash表的结构是怎样的呢?继续往下看!

2.2 哈希表ditcht

hash表的结构定义如下:

typedf struct dictht{
    
    
    dictEntry **table;//存储数据的数组 二维
    unsigned long size;//数组的大小
    unsigned long sizemask;//哈希表的大小的掩码,用于计算索引值,总是等于size-1
    unsigned long used; 哈希表中中元素个数
}dictht;
  • table 是一个二维数组,第一维度数组表示hash表的槽位,第二个维度是每一个槽对应的链表。因为是采用拉链法来解决冲突的,所以存在相同槽位的数据,会以链表的形式连接在一起。
  • size 表示数组的大小,也就是槽位的数量。
  • sizemask 哈希表的大小的掩码,用于计算索引值。
  • used 记录hash表中实际存放元素的个数。

二维数组的结构又是如何定义的呢?

2.3 真正的存储结构dictEntry

上面提到的二维数组dictEntry,是真正的存储key-value键值对的地方,结构定义如下:

typedf struct dictEntry{
    
    
    void *key;//键
    union{
    
    
        void val;
        unit64_t u64;
        int64_t s64;
        double d;
    }v;//值
    struct dictEntry *next;//指向下一个节点的指针
}dictEntry;

key表示键,v表示值

next是指向下一个结点的指针,因为这里的hash表是通过拉链法来解决冲突的。

下面是完整的dict字典的结构图

在这里插入图片描述

3. 扩容和缩容

hash表的扩容是为了减少hash冲突的概率,当hash表中的数据逐渐增多的时候,会导致冲突的概率增大,从而导致每个槽位下的链表的长度会变长。那么就会影响到查询的效率了。

而hash表的缩容是为了减少空间的消耗。Redis的数据是保存在内存当中的,若一个hash表占用很大的空间,里面的数据却很少,那么是极度的浪费,所以需要缩容操作。

在Java当中HashMap有负载因子0.75,当hash表当中实际的元素是hash表槽位的0.75倍的时候,会发生扩容。同样的,在Redis当中,也有负载因子的概念,计算公式是hash表中已保存结点的数量/hash表的长度。

也就是上面结构中提到的 factor = ht[0].used / ht[0].size

Redis中,有三条关于扩容和缩容的规则:

  • 没有执行BGSAVE和BGREWRITEAOF指令的情况下,hash表的负载因子大于等于1的时候进行扩容。
  • 正在执行BGSAVE和BGREWRITEAOF指令的情况下,hash表的负载因子大于等于5的时候进行扩容。
  • 负载因子小于0.1的时候,Redis自动开始对Hash表进行缩容操作。

而不管缩容还是扩容,大小是有规定的,如下:

  • 扩容:扩容后的dictEntry数组长度为第一个大于等于 ht[0].used * 2 的 2^n

    也就是第一个大于等于已使用数量的两倍的2的幂次方。

  • 缩容:缩容后的dictEntry数组长度为第一个大于等于 ht[0].used 的 2^n

    也就是第一个大于等于已使用数量的2的幂次方。

4. rehash

第三节介绍了为何需要扩容缩容、扩容缩容的规则以及时机。下面将介绍扩容和缩容的过程,在Redis当中在扩容和缩容的时候,会执行rehash。

对比Java当中的HashMap的rehash,java当中需要新建一个hash表,然后一次性的将旧表里的数据进行rehash到新的hash表当中,之后在释放掉原油的hash表。而这一过程的时间复杂度达到了O(n)。

但是Redis是使用单线程的方式来执行请求命令的,所以无法接受一个时间复杂度为O(n)的操作,所以这就需要渐进式rehash

渐进式,顾名思义也就是一步一步的进行rehash,将一个完整的rehash的过程给拆成多次取执行。

下面是完整的rehash的过程

  • 首先当需要扩容或者缩容的时候,会根据上面提到的规则,在dictht[1]当中分配足够的空间。
  • 然后在dict当中维护一个变量,也就是前面提到的rehashidx,用于标记rehash的进度,将其初始化为0,表示rehash正式开始。
  • rehash进行期间,每次对字典执行添加、删除、查找或者更新操作的时候,除了执行指定的操作之外,还会顺带将dictht[0] hash表当中在rehashidx索引上的所有键值对进行rehash到dictht[1]当中,当一次rehash工作完成之后,会将rehashidx的值+1。
  • 同时在循环时间事件serverCron当中,会调用rehash相关函数,在1ms的时间内,进行rehash处理,每次仅处理少量的转移任务(100个元素)
  • 随着字典操作的不断执行,最终在某个时间点上,dictht[0]当中所有的键值对都会被rehash到dictht[1]当中,此时将rehashidx属性值设置为-1,表示rehash操作已经完成,将dictht[0]重新赋值dictht[1],接着清空dictht[1]。

想必在这个rehash过程当中,还有很多的疑问,rehash过程中,如何执行的增删改查呢?

  • 增加操作:直接将数据添加到dictht[1]当中
  • 修改操作:首先寻找数据在不在dictht[0]当中,若存在,则修改,否则去dictht[1]当中去找,若存在则修改。
  • 删除操作:和上面修改一样,先要定位到元素所在哪个hash表当中,然后执行删除操作。
  • 查找操作:和上面寻找的过程一样。

上面增删改查的操作,保证了dictht[0]当中的数据只会减少不增加,最终就没有数据了。

渐进式rehash的优缺点

优点:采用了分而治之的思想,将 rehash 操作分散到每一个对该哈希表的操作上以及定时函数上,避免了集中式rehash 带来的性能压力。

缺点:在 rehash 的时间内,需要保存两个 hash 表,对内存的占用稍大,而且如果在 redis 服务器本来内存满了的时候,突然进行 rehash 会造成大量的 key 被抛弃

5. hash相关指令

5.1 hset/hsetnx

  • hset

    使用方法:hset hash field value

    将哈希表hash 中键为field的值设置为value

    在这里插入图片描述

    若当前hash表不存在,那么会新建一个哈希表并执行hset操作。返回1

    若field键值对已经存在于哈希表中,新的value会覆盖旧值。返回0

  • hsetnx

    使用方法:hsetnx hash field value

    和上面的区别在于,仅当field尚未存在于哈希表当中,将它的值设置为value

    在这里插入图片描述

    若给定的field已经存在于哈希表当中,则不进行覆盖,返回0.否则执行设置键值对返回1.

5.2 hget

使用方法:hget hash field

返回哈希表hash当中 键为field的值。

不存在返回nil

在这里插入图片描述

5.3 exists

使用方法:exists hash field

和上面的类似,只是这里是判断哈希表hash中是否存在键field

存在返回1 不存在返回0

在这里插入图片描述

5.4 hdel

使用方法:hdel hash field [field …]

删除哈希表hash当中一个或多个指定键的键值对,不存在的键会被忽略掉。返回删除成功的键值对的数量。

在这里插入图片描述

5.5 hlen

使用方法:hlen hash

返回哈希表hash当中 键值对的数量

在这里插入图片描述

5.6 hstrlen

使用方法:hstrlen hash field

返回指定的哈希表hash当中的键为field对应的值的长度。

在这里插入图片描述

5.6 hincrby/hincrbyfloat

  • hincrby

使用方法:hincrby hash field increment

给指定的哈希表hash 中的field键对应的value值加上increment

增加的值可以是负数,相当于执行了减法操作。

  • 若哈希表hash不存在,则会新建一个哈希表并执行命令。

  • 若field不存在,那么在执行命令之前,会讲field对应的值初始化为0,然后执行hincrby

  • 对一个字符串的值执行hincrby 会抛出错误。
    在这里插入图片描述

  • hincrbyfloat

    和上面的区别在于,这里是浮点数的变动。

5.7 hmset

使用方法:hmset hash field value [field value …]

和hset的区别在于,这个指令可以同时讲多个键值对设置到哈希表hash当中。

在这里插入图片描述

5.8 hmget

使用方法:hmget hash field [field …]

返回指定哈希表hash当中键为field的值,可以是多个

若不存在则返回nil

在这里插入图片描述

5.9 hkeys/hvals

  • hkeys

    使用方法 :hkeys hash

    返回哈希表 hash中所有的键
    在这里插入图片描述

  • hvals

    使用方法:hvals hash

    返回哈希表hash中所有键对应的值

    在这里插入图片描述

5.10 hgetall

使用方法:hgetall hash

返回哈希表hash中所有的键值对,键在前值在后的输出

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/110885283