开发 数组里面的字典_Redis字典结构与rehash解读

关注公众号:后端技术漫谈,技术之路不迷路~

字典是一种用于保存键值对的抽象数据结构,也被称为查找表、映射或关联表。

在字典中,一个键(key)可以和一个值(value)进行关联,这些关联的键和值就称之为键值对。

抽象数据结构,啥意思?就是可以需要实际的数据结构是实现这个功能。抽象,意味着它这是实现功能的标准,凡是能够完成这些功能的都可以是其实现。

redis的字典

字典作为一种数据结构内置在很多高级编程语言里面,但是redis是基于C语言进行开发的,所以没有内置这种数据结构,redis只能构建自己的字典实现。

字典通常可以由两种底层数据结构组成,分别是线性表(数组)和hash表。而redis一般是采用hash表的方式进行构建

redis字典为啥不用线性表实现

字典基于用线性表实现,如果我这个字典有200个键值对,那么我就开辟一个长度为200的数组对这些元素进行放置。

基于线性表实现的字典的优缺点很明显:

1、实现简单,适用于任意关键码类型。

2、平均检索效率低(线性时间),表长度n比较大时,检索比较耗时。

3、删除操作的效率比较低,不太适合频繁变动的字典。

字典在插入删除上的频繁让线性表无法胜任此任务。

哈希如何实现字典

之前写过一篇文章,关于java中的hashcode解析,有兴趣的读者可以回看下一些经典的hash函数和实现

面试官问我:hashcode 是什么?和equals是兄弟吗?

redis字典所使用的哈希表由dict.h/dictht组成

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

可以看到redis声明了一个结构体,里面由一个哈希表数组,哈希表数组大小的long值,一个用于计算索引的哈希表大小掩码以及已使用的节点数构成。

这个哈希表数组,存放的是哈希节点dicEntry,我们会将key-value键值对给它放进去。

typedef struct dictEntry {
    void *key;  //存放key值
    union {
        void *val;    //存放value值
        uint64_t u64;    //uint64_t整数
        int64_t s64;    //int64_t整数
    }v;
    struct dictEntry *next;    //指向下个哈希表节点,形成链表
}dictEntry;
affa67dde8206a6061e0d7a8d16745ae.png

如图所示就通过next指针来将两个索引相同的键k1和k0连接在一起。

347b7c39b4be6ce7461f2844bf617d8c.png

Redis 中的字典由 dict.h/dict 结构表示:

typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

可以看到字典里有一个长度为2的哈希表数组,那么为啥不是三个四个甚至更多呢?感觉哈希表越多不是效率更快吗?

其实设置2的原因在于,h[0]用于存储,h[1]用于当容量不足时进行扩充,更多的哈希表也用不上,反而可能在扩充时要同步成为性能瓶颈。

字典如何增添一个元素

当要将一个新的键值对加入到字典中的时候,首先要计算这个key的哈希值和索引值,然后再根据这个索引值放入字典中h[0]的索引位置

8abf4b7054e480e20aaf939fc83d585a.png举个例子, 对于图 4-4 所示的字典来说, 如果我们要将一个键值对 k0 和 v0 添加到字典里面, 那么程序会先使用语句:

hash = dict->type->hashFunction(k0);

计算键 k0 的哈希值。

假设计算得出的哈希值为 8 , 那么程序会继续使用语句:

index = hash & dict->ht[0].sizemask = 8 & 3 = 0;

计算出键 k0 的索引值 0 , 这表示包含键值对 k0 和 v0 的节点应该被放置到哈希表数组的索引 0 位置上, 如图 所示。

6e55594681beda7e7925db26d16e28d1.png

什么时候会进行扩容

按照java中hashmap的说法,当负载因子loadFactor>0.75的情况下会进行扩容

在redis中,字典里的哈希会根据以下两种情况进行扩容:

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

其中哈希表的负载因子可以通过公式:

# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

渐进式rehash如何实现

首先要清楚为什么rehash的时候要渐进式。

这就好比去参加高考,肯定是初中毕业后读三年高中,一点点学习高中知识后才可以参加高考,这才可以取得不错的成绩。学习是循序渐进的,hash也要,不然中考完直接去参加高考,这谁顶得住啊。

748b2edcca365ca5c4694ec7843d6944.png

Rehash操作分为两种:

扩展:当负载因子较大时,应该扩大 dictht::size 以降低平均长度,加快查询速度。
收缩:当负载因子较小时,应该减小 dictht::size 以减少对内存的浪费。

当整体的数据量比较少,如百八十个key-value对存储的时候,hash的过程肯定耗时不会很多。但是在生产换镜下,一个数据库下key-value值都是有百万级别的,在进行rehash操作的时候势必会达到秒级别的运算。所以这个hash的过程不是一次性集中的完成,而是分多次渐进式的完成。

