最近,小黑哥的一个朋友出去面试,回来跟小黑哥抱怨,面试官不按套路出牌,直接打乱了他的节奏。

事情是这样的,前面面试问了几个 Java 的相关问题,我朋友回答还不错,接下来面试官就问了一句:看来 Java 基础还不错,Java HashMap 你熟悉吧?

我朋友回答。工作经常用,有看过源码。

我朋友本来想着,你随便来吧,这个问题之前已经准备好了,随便问吧。

谁知道,面试官下面一句:

那好的,我们来聊聊 Redis 字典吧。

直接将他整蒙逼。

小黑哥的朋友由于没怎么研究过 Redis 字典,所以这题就直接回答不知道了。

当然,如果面试中真不知道,那就回答不了解,直接下一题,不要乱答。

不过这一题,小黑哥觉得还是很可惜,其实 Redis 字典基本原理与 HashMap 差不多,那我们其实可以套用这其中的原理,不求回答满分,但是怎么也可以得个及格分吧~

面试过程真要碰到这个问题,我们可以从下面三个方面回答。

  • 数据结构
  • 元素增加过程
  • 扩容

字典数据结构

说起字典,也许大家比较陌生,但是我们都知道 Redis 本身提供 KV 查询的方式,这个 KV 就是其实通过底层就是通过字典保存。

另外,Redis 支持多种数据类型,其中一种类型为 Hash 键,也可以用来存储 KV 数据。

小黑哥刚开始了解的这个数据结构的时候,本来以为这个就是使用字典实现。其实并不是这样的,初始创建 Hash 键,默认使用另外一种数据结构-ZIPLIST(压缩列表),以此节省内存空间。

不过一旦以下任何条件被满足,Hash 键的数据结构将会变为字典,加快查询速度。

  • 哈希表中某个键或某个值的长度大于 server.hash_max_ziplist_value (默认值为 64 )。
  • 压缩列表中的节点数量大于 server.hash_max_ziplist_entries (默认值为 512 )。

Redis 字典新建时默认将会创建一个哈希表数组,保存两个哈希表。

其中 ht[0] 哈希表在第一次往字典中添加键值时分配内存空间,而另一个 ht[1] 将会在下文中扩容/缩容才会进行空间分配。

字典中哈希表其实就等同于Java HashMap,我们知道 Java 采用数组加链表/红黑树的实现方式,其实哈希表也是使用类似的数据结构。

哈希表结构如下所示:

其中 table 属性是个数组, 其中数组元素保存一种 dictEntry 的结构,这个结构完全类似与 HashMap 中的 Entry 类型,这个结构存储一个 KV 键值对。

同时,为了解决 hash 碰撞的问题,dictEntry 存在一个 next 指针,指向下一个dictEntry ,这样就形成 dictEntry 的链表。

现在,我们回头对比 Java 中 HashMap,可以发现两者数据结构基本一致。

只不过 HashMap 为了解决链表过长问题导致查询变慢,JDK1.8 时在链表元素过多时采用红黑树的数据结构。

下面我们开始添加新元素,了解这其中的原理。

元素增加过程

当我们往一个新字典中添加元素,默认将会为字典中 ht[0] 哈希表分配空间,默认情况下哈希表 table 数组大小为 4(DICT_HT_INITIAL_SIZE)。

新添加元素的键值将会经过哈希算法,确定哈希表数组的位置,然后添加到相应的位置,如图所示:

继续增加元素,此时如果两个不同键经过哈希算法产生相同的哈希值,这样就发生了哈希碰撞。

假设现在我们哈希表中拥有是三个元素,:

我们再增加一个新元素,如果此时刚好在数组 3 号位置上发生碰撞,此时 Redis 将会采用链表的方式解决哈希碰撞。

注意,新元素将会放在链表头结点,这么做目的是因为新增加的元素,很大概率上会被再次访问,放在头结点增加访问速度。

这里我们在对比一下元素添加过程,可以发现 Redis 流程其实与 JDK 1.7 版本的 HashMap 类似。

当我们元素增加越来越多时,哈希碰撞情况将会越来越频繁,这就会导致链表长度过长,极端情况下 O(1) 查询效率退化成 O(N) 的查询效率。

为此,字典必须进行扩容,这样就会使触发字典 rehash 操作。

扩容

