阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!

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

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

我朋友回答。工做常常用,有看过源码。redis

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

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

那好的,咱们来聊聊 Redis 字典吧。服务器

直接将他整蒙逼。数据结构

小黑哥的朋友因为没怎么研究过 Redis 字典,因此这题就直接回答不知道了。ide

固然,若是面试中真不知道,那就回答不了解,直接下一题,不要乱答。学习

不过这一题,小黑哥以为仍是很惋惜,其实 Redis 字典基本原理与 HashMap 差很少,那咱们其实能够套用这其中的原理,不求回答满分,可是怎么也能够得个及格分吧~idea

面试过程真要碰到这个问题,咱们能够从下面三个方面回答。

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

字典数据结构

提及字典,也许你们比较陌生,可是咱们都知道 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]

s

扩容 操做须要将 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...
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客: studyidea.cn

相关文章
相关标签/搜索