Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

Stella981
• 阅读 640

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

在字典中,一个 键 (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;

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

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

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

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]的索引位置

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

举个例子, 对于图 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 位置上, 如图 所示。

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

什么时候会进行扩容

按照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也要,不然中考完直接去参加高考,这谁顶得住啊。

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

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 操作已完成。

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]用以存储发生冲突的记录。

转自公众号: JerryCodes

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!

Redis面试:八问字典内部构造与rehash,这谁顶的住啊!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Bill78 Bill78
3年前
Python 字典常用方法总结
Python字典可存储任意类型对象,如字符串、数字、元组……优点:取值方便,速度快1,创建字典字典由键(key)和对应值(value)成对组成。字典也被称作关联数组或哈希表。基本语法如下:dict{'Alice':'2341','Beth':'9102','Cecil':'3258'}注意:每个键与值用冒号隔开(:)
陈占占 陈占占
2年前
python 字典
字典(dict):以键值对的方式存在,以大括号为标志、在字典里面键是不能修改的,值可以修改语法格式:字典名{key1:value1,key2:value2,.......}note:是无序的类型,建必须唯一,值不必。索引是以键为下标,不能索引键对应的值,键不能为列表特点:1.键值之间必须用冒号(:)隔开2.项与项之间必须用逗号(,)隔开3.字典中的键必须
Stella981 Stella981
3年前
Nginx + lua +[memcached,redis]
精品案例1、Nginxluamemcached,redis实现网站灰度发布2、分库分表/基于Leaf组件实现的全球唯一ID(非UUID)3、Redis独立数据监控,实现订单超时操作/MQ死信操作SelectPollEpollReactor模型4、分布式任务调试Quartz应用
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
ThinkPHP 根据关联数据查询 hasWhere 的使用实例
很多时候,模型关联后需要根据关联的模型做查询。场景:广告表(ad),广告类型表(ad\_type),现在需要筛选出广告类型表中id字段为1且广告表中status为1的列表先看关联的设置部分 publicfunctionadType(){return$thisbelongsTo('A
菜园前端 菜园前端
1年前
什么是字典?
原文链接:什么是字典?与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储。在ES6中新增了Map字典。实现功能delete删除元素clear清空所有元素set添加/覆盖元素get查找/返回元素的值has判断是否包含某个元素应用场景1.
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这