当 Redis 进行 Rehash 扩容操作,首先将会为字典没有用到 ht[1] 哈希表分配更大空间。

画外音:ht[1] 哈希表大小为第一个大于等于 ht[0].used*2 的 2^2(2的n 次方幂)

然后再将 ht[0] 中所有键值对都迁移到 ht[1] 中。

当节点全部迁移完毕,将会释放 ht[0]占用空间,并将 ht[1] 设置为 ht[0]

扩容 操作需要将 ht[0]所有键值对都 Rehashht[1] 中,如果键值过多,假设存在十亿个键值对,这样一次性的迁移,势必导致服务器会在一段时间内停止服务。

另外如果每次 rehash 都会阻塞当前操作,这样对于客户端处理非常不友好。

为了避免 rehash对服务器的影响,Redis 采用渐进式的迁移方式,慢慢将数据迁移分散到多个操作步骤。

这个操作依赖字典中一个属性 rehashidx,这是一个索引位置计数器,记录下一个哈希表 table 数组上元素,默认情况为值为 -1

假设此时扩容前字典如图所示:

当开始 rehash 操作,rehashidx将会被设置为 0

这个期间每次收到增加,删除,查找,更新命令,除了这些命令将会被执行以外,还会顺带将 ht[0]哈希表在 rehashidx 位置的元素 rehash 到 ht[1] 中。

假设此时收到一个 K3 键的查询操作,Redis 首先执行查询操作,接着 Redis 将会为 ht[0]哈希表上 table 数组第 rehashidx索引上所有节点都迁移到 ht[1] 中。

当操作完成之后,再将 rehashidx 属性值加 1。

最后当所有键值对都 rehashht[1]中时,rehashidx将会被重新设置为 -1。

虽然渐进式的 rehash 操作减少了工作量,但是却带来键值操作的复杂度。

这是因为在渐进式 rehash 操作期间,Redis 无法明确知道键到底在 ht[0]中,还是在 ht[1] 中,所以这个时候 Redis 不得不查找两个哈希表。

以查找为例,Redis 首先查询 ht[0] ,如果没找到将会继续查找 ht[1],除了查询以外,更新,删除也会执行如上的操作。

添加操作其实就没这么麻烦,因为ht[0]不会在使用,那就统一都添加到 ht[1] 中就好了。

最后我们再对比一下 Java HashMap 扩容操作,它是一个一次性操作,每次扩容需要将所有键值对都迁移到新的数组中,所以如果数据量很大,消耗时间就会久。

总结

Redis 字典使用哈希表作为底层实现,每个字典包含两个哈希表,一个平时使用,一个仅在 rehash 操作中使用。

哈希表总的来说,跟 Java HashMap 真的很类似,底层实现也是一个数组加链表数据结构。

最后,当对哈希表进行扩容操作时间,将会采用渐进性 rehash 操作,慢慢将所有键值对迁移到新哈希表中。

其实了解 Redis 字典的其中的原理,再去比较 Java HashMap ,其实可以发现这两者有如此多的相似点。

所以学习这类知识时,不要仅仅去背,我们要了解其底层原理,知其然知其所以然。

