你知道的越多,不知道的就越多,业余的像一棵小草!

成功路上并不拥挤,因为坚持的人不多。

编辑:业余草

zhouwenxing.github.io

推荐:https://www.xttblog.com/?p=5193

前言

Redis 是一个键值对数据库,其键是通过哈希进行存储的。整个 Redis 可以认为是一个外层哈希,之所以称为外层哈希,是因为 Redis 内部也提供了一种哈希类型,这个可以称之为内部哈希。当我们采用哈希对象进行数据存储时,对整个 Redis 而言,就经过了两层哈希存储。

哈希对象

哈希对象本身也是一个 key-value 存储结构,底层的存储结构也可以分为两种:ziplist(压缩列表) 和 hashtable(哈希表)。这两种存储结构也是通过编码来进行区分:

hashtable

Redis 中的 key-value 是通过 dictEntry 对象进行包装的,而哈希表就是将 dictEntry 对象又进行了再一次的包装得到的,这就是哈希表对象 dictht

typedef struct dictht {dictEntry **table;//哈希表数组unsigned long size;//哈希表大小unsigned long sizemask;//掩码大小,用于计算索引值,总是等于size-1unsigned long used;//哈希表中的已有节点数
} dictht;

注意:上面结构定义中的 table 是一个数组,其每个元素都是一个 dictEntry 对象。

字典

字典,又称为符号表(symbol table),关联数组(associative array)或者映射(map),字典的内部嵌套了哈希表 dictht 对象,下面就是一个字典 ht 的定义:

typedef struct dict {dictType *type;//字典类型的一些特定函数void *privdata;//私有数据,type中的特定函数可能需要用到dictht ht[2];//哈希表(注意这里有2个哈希表)long rehashidx; //rehash索引,不在rehash时,值为-1unsigned long iterators; //正在使用的迭代器数量
} dict;

其中 dictType 内部定义了一些常用函数,其数据结构定义如下:

typedef struct dictType {uint64_t (*hashFunction)(const void *key);//计算哈希值函数void *(*keyDup)(void *privdata, const void *key);//复制键函数void *(*valDup)(void *privdata, const void *obj);//复制值函数int (*keyCompare)(void *privdata, const void *key1, const void *key2);//对比键函数void (*keyDestructor)(void *privdata, void *key);//销毁键函数void (*valDestructor)(void *privdata, void *obj);//销毁值函数
} dictType;

当我们创建一个哈希对象时,可以得到如下简图(部分属性被省略):

redis中的哈希对象

rehash 操作

dict 中定义了一个数组 ht[2]ht[2] 中定义了两个哈希表:ht[0]ht[1]。而 Redis 在默认情况下只会使用 ht[0],并不会使用 ht[1],也不会为 ht[1] 初始化分配空间。

当设置一个哈希对象时,具体会落到哈希数组(上图中的 dictEntry[3])中的哪个下标,是通过计算哈希值来确定的。如果发生哈希碰撞(计算得到的哈希值一致),那么同一个下标就会有多个 dictEntry,从而形成一个链表(上图中最右边指向 NULL 的位置),不过需要注意的是最后插入元素的总是落在链表的最前面(即发生哈希冲突时,总是将节点往链表的头部放)。

当读取数据的时候遇到一个节点有多个元素,就需要遍历链表,故链表越长,性能越差。为了保证哈希表的性能,需要在满足以下两个条件中的一个时,对哈希表进行 rehash(重新散列)操作:

  • 负载因子大于等于 1dict_can_resize1 时。

  • 负载因子大于等于安全阈值(dict_force_resize_ratio=5)时。

PS:负载因子 = 哈希表已使用节点数 / 哈希表大小(即:h[0].used/h[0].size)。

rehash 步骤