以下是哈希表渐进式 rehash 的详细步骤:

  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。74925f1f6498ace613492b933951bdad.gif

rehash的过程中有数据变化怎么办

关于字典的操作无非就是四个,增删改查。

操作类型过程
增加直接将key-value对增加到h[1]中
删除先删除h[0],再删除h[1]
修改直接修改h[1]
查找先在h[0]中查找,查询不到再到h[1]中

这样就能保证redis在h[0]上是只少不多,所有的记录都会被迁移到h[1]上。

如何解决哈希碰撞

这个问题还是我面试腾讯的时候面试官问我的原题。

一开始我说了两个思路,一个是无限增大线性表的容量,一个是采用数组+链表的方式。

面试官:对这两个都是构成hash的方式,但是如果我的两个键值对的hashcode是一样的呢?

我:那就可以将这个hash算法设计的复杂化,比如hash里头再嵌套一层hash,这样碰撞的几率就会变小了。

面试官:这种方法其实也是可以的,那还有没有其他方法呢?

我:....(支支吾吾中)

然后面试就结束了orz

其实还有另一种方法我不知道就是公共溢出区法

建立一个公共溢出区,假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

参考文献:
《redis设计与实现》
https://blog.csdn.net/Time_Limit/article/details/106633269

a95ba059077f4396cead61dfd898aafe.gif

往期精彩文章:

阔别2020  |  我的年度总结

Socket粘包问题的3种解决方案,最后一种最完美!

一条失去where的动态SQL导致的线上故障

一枚程序猿的MacBook M1使用体验

半夜里,有程序从虚拟机里跑出来了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/555075.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatisPlus 学习笔记_MP的AR模式

狂神说 MyBatisPlus 学习笔记 一、快速入门 文档:https://mp.baomidou.com/ 使用第三方组件: 导入对应依赖研究依赖如何配置代码如何编写提高扩展技术能力 步骤: 1、创建数据库 mybatis_plus 2、创建user表 DROP TABLE IF EXISTS user;CREATE…

ajax调用java程序,从微信小程序到鸿蒙JS开发-JS调用Java

除轻量级智能穿戴设备,现鸿蒙支持的手机、汽车、TV、手表、平板等属于富鸿蒙,在JS语言的项目中也有Java模块,并提供了JS跨语言调用Java方法的技术。现需要实现查看商品评论时,统计出长评、中评和短评的比例,这里将评论…

文本删除空行_010 Editor for mac(文本和十六进制编辑器)

为大家带来最新版本的010 Editor for mac,这是一款专业的文本和十六进制编辑器,新版本的010 editor mac版包含了语法突出显示、更多字符集支持、添加了删除行和删除空行命令等新功能,另外修复了各种错误,功能更加全面。010editor …

Mybatis-Plus之四种lambda方式LambdaQueryWrapper,QueryWrapper<实体>().lambda(),LambdaQueryChainWrapper<实体>

Mybatis-Plus之四种lambda方式 lambda四种表达形式 前言 使用了lambda表达式 可以通过方法引用的方式来使用实体字段名的操作&#xff0c;避免直接写数据库表字段名时的错写名字&#xff1b; 一、LambdaQueryWrapper<> /*** lambda 条件构造器* 生成的sql语句 SELECT…

sql怎么修改服务器角色,sql角色服务器的设置

sql角色服务器的设置 内容精选换一换如果您需要对华为云上购买的DDM资源&#xff0c;为企业中的员工设置不同的访问权限&#xff0c;为达到不同员工之间的权限隔离&#xff0c;您可以使用统一身份认证服务(Identity and Access Management&#xff0c;简称IAM)进行精细的权限管…

MyBatis-Plus——字段类型处理器TypeHandler

字段类型处理器&#xff08;TypeHandler&#xff09; 1&#xff0c;准备工作 &#xff08;1&#xff09;MyBatis 中的 TypeHandler 类型处理器用于 JavaType 与 JdbcType 之间的转换&#xff0c;假设我们用户表中有一个联系方式字段&#xff0c;类型为字符串&#xff1a; &am…

额外参数_Pytorch获取模型参数情况的方法

分享人工智能技术干货&#xff0c;专注深度学习与计算机视觉领域&#xff01;相较于Tensorflow&#xff0c;Pytorch一开始就是以动态图构建神经网络图的&#xff0c;其获取模型参数的方法也比较容易&#xff0c;既可以根据其内建接口自己写代码获取模型参数情况&#xff0c;也可…

Mybatis-Plus之逻辑删除

概念 什么是逻辑删除 逻辑删除:假删除。将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录。 数据库实现思路:插入数据时,标记为未删除状态;查询、修改时,只获取未删除状态的数据进行操作;删除时则更新删除状态为已删除…

查看分支编码_MySQL分支数据库MariaDB之CentOS安装教程

