三、Redis五种常用数据结构-Hash

Hash是redis中常用的一种无序数据结构。结构类似HashMap。
具体结构如下:key field value

1、优缺点

1.1、优点

  • 同类数据归类整合储存,方便数据管理。
  • 相比于string操作消耗内存和CPU更小。
  • 分字段存储,节省网络流量。

1.2、缺点

  • 过期时间无法设置在field上,只能设置在key上
  • redis集群下不适合大规模使用

2、Hash底层结构

2.1、ziplist-压缩列表

2.1.1、使用条件
  • 哈希对象存储的键值对个数小于512个
  • 哈希对象存储的键值对的键和值的字符串长度小于64字节
2.1.2、数据结构

见list

2.1.3、ziplist的优点
  • 为什么不直接使用hashtable?

相比于hashtable,ziplist结构少了指针,减少了内存的使用。在redis中内存是非常珍贵的。

  • 为什么不使用linkedlist?

ziplist存储时内存地址分配是连续,查询更快。

2.2、dict-字典

字典是在hash存储的数据不满足ziplist中的两个任意一个条件时,使用的数据结构。
由于dict是一种常用的数据结构,但是c语言并不具备此种数据结构,因此redis开发人员自己设计和开发了redisdict结构。详细结构如下:

typedf struct dict{
    dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储
    void *private;//私有数据
    dictht ht[2];//两张hash表 
    int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
    unsigned long iterators; //正在迭代的迭代器数量
}dict;
  • typeprivate这两个属性是为了实现字典多态而设置额,当字典中存放着不同类型的值,对应的复制、比较函数也是不一样,这两个字段组合起来可以实现多态的方法调用。
  • ht[2],两个hash
  • rehashidx,辅助变量,用于记录rehash过程的进度,以及是否正在进行rehash等信息。当此值为-1时表示该dict没有进行rehash操作。
  • iterators,记录此时dict有几个迭代器正在进行遍历过程。
2.2.1、dictht-哈希表

dict结构上可以看出,dict实际上就是对dictht的操作,dictht的具体结构如下:

typedf struct dictht{
    dictEntry **table;//存储数据的数组 二维
    unsigned long size;//数组的大小
    unsigned long sizemask;//哈希表的大小的掩码,用于计算索引值,总是等于//size-1
    unsigned long used; 哈希表中中元素个数
}dictht;
  • table是一个dictEntry类型的数组,用户真正存储数据。
  • size表示**table这个数组的大小。
  • sizemask用于计算索引的位置,总是等于size-1
  • used表示**table数组中已有的节点个数。
2.2.2、dictEntry

上面分析dictht实际存储数据的是dictEntry数组,其结构定义如下:

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

整个dict字典的结构示意图如下:
image.png

2.2.3、扩容与缩容

当哈希表的数量主键增大时,此时添加数据,产生hash冲突的概率主键增大,且dict也是采用拉链法解决hash冲突的,因此随着hash冲突的增加,链表的长度也在逐渐增大。这时查询的速度会随着链表的长度主键变慢。相反,当元素主键减少时,元素占用dict的空间逐渐减少,处于对内存的极致利用,此时就需要进行缩容操作。
dict的扩容和缩容操作有一点和Java中的HashMap结构类似,都有负载因子。负载因子一般用于表示集合当前被数据填充的程度。在Redis的字典dict中,负载因子=哈希表已存节点数量/哈希表长度,即:

load factor=ht[0].used/ht[0].size

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

  • 没有执行BGSAVEBGREWRITEAOF指令的情况下,哈希表的负载因子大于等于1时进行扩容。
  • 正在执行BGSAVEBGREWRITEAOF指令的情况下,哈希表的负载因子大于等于5时进行扩容。
  • 负载因子小于0.1时,Redis自动对哈希表进行缩容操作。

Redis扩容和缩容的数量规则:

  • 扩容后:扩容后的dictEntry数组数量为第一个大于等于ht[0].used*22^n;
  • 缩容后:缩容后的dictEntry数组数量为第一个大于等于ht[0].used2^n;
2.2.4、rehash

Redis的扩容或者缩容,与Java中的HashMap类似都有rehash过程。Java中的HashMaprehash过程如下:

  1. 新建一个哈希表,一次性将当前的数据全部rehash,然后复制到新的哈希表上。
  2. 舍弃掉原来的哈希表,而持有新的hash表。这个过程是一个时间复杂度为O(n)的操作。

对于单线程的Redis而言很难承受这么高的时间复杂度的操作。因此Redisrehash操作相比较于HashMap有所不同。Redis采用渐进式rehash的方式。其过程如下:

  1. 假设当前数据在ht[0]上,那么首先会为ht[1]分配到足够的空间。如果是扩容ht[1]就按照扩容规则进行设置。如果是缩容ht[1]就按照缩容规则设置。
  2. dict结构中有个rehashidx字段,用来记录rehash的位置。**rehash=0,**表示rehash开始。
  3. rehash进行期间,每次对字典进行添加、删除、查找、更新操作时,除了执行指定的操作外,还会顺带将ht[0]哈希表上的数据rehashht[1]上,每rehash一个ht[0]上的数据到ht[1]rehashidx都会加1。每次顺带的rehash操作只会搬移少量的数据(100个元素)。
  4. 随着字典操作的不断进行,在某个时刻,ht[0]上的所有数据全部被rehashht[1]上,这时rehashidx的值为-1,表示rehash的操作已完成。

以上就是Redis中的dict渐进式rehash过程,但是这个过程存在两个问题:

  1. 在第三步说了,每次在对字典执行增删改查时才会触发rehash过程。万一某一时间段,一直都没有请求怎么办?

A:Redis中有一个定时器,会定时去判断rehash是否完成,如果没有完成,则继续进行rehash操作。

  1. rehash过程中维护两个hash表,是如何对外提供服务的?

A:对于添加操作,会将数据直接添加到ht[1]上,这样就会保证ht[0]上的数据只会减少不会增加。而对于**删除、更改、查询操作。**会直接在ht[0]上进行操作,尤其这三个操作都会涉及到查询,当在**ht[0]**上查询不到时,会接着去**ht[1]**上查找,如果在找不到,则表示此key不存在。

2.2.5、渐进式rehash的优缺点
  • 优点:采用分而治之的思想,将rehash操作分散到每一个对该哈希表的操作上以及定时函数上,避免了集中式的rehash带来的性能压力。
  • 缺点:在rehash期间内,需要保存两个hash表,对内存的占用稍大,而且如果在redis服务器内存满了的时候,突然进行**rehash**操作,会造成大量key被抛弃
2.2.6、思考题

为什么扩容时需要考虑BGSAVE的影响,而缩容时不需要?

  • BGSAVE时,dict进行扩容,则此时就需要为ht[1]分配内存,若是ht[1]的数据量很大时,就会占用更多的系统内存,造成内存页过多分离,所以为了避免系统耗费更多的开销去回收内存,此时最好不要进行扩容。
  • 缩容时,结合缩容的条件,此时负载因子<0.1,说明此时的dict中的数据很少,就算为ht[1]分配内存,也消耗不了多少资源。
  • 13
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值