首页 > 数据库 >redis之hash解析

redis之hash解析

时间:2023-06-12 18:12:48浏览次数:48  
标签:rehash hash Redis redis dict 哈希 dictht 解析

Redis底层数据结构之hash

hash是日常开发过程中使用Redis的一个数据结构,其底层实现方式有两种,如下所示。一种是zipList,这种是当hash结构的V值较小的时候使用的编码方式。这个已经在上一篇文章中介绍过了。这篇文章主要讲解一下另外一种实现方式,字典dict,当hash结构的V值较大时采用的编码方式。

1|0  dict#

这里又要开始鞭尸C语言了,字典dict作为一种常用的数据结构,C语言内部并不具备,因而Redis的开发人员自己设计和开发了Redis中的dict结构,其定义如下:

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有几个迭代器正在进行遍历过程

1|1  dictht#

由上面可以看出,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-1used表示dictht中已有的节点数量,其示意图如下所示:

1|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的结构示意图如下所示:

 

上图是一个没有处于rehash状态下的字典dict,整个dict中有两个哈希表dictht,其中一个哈希表存储数据,另一个哈希表为空。

2|0  扩容与缩容#

当哈希表中元素数量逐渐增加时,此时产生hash冲突的概率逐渐增大,且由于dict也是采用拉链法解决hash冲突的,随着hash冲突概率上升,链表会越来越长,这就会导致查找效率下降。相反,当元素不断减少时,元素占用dict的空间就越少,出于对内存的极致利用,此时就需要进行缩容操作。

既然说到扩容和缩容,熟悉Java集合的小伙伴是不是想到了什么。不错,那就是负载因子。负载因子一般用于描述集合当前被填充的程度。在Redis的字典dict中,负责因子=哈希表中已保存节点数量/哈希表的大小,即:

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

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

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

其中,扩容和缩容的数量大小也有一定的规则:

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

3|0  rehash#

Java中的HashMap类似,当Redis中的dict进行扩容或者缩容,会发生reHash过程。JavaHashMaprehash过程如下:新建一个哈希表,一次性将当前所有节点进行rehash然后复制到新哈希表相应的位置上,之后释放掉原有的hash表,而持有新的表,这个过程是一个时间复杂度为O(n)的操作。而对于单线程的Redis而言很难承受这么高时间复杂度的操作,因而其rehash的过程有所不同,使用的是一种称之为渐进式rehash的方式,一点一点地进行搬迁。其过程如下:

  • 假设当前数据在dictht[0]中,那么首先为dictht[1]分配足够的空间,如果是扩容,则dictht[1]大小就按照扩容规则设置;如果是缩减,则dictht[1]大小就按照缩减规则进行设置;
  • 在字典dict中维护一个变量,rehashidx=0,表示rehash正式开始;
  • rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将dictht[0]哈希表在rehashidx索引上的所有键值对rehashdictht[1],当一次rehash工作完成之后,程序将rehashidx属性的值+1。同时在serverCron中调用rehash相关函数,在1ms的时间内,进行rehash处理,每次仅处理少量的转移任务(100个元素);
  • 随着字典操作的不断执行,最终在某个时间点上,dictht[0]的所有键值对都会被rehashdictht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成;

上述就是Redisdict的渐进式rehash过程,但在这个过程会存在两个明显问题。第一,第三步说了,每次对字典执行增删改查时才会触发rehash过程,万一某一时间段并没有任何请求命令呢?此时应该怎么办?第二,在维护两个dictht的时候,此时哈希表如何正常对外提供服务?

Redis的设计人员在设计时就已经考虑到了这两个问题。对于第一个问题,Redis在有一个定时器,会定时去判断rehash是否完成,如果没有完成,则继续进行rehash。定时函数如下所示:

// 服务器定时任务 void databaseCron() { ... if (server.activerehashing) { for (j = 0; j < dbs_per_call; j++) { int work_done = incrementallyRehash(rehash_db);//rehash方法 if (work_done) { /* If the function did some work, stop here, we'll do * more at the next cron loop. */ break; } else { /* If this db didn't need rehash, we'll try the next one. */ rehash_db++; rehash_db %= server.dbnum; } } } }

对于第二个问题,对于添加操作,会将新的数据直接添加到dictht[1]上面,这样就可以保证dictht[0]上的数量只减少不增加。而对于删除、更改、查询操作,会直接在dictht[0]上进行,尤其是这三个操作,都会涉及到查询,当在dictht[0]上查询不到时,会接着去dictht[1]上查找,如果再找不到,则表明不存在该K-V值。