MariaDB数据库管理系统是MySQL的一个分支&#xff0c;由MySQL的创始人Michael Widenius主持开发。采用GPL授权许可 MariaDB的目的是完全兼容MySQL&#xff0c;包括API和命令行&#xff0c;在存储引擎方面&#xff0c;使用XtraDB(英语&#xff1a;XtraDB)来代替MySQL的InnoDB。1…

关联规则算法c语言样例及分析_推荐系统总结系列-关联规则算法(四)

基于关联规则的推荐有三种方法&#xff1a;Apriori关联规则算法FP Tree关联规则算法&#xff1b;PrefixSpan关联规则算法&#xff1b;关联规则挖掘推荐算法&#xff1a;关联规则挖掘是一种在大规模交易中识别类似规则关系模式的通用技术&#xff0c;可以应用到推荐系统中。交易…

Mysql - Innodb锁、事务与隔离级别

我们的数据库一般都会并发执行多个事务&#xff0c;多个事务可能会并发的对相同的一批数据进行增删改查操作&#xff0c;可能就会导致脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题&#xff0c;为了解决多事务并发问题&#xff0c;数据库…

语言非递归求解树的高度_算法素颜(11):无死角“盘”它!二分查找树

引言《菜鸟也能“种”好二叉树&#xff01;》一文中提到了&#xff1a;为了方便查找&#xff0c;需要进行分层分类整理。而满足这种目标的数据结构之一就是树。树的叶子节点可以看作是最终要搜寻的目标物&#xff1b;叶子节点以上的每一层&#xff0c;都可以看作是一个大类别、…

Mysql InnoDB存储引擎的锁相关

Mysql InnoDB存储引擎的锁相关 InnoDB下&#xff0c;mysql四个级别隔离下加锁操作 四个级别隔离的写操作都加X锁串行化下读加S锁select … for update, select … lock in share mode 分别加x锁&#xff0c;s锁在需要加锁的场景下&#xff0c;会根据情况使用三种加锁策略&…

显示器尺寸对照表_电脑显示器尺寸对照表一览,教你怎么选择最适合自己的显示器尺寸...

显示小课堂&#xff1a;显示器买大买小谁说了算&#xff1f; [本文来自&#xff1a;www.ii77.com]今天&#xff0c;笔者想和大家讨论一下关于显示器尺寸选择方面的问题。通过这两年显示器行业的发展我们不难看出&#xff0c;现在显示器的尺寸越来越大&#xff0c;三十几吋、四十…

MySQL事务隔离级别理解_解读MYSQL的可重复读、幻读及实现原理

前言 提到事务&#xff0c;你肯定不会陌生&#xff0c;最经典的例子就是转账&#xff0c;甲转账给乙100块&#xff0c;当乙的账户中到账100块的时候&#xff0c;甲的账户就应该减去100块&#xff0c;事务可以有效的做到这一点。 在MySQL中&#xff0c;事务支持实在引擎层实现的…

MySQL 是如何实现四大隔离级别的?

MySQL 是如何实现四大隔离级别的&#xff1f; 在mvcc下&#xff0c;mysql中用到的锁还是共享锁和排他锁么&#xff1f;如果是的话&#xff0c;那么是怎样结合锁和mvcc来实现rc和rr隔离级别的呢&#xff1f;还有mysql中在ru隔离级别下&#xff0c;两个事务同时读取数据对象A&am…

Linux命令 移动/复制文件/目录到指定目录下

1、同一个服务器下复制文件或文件夹 1.1 复制文件 复制文件&#xff1a;把1.txt 复制到根目录下的sbin目录 cp 文件名&#xff08;可带路径&#xff09;目标路径&#xff08;带路径&#xff09;如&#xff1a;cp 1.txt ~/sbin/1,2 复制目录 复制目录&#xff1a;把relea…

c mysql web开发实例教程_Web开发(六)MySql

数据库简介数据库(DB)数据库(database&#xff0c;DB)是指长期存储在计算机内的&#xff0c;有组织&#xff0c;可共享的数据的集合。数据库中的数据按一定的数学模型组织、描述和存储&#xff0c;具有较小的冗余&#xff0c;较高的数据独立性和易扩展性&#xff0c;并可为各种…

Git——工作中使用命令详解

1、Linux常用命令 cd&#xff1a;改变目录cd…&#xff1a;返回上级目录pwd&#xff1a;显示当前目录clear&#xff1a;清屏ls&#xff1a;显示当前目录所有文件touch&#xff1a;添加文件rm&#xff1a;删除文件mkdir&#xff1a;新建文件夹rm -r&#xff1a;删除文件夹mv&am…

Java中常见null简析

对于每一个Java程序员来说,null肯定是一个让人头痛的东西,今天就来总结一下Java中关于null的知识。 1.null不属于任何类型,可以被转换成任何类型,但是用instanceof永远返回false. 2.null永远不能和八大基本数据类型进行赋值运算等,否则不是编译出错,就是运行出错. 3.null可以…