帮助资料

  1. https://redisbook.readthedocs.io/en/latest/internal-datastruct/dict.html

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!的更多相关文章

  1. 厉害!这份阿里面试官 甩出的Spring源码笔记,GitHub上已经爆火

    前言 时至今日,Spring 在 Java 生态系统与就业市场上,面试出镜率之高,投产规模之广,无出其右.随着技术的发展,Spring 从往日的 IoC 框架,已发展成 Cloud Native 基础 ...

  2. 阿里面试官用HashMap把我问倒了

    本人是一名大三学生,最近在找暑期实习,其中也面试过两次阿里,一次菜鸟网络部门.一次网商银行部门,当然我都失败了,同时也让我印象很深刻,因此记录了其中一些面试心得,我觉得这个问题很值得分享,因此分享给大 ...

  3. 当阿里面试官问我:Java创建线程有几种方式?我就知道问题没那么简单

    这是最新的大厂面试系列,还原真实场景,提炼出知识点分享给大家. 点赞再看,养成习惯~ 微信搜索[武哥聊编程],关注这个 Java 菜鸟. 昨天有个小伙伴去阿里面试实习生岗位,面试官问他了一个老生常谈的 ...

  4. 100道Java高频面试题(阿里面试官整理)

    我分享文章的时候,有个读者回复说他去年就关注了我的微信公众号,打算看完我的所有文章,然后去面试,结果我后来很长时间不更新了...所以为了弥补一直等我的娃儿们,给大家的金三银四准备了100道花时间准备的 ...

  5. 阿里面试官:字符串在JVM中如何存放?90%的人就真的只回答在哪里存放

    目录: 一道面试题的引出 案例分析 intern 源码分析 总结 1. 一道面试题的引出 在面试BAT这种一线大厂时,如果面试官问道:字符串在 JVM 中如何存放?大多数人能顺利的给出如下答案: 字符 ...

  6. 阿里面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存,我蒙了!

    目录 1. LRU 缓存介绍 2. ConcurrentLinkedQueue简单介绍 3. ReadWriteLock简单介绍 4.ScheduledExecutorService 简单介绍 5. ...

  7. 阿里面试官最喜欢问的21个HashMap面试题

    1.HashMap 的数据结构? A:哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点.当链表长度超过 8 时,链表转换为红黑树. transient Node<K,V>\[\ ...

  8. 阿里面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜

    本文首发于微信公众号:程序员乔戈里 乔哥:首先说说什么是Unicode.码点吧~要想搞懂,这些概念必须清楚 什么是Unicode? 下图来自http://www.unicode.org/standar ...

  9. 阿里技术专家十五问,真题面试刀刀见肉,快来和阿里面试官battle

    引言 2020阿里巴巴专家组出题,等你来答: 题目:如何判断两个链表是否相交 出题人:阿里巴巴新零售技术质量部 参考答案: $O(n^2)$: 两层遍历,总能发现是否相交 $O(n)$: 一层遍历,遍 ...

随机推荐

  1. echarts全国疫情统计可视化地图(第一阶段)

    DBUtil.java package com.helloechart; import java.sql.Connection; import java.sql.DriverManager; impo ...

  2. 用 Python 制作一个艺术签名小工具,给自己设计一个优雅的签名

    生活中有很多场景都需要我们签字(签名),如果是一些不重要的场景,我们的签名好坏基本无所谓了,但如果是一些比较重要的场景,如果我们的签名比较差的话,就有可能给别人留下不太好的印象了,俗话说字如其人嘛,本 ...

  3. Linux中文解决

    中文编码问题 安装中文语言包 locale -a | grep zh 查看是否有中文语言包 local 查看是否 LC_TYPE 为空 在 /etc/profile.d/ 下创建 lc_type.sh ...

  4. Surface Pro 6 遇到的一系列问题

    当屏幕很烫的时候,触摸屏会部分失灵,越烫越明显,但是 Surface Pen 仍然可以使用,建议这个时候关机,等它冷静了再开机 不过不排除更新导致的触控失灵(新的更新没有考虑到老的硬件,微软也许之后永 ...

  5. Weblogic 连接 RMI 服务报错 Connection refused

    WebLogic 连接 RMI 服务报错 Connection refused 访问 WebLogic RMI 服务报错,连接被拒绝,连接超时. 奇怪的是,报错的 host 根本不是我要访问的. 报错 ...

  6. Java—时间的原点 计算时间所使用的 Date类/DateFormat类/Calendar类

    Date类 类 Date 表示特定的瞬间,精确到毫秒. 毫秒概念:1000毫秒=1秒 毫秒的0点: System.currentTimeMillis()  返回值long类型参数 用于获取当前日期的毫 ...

  7. 将Asp.Net Core3.1项目,使用Docker 部署到Centos 8

    一.准备工具 Win 10 Centos 8 Visual Studio 2019 Docker Desktop  下载地址:https://download.docker.com/win/stabl ...

  8. C#LeetCode刷题之#237-删除链表中的节点(Delete Node in a Linked List)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3832 访问. 请编写一个函数,使其可以删除某个链表中给定的(非末 ...

  9. 初识ABP vNext(1):开篇计划&基础知识

    目录 前言 开始 审计(Audit) 本地化(Localization) 事件总线(Event Bus) 多租户(multi-tenancy technology) DDD分层 实体(Entity) ...

  10. SpringBoot --- 自定义 Starter

    SpringBoot --- 自定义 Starter 创建 1.需要创建一个新的空工程 2.新的工程需要引入两个模块 一个Maven 模块 作为启动器 一个SpringBoot 模块 作为自动配置模块 ...