扩展哈希和收缩哈希都是通过执行 rehash 来完成,这其中就涉及到了空间的分配和释放,主要经过以下五步:

  1. 为字典 dictht[1] 哈希表分配空间,其大小取决于当前哈希表已保存节点数(即:ht[0].used):

  • 如果是扩展操作则 ht[1] 的大小为 2 的 n 次方中第一个大于等于 ht[0].used * 2 属性的值(比如 used=3,此时ht[0].used * 2=6,故 23 次方为 8 就是第一个大于 used * 2 的值(2 的 2 次方 < 6 且 2 的 3 次方 > 6))。`

  • 如果是收缩操作则 ht[1] 大小为 2 的 n 次方中第一个大于等于 ht[0].used 的值。

  • 将字典中的属性 rehashix 的值设置为 0,表示正在执行 rehash 操作。

  • ht[0] 中所有的键值对依次重新计算哈希值,并放到 ht[1] 数组对应位置,每完成一个键值对的 rehash之后 rehashix 的值需要自增 1

  • ht[0] 中所有的键值对都迁移到 ht[1] 之后,释放 ht[0] ,并将 ht[1] 修改为 ht[0],然后再创建一个新的 ht[1] 数组,为下一次 rehash 做准备。

  • 将字典中的属性 rehashix 设置为 -1,表示此次 rehash 操作结束,等待下一次 rehash

  • 渐进式 rehash

    Redis 中的这种重新哈希的操作「因为不是一次性全部 rehash,而是分多次来慢慢的将 ht[0] 中的键值对 rehashht[1],故而这种操作也称之为渐进式 rehash。渐进式 rehash 可以避免集中式 rehash 带来的庞大计算量,是一种分而治之的思想。

    在渐进式 rehash 过程中,因为还可能会有新的键值对存进来,此时** Redis 的做法是新添加的键值对统一放入 ht[1] 中,这样就确保了 ht[0] 键值对的数量只会减少**。

    当正在执行 rehash操作时,如果服务器收到来自客户端的命令请求操作,则「会先查询 ht[0],查找不到结果再到ht[1] 中查询」

    ziplist

    关于 ziplist 的一些特性,之前的文章中有单独进行过分析,想要详细了解的,可以看这篇文章《从根上理解ziplist为什么要牺牲速度而进行压缩!》。但是需要注意的是哈希对象中的 ziplist 和列表对象中 ziplist 的有一点不同就是哈希对象是一个 key-value 形式,所以其 ziplist 中也表现为 key-valuekeyvalue 紧挨在一起:

    redis中的ziplist

    ziplist 和 hashtable 的编码转换

    当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用 ziplist 编码来进行存储:

    • 哈希对象中的所有键值对总长度(包括键和值)小于等于 64字节(这个阈值可以通过参数 hash-max-ziplist-value 来进行控制)。

    • 哈希对象中的键值对数量小于等于 512 个(这个阈值可以通过参数 hash-max-ziplist-entries 来进行控制)。

    一旦不满足这两个条件中的任意一个,哈希对象就会选择使用 hashtable 编码进行存储。

    哈希对象常用命令

    • hset key field value:设置单个 field(哈希对象的 key 值)。

    • hmset key field1 value1 field2 value2 :设置多个 field(哈希对象的 key 值)。

    • hsetnx key field value:将哈希表 key 中域 field 的值设置为 value,如果 field 已存在,则不执行任何操作。

    • hget key field:获取哈希表 key 中的域 field 对应的 value

    • hmget key field1 field2:获取哈希表 key 中的多个域 field 对应的 value

    • hdel key field1 field2:删除哈希表 key 中的一个或者多个 field

    • hlen key:返回哈希表key中域的数量。

    • hincrby key field increment:为哈希表 key 中的域 field 的值加上增量 incrementincrement 可以为负数,如果 field 不是数字则会报错。

    • hincrbyfloat key field increment:为哈希表 key 中的域 field 的值加上增量 incrementincrement 可以为负数,如果 field 不是 float 类型则会报错。

    • hkeys key:获取哈希表 key 中的所有域。

    • hvals key:获取哈希表中所有域的值。

    了解了操作哈希对象的常用命令,我们就可以来验证下前面提到的哈希对象的类型和编码了,在测试之前为了防止其他 key 值的干扰,我们先执行 flushall 命令清空 Redis 数据库。

    然后依次执行如下命令:

    hset address country china
    type address
    object encoding address
    

    得到如下效果:

    ziplist

    可以看到当我们的哈希对象中只有一个键值对的时候,底层编码是 ziplist

    现在我们将 hash-max-ziplist-entries 参数改成 2,然后重启 Redis,最后再输入如下命令进行测试:

    hmset key field1 value1 field2 value2 field3 value3
    object encoding key
    

    输出之后得到如下结果:

    在这里插入图片描述

    可以看到,编码已经变成了 hashtable

    总结

    本文主要介绍了 Redis5 种常用数据类型中的哈希类型底层的存储结构 hashtable 的使用,以及当 hash 分布不均匀时候 Redis 是如何进行重新哈希的问题,最后了解了哈希对象的一些常用命令,并通过一些例子验证了本文的结论。

面试官:Redis中哈希分布不均匀该怎么办相关推荐

  1. 求职面试时,如何从面试官话语中揣测是否被录用?

    求职面试时,如何从面试官话语中揣测是否被录用? 面试官: 非常感谢您来应聘,我们会尽快联系你,最晚明天下班前给您答复. 基本上可以肯定就是你了,除非遇到特殊情况,HR可能在你没回到家就给你打电话,通知 ...

  2. 阿里面试官Redis把我问到哑口无言…

    Redis在国内各大公司都很热门,比如新浪.阿里.腾讯.百度.美团.小米等.Redis也是大厂面试最爱问的,尤其是Redis客户端.Redis高级功能.Redis持久化和开发运维常用问题探讨.Redi ...

  3. 面试官 | Java中的注解是如何工作的?

    自Java5.0版本引入注解之后,它就成为了Java平台中非常重要的一部分.开发过程中,我们也时常在应用代码中会看到诸如@Override,@Deprecated这样的注解.这篇文章中,我将向大家讲述 ...

  4. 面试官 | SpringBoot 中如何实现异步请求和异步调用?

    作者 | 会炼钢的小白龙 来源 | cnblogs.com/baixianlong/p/10661591.html 一.SpringBoot中异步请求的使用 1.异步请求与同步请求 特点: 可以先释放 ...

  5. 惊艳面试官-Java中关于随机数生成8种方式的思考

    Java中生成随机数常用的有下面这8种写法:简而言之,名称带安全的未必安全,名字简洁的未必简单. Math.random() Random ThreadLocalRandom SecureRandom ...

  6. 面试官:Redis分布式锁解决方案是什么?

    今天博主在这片文章中主要给大家讲下Redis分布式锁的原理以及解决方案 学到三连呦 1.Redis分布式锁原理 1.1.简述 我们知道分布式锁的特性是排他.避免死锁.高可用.分布式锁的实现可以通过数据 ...

  7. Redis中哈希hash数据类型(增加修改(设置单一属性、设置多个属性)、获取(获取键所有属性、获取单一属性值、获取多个属性值)、删除、使用hash可能出现的问题)

    hash⽤于存储对象,对象的结构为属性.值 值的类型为string [应用:如购物车内某个宝贝的所有属性]  [help hset] 1. 增加.修改 1.1 设置单个属性 hset key fiel ...

  8. 面试官这Redis夺命连环12问,谁顶得住?

    面试官这夺命连环12问,谁顶得住? ⏬ 面试官: 同学,我看你每个项目中都用到了Redis,你能说说你是怎样使用Redis的吗? 小A同学: 主要用来做缓存,分布式Session, 阅读量/点赞数统计 ...

  9. 是面试官放水,还是公司太缺人?这都能过,字节跳动原来这么容易进...

    "字节是大企业,是不是很难进去啊?"  "在字节做软件测试,能得到很好的发展吗?  一进去就有9.5K,其实也没有想的那么难" 直到现在,心情都还是无比激动! ...

最新文章

  1. CCRD_TOC_2015_EULAR专刊第二辑
  2. Android工程师面试该怎么准备?终局之战
  3. 数据挖掘的一个完整过程
  4. SAP Spartacus TypeScript源代码中的三个点用法
  5. 2012 Multi-University #8
  6. Spark 1.1.1 Submitting Applications
  7. 无法读取内存属于错误吗_索佳全站仪错误信息讲解
  8. 【严重抗议】主播都是阿里程序猿的直播,他们也是够了!
  9. Leetcode 214.最短回文串
  10. 非标准硬件控制之增加系统API
  11. 百度开源的71款项目
  12. Vim 增加man快捷方式
  13. 学习matlab(六)——微分和积分
  14. jpa Specification fetch查询报错,query specified join fetching, but the owner of the fetched association
  15. OneNote无法登录 遇到临时服务器问题
  16. es启动错误max number of threads [3802] for user [elasticsearch] is too low, increase to at leas
  17. 思科6509 引擎720-3BXL 更换风扇造成设备重启
  18. Tief Meer alt singen nennen Papa.Magnam natus consequuntur corporis laudantium.
  19. 物联计算机大赛,【实践科】关于举办2020“天翼物联杯”中国高校计算机大赛-网络技术挑战赛校内选拔赛的通知...
  20. 鸿蒙系统卸载预装,鸿蒙系统怎么退回安卓 鸿蒙系统怎么卸载

热门文章

  1. 通过pdf的url在线浏览pdf
  2. SEO 和 SEM 的优缺点有哪些区别和优势
  3. maya之坐标轴与模型显示状态
  4. minimax算法及α-β剪枝算法
  5. 【pytest】(六) pytest中fixture的使用
  6. 机器学习、数据挖掘、统计建模的技术担当,20款免费预测分析软件
  7. 微信小程序开发工具调试
  8. 剑指Offe 50:数组中重复的数字
  9. 阿里acp认证是什么 阿里acp认证含金量高吗
  10. 如何在 Ubuntu 和其他 Linux 发行版中启动、停止和重启服务