3|1  渐进式rehash的优缺点#

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

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

4|0  思考题#

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

  • BIGSAVE时,dict要是进行扩容,则此时就需要为dictht[1]分配内存,若是dictht[0]的数据量很大时,就会占用更多系统内存,造成内存页过多分离,所以为了避免系统耗费更多的开销去回收内存,此时最好不要进行扩容;
  • 缩容时,结合缩容的条件,此时负载因子<0.1,说明此时dict中数据很少,就算为dictht[1]分配内存,也消耗不了多少资源;

5|0  总结#

 

标签:rehash,hash,Redis,redis,dict,哈希,dictht,解析
From: https://www.cnblogs.com/shoshana-kong/p/17475753.html

相关文章

  • Redis rehash
     Redisrehash是什么?Redisrehash是一种渐进式的哈希表扩展或收缩的机制,用于保持哈希表的负载因子在一个合理的范围内,提高哈希表的性能和空间利用率12。哈希表是Redis的基础数据结构,用于存储键值对。哈希表由一个数组和一个链表组成,数组的每个元素是一个指向链表的指针,链......
  • Redis高可用的三种实现方式
    Redis高可用的三种实现方式一、高可用的概念​高可用(HighAvailability,即HA),指的是通过尽量缩短日常维护操作和突发的系统崩溃所导致的停机时间,以提高系统和应用的可用性。一个业务系统如果全年无一时刻不在提供服务,它的可用性可达100%。那么什么样的系统可以称之为高可用呢,业......
  • 开发一次、运行多端:Weex与小程序容器的卓越优势解析
    Weex是一个跨平台的移动应用开发框架,由阿里巴巴旗下的阿里巴巴前端团队开发。它允许开发者使用单一的代码库来构建同时适用于iOS和Android平台的移动应用。Weex使用基于Vue.js的声明式语法来描述应用程序的界面,并通过JavaScript运行时引擎在移动设备上解析和渲染界面。 Weex......
  • redis的消息发布订阅实现
    文章目录前言一、创建好springboot项目,引入核心依赖二、使用步骤1.自定义一个消息接受类2.声名一个消息配置类3.编写一个测试类总结前言一般项目中都会使用redis作为缓存使用,加速用户体验,实现分布式锁等等,redis可以说为项目中的优化,关键技术实现立下了汗马功劳.今天带来它......
  • 腾讯DNS的缺陷?(人为?)无法解析
    疼讯dns无法解析xiaohongshu.com,小红书(电脑网页现在可以看小红书的)[换openDNS后立即正常]讽刺的是小红书用的服务器还是疼讯云的!!!解析服务如此强大,是否因为过滤审核太多次而误杀了正常网站?   ......
  • redis三:key常用命令
    1.keys*显示所有keyexistsk1k2...有几个就显示几2.typekey显示key的类型 3.delkey删除指定的key4unlinkkey非阻塞删除,del原子的有可能阻塞5.expirekey秒为key设置过期时间ttlkey查看还有多少秒过去,-1永不过期,-2表示已过期 6. redis带着16个库,默认在......
  • 进程在用户态和内核态的区别[独家解析]
     先看基础常识:基础内核在创建进程的时候,会为进程创建相应的堆栈。   每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。 当进程在用户空间运行时,CPU寄存器里面的内容是用户堆栈地址,使用用户栈  当进程在内核空间时,CPU寄存器里面的内容是内核栈......
  • Redis实现分页和多条件模糊查询方案
    简介: 本文将基于Redis提供条件查询+分页的技术解决方案。 导言Redis是一个高效的内存数据库,它支持包括String、List、Set、SortedSet和Hash等数据类型的存储,在Redis中通常根据数据的key查询其value值,Redis没有模糊条件查询,在面对一些需要分页、排序以及条件查询的场景时(如......
  • Java开发 - 让你少走弯路的Redis集群搭建
    前言前文中,我们已经对Redis的单节点哨兵的搭建方式做了演示和测试,相信大家已经了解了怎么操作,虽然是单节点,但基本已经满足了部分公司的日常需要,毕竟Redis集群不是什么项目都适用,用上了Redis,也未必需要使用哨兵,甚至集群。但今天,我们还是要把Redis哨兵集群的搭建方式给大家做个分享,万......
  • redis工具类
    packagecom.yashi.common.utils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.HashOperations;importorg.springframework.data.redis.core.ListOperations;importorg.springframework.data.redis.cor......