关于java:精心整理了20道Redis经典面试题珍藏版

3次阅读

共计 9273 个字符,预计需要花费 24 分钟才能阅读完成。

无关 Redis 之前有独自写过几篇文章

Redis 缓存穿透、击穿、雪崩,数据库与缓存一致性

谈谈 Redis 五种数据结构及实在利用场景

一文让你明确 Redis 长久化(RDB、AOF)

怎么实现 Redis 的高可用?(主从、哨兵、集群)

之所以独自写,是因为这几块内容比拟大,而且也很重要,所以想写的具体点、深刻点,让大家在了解的根底上记住它们, 这让就不容易遗记。

所以无关以上相干的面试题就不再独自整顿了, 具体能够看相干文章。

  • redis 是什么?
  • Redis 为什么这么快?
  • 为什么 Redis 6.0 之后改多线程呢?
  • 你理解 Redis 的过期策略吗?
  • 聊聊 Redis 内存淘汰策略?
  • Redis 事务机制是怎么的?
  • 说说 Redis 哈希槽的概念?
  • 为什么 Redis Cluster 会设计成 16384 个槽呢?
  • Redis 在集群中查找 key 的时候,是怎么定位到具体节点的?
  • Redis 底层应用的什么协定?
  • Redis 的 Hash 抵触怎么办?
  • Redis 相比 memcached 有哪些劣势?
  • 有哪些方法能够升高 Redis 的内存应用状况呢?
  • Redis 中获取海量数据的正确操作形式?
  • 如何应用 Redis 的性能更高?
  • 如何解决 Redis 的并发竞争 Key 问题?
  • 应用过 Redis 做异步队列么,你是怎么用的?
  • 用 Redis 做过延时队列吗? 具体应该怎么实现?
  • 应用 Redis 统计网站的 UV,应该怎么做?
  • 什么是热 Key 问题,如何解决热 key 问题?

1、redis 是什么

Redis 是 C 语言开发的一个开源的高性能键值对(key-value)的内存数据库,它是一种 NoSQL(泛指非关系型)的数据库。

与 MySQL 数据库不同的是,Redis 的数据是存在内存中的。它的读写速度十分快,每秒反对并发 10W QPS。因而 Redis 被广泛应用于缓存,另外,Redis 也常常用来做分布式锁, 也能够用来做消息中间件等。

除此之外,Redis 还反对事务、长久化、LUA 脚本、多种集群计划。

2、Redis 为什么这么快?

1) 齐全基于内存存储实现

齐全基于内存,绝大部分申请是纯正的内存操作,十分疾速。数据存在内存中,相似于 HashMap,HashMap 的劣势就是查找和操作的工夫复杂度都是 O(1);

2) 正当的数据编码

Redis 反对多种数据数据类型, 每种根本类型可能对多种数据编码。什么时候, 应用什么样数据类型,应用什么样编码,是 redis 设计者总结优化的后果。

3) 单线程模型

Redis 是单线程模型的,而单线程防止了 CPU 不必要的上下文切换和竞争锁的耗费。也正因为是单线程,如果某个命令执行过长(如 keys,hgetall 命令), 会造成排队阻塞。

Redis 6.0 引入了多线程提速,它的执行命令操作内存的依然是个单线程的。

4) 正当的线程模型

应用多路 I / O 复用模型,非阻塞 IO;

多路 I / O 复用技术能够让单个线程高效的解决多个连贯申请,而 Redis 应用 epoll 作为 I / O 多路复用技术的实现。并且,Redis 本身的事件处理模型将 epoll 中的连贯、读写、敞开都转换为事件,不在网络 I / O 上节约过多的工夫。

3、为什么 Redis 6.0 之后改多线程呢?

Redis6.0 之前,Redis 在解决客户端的申请时,包含读 socket、解析、执行、写 socket 等都由一个程序串行的主线程解决,这就是所谓的“单线程”。

redis 应用多线程并非是齐全摒弃单线程,redis 还是应用单线程模型来解决客户端的申请,只是应用多线程来解决数据的读写和协定解析,执行命令还是应用单线程。

这样做的目标是因为 redis 的性能瓶颈在于网络 IO 而非 CPU,应用多线程能晋升 IO 读写的效率,从而整体进步 redis 的性能。

4、你理解 Redis 的过期策略吗?

咱们在 set key 的时候,能够给它设置一个过期工夫,比方 expire key 60。指定这 key 60s 后过期,60s 后 redis 是如何解决的?咱们先来介绍几种过期策略:

定时过期

每个设置过期工夫的 key 都须要创立一个定时器,到过期工夫就会立刻对 key 进行革除。该策略能够立刻革除过期的数据,对内存很敌对;然而会占用大量的 CPU 资源去解决过期的数据,从而影响缓存的响应工夫和吞吐量。

惰性过期

只有当拜访一个 key 时,才会判断该 key 是否已过期,过期则革除。该策略能够最大化地节俭 CPU 资源,却对内存十分不敌对。极其状况可能呈现大量的过期 key 没有再次被拜访,从而不会被革除,占用大量内存。

定期过期

每隔肯定的工夫,会扫描肯定数量的数据库的 expires 字典中肯定数量的 key,并革除其中已过期的 key。该策略是前两者的一个折中计划。通过调整定时扫描的工夫距离和每次扫描的限定耗时,能够在不同状况下使得 CPU 和内存资源达到最优的均衡成果。

Redis 中同时应用了 惰性过期 定期过期 两种过期策略。

假如 Redis 以后寄存 30 万个 key,并且都设置了过期工夫,如果你每隔 100ms 就去查看这全副的 key,CPU 负载会特地高,最初可能会挂掉。

因而,redis 采取的是定期过期,每隔 100ms 就随机抽取肯定数量的 key 来检查和删除的。

然而呢,最初可能会有很多曾经过期的 key 没被删除。这时候,redis 采纳惰性删除。在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期工夫并且曾经过期了,此时就会删除。

然而呢,如果定期删除漏掉了很多过期的 key,而后也没走惰性删除。就会有很多过期 key 积在内存中,间接会导致内存爆的。或者有些时候,业务量大起来了,redis 的 key 被大量应用,内存间接不够了,
这个时候就须要内存淘汰策略来爱护本人了。

5、聊聊 Redis 内存淘汰策略?

redis 有 8 种内存淘汰策略。

  • volatile-lru:当内存不足以包容新写入数据时,从设置了过期工夫的 key 中应用 LRU(最近起码应用)算法进行淘汰;
  • allkeys-lru:当内存不足以包容新写入数据时,从所有 key 中应用 LRU(最近起码应用)算法进行淘汰;
  • volatile-lfu:4.0 版本新增,当内存不足以包容新写入数据时,在过期的 key 中,应用 LFU 算法进行删除 key;
  • allkeys-lfu:4.0 版本新增,当内存不足以包容新写入数据时,从所有 key 中应用 LFU 算法进行淘汰;
  • volatile-random:当内存不足以包容新写入数据时,从设置了过期工夫的 key 中,随机淘汰数据;
  • allkeys-random:当内存不足以包容新写入数据时,从所有 key 中随机淘汰数据;
  • volatile-ttl:当内存不足以包容新写入数据时,在设置了过期工夫的 key 中,依据过期工夫进行淘汰,越早过期的优先被淘汰;
  • noeviction:默认策略,当内存不足以包容新写入数据时,新写入操作会报错;

6、聊聊 Redis 事务机制?

Redis 通过 MULTI、EXEC、WATCH 等一组命令汇合,来实现事务机制。事务反对一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会依照程序串行化执行队列中的命令,其余客户端提交的命令申请不会插入到事务执行命令序列中。

简言之,Redis 事务就是 程序性、一次性、排他性 的执行一个队列中的一系列命令。

Redis 执行事务的流程如下:

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD)
命令 形容
EXEC 执行所有事务块内的命令
DISCARD 勾销事务,放弃执行事务块内的所有命令
MULTI 标记一个事务块的开始
UNWATCH 勾销 WATCH 命令对所有 key 的监督。
WATCH 监督 key,如果在事务执行之前,该 key 被其余命令所改变,那么事务将被打断。

无关 redis 事务须要留神的就是

1)与 mysql 中事务不同,在 redis 事务遇到执行谬误的时候,不会进行回滚,而是简略的放过了,并保障其余的命令失常执行(所以说 redis 的事务并不是保障原子性)。

2)当事务的执行过程中,如果 redis 意外的挂了。很遗憾只有局部命令执行了,前面的也就被抛弃了。

7、说说 Redis 哈希槽的概念?

Redis 集群没有应用一致性 hash, 而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定搁置哪个槽,集群的每个节点负责一部分 hash 槽。

应用哈希槽的益处就在于能够不便的增加或移除节点。这种构造无论是增加删除或者批改某一个节点,都不会造成集群不可用的状态。

当须要减少节点时,只须要把其余节点的某些哈希槽挪到新节点就能够了;

当须要移除节点时,只须要把移除节点上的哈希槽挪到其余节点就行了;

在这一点上,咱们当前新增或移除节点的时候不必先停掉所有的 redis 服务。

8、为什么 RedisCluster 会设计成 16384 个槽呢?

2 的 14 次方就是 16384, 这个当然不说肯定要设计成 16384 个槽, 作者对这个也做了解释。

地址如下: https://github.com/antirez/re…

1) 如果槽位为 65536,发送心跳信息的音讯头达 8k,发送的心跳包过于宏大。

如上所述,在音讯头中,当槽位为 65536 时,这块的大小是: 65536÷8÷1024=8kb 因为每秒钟,redis 节点须要发送肯定数量的 ping 音讯作为心跳包,如果槽位为 65536,这个 ping 音讯的音讯头太大了,节约带宽。

2) redis 的集群主节点数量根本不可能超过 1000 个。

如上所述,集群节点越多,心跳包的音讯体内携带的数据越多。如果节点过 1000 个,也会导致网络拥挤。因而 redis 作者,不倡议 redis cluster 节点数量超过 1000 个。那么,对于节点数在 1000 以内的 redis cluster 集群,16384 个槽位够用了。没有必要拓展到 65536 个。

3) 槽位越小,节点少的状况下,压缩率高

Redis 主节点的配置信息中,它所负责的哈希槽是通过一张 bitmap 的模式来保留的,在传输过程中,会对 bitmap 进行压缩,然而如果 bitmap 的填充率 slots / N 很高的话(N 示意节点数),bitmap 的压缩率就很低。

如果节点数很少,而哈希槽数量很多的话,bitmap 的压缩率就很低。

9、Redis 在集群中查找 key 的时候,是怎么定位到具体节点的?

应用 CRC16 算法 对 key 进行 hash, 再将 hash 值对 16384 取模,失去具体的槽位依据节点和槽位的映射信息(与集群建设连贯后,客户端能够获得槽位映射信息),找到具体的节点地址 去具体的节点找 key 如果 key 不在这个节点上,则 redis 集群会返回 moved 指令,加上新的节点地址给客户端。

同时,客户端会刷新本地的节点槽位映射关系如果槽位正在迁徙中,那么 redis 集群会返回 asking 指令给客户端,这是长期纠正,客户端不会刷新本地的节点槽位映射关系

10、Redis 底层,应用的什么协定?

RESP 英文全称是 Redis Serialization Protocol, 它是专门为 redis 设计的一套序列化协定. 这个协定其实在 redis 的 1.2 版本时就曾经呈现了, 然而到了 redis2.0 才最终成为 redis 通信协定的规范。

RESP 次要有实现简略、解析速度快、可读性好等长处。

11、Redis 的 Hash 抵触怎么办

Redis 中的 Hash 和 Java 的 HashMap 更加类似, 都是数组 + 链表的构造,当产生 hash 产生碰撞时将会把元素追加到链表上。

在 Redis 中 hash 的内部结构也是一样的: 第一维是数组, 第二维是链表. 组成一个 全局哈希表

在 Java 中 HashMap 扩容是个很耗时的操作, 须要去申请新的数组,扩容的老本并不低,因为须要遍历一个工夫复杂度为 O(n)的数组,并且为其中的每个 enrty 进行 hash 计算。退出到新数组中。

为了谋求高性能,Redis 采纳了渐进式 rehash 策略. 这也是 hash 中最重要的局部.

redis 在扩容的时候执行 rehash 策略会保留新旧两个 两个全局哈希表,查问时也会同时查问两个全局哈希表 ,Redis 会将旧 全局哈希表 中的内容一点一点的迁徙到新的 全局哈希表 中, 当迁徙实现时, 就会用新的 全局哈希表 取代之前的。当 全局哈希表 移除了最初一个元素之后, 这个数据结构将会被删除.

失常状况下, 当 全局哈希表 中元素的个数等于数组的长度时, 就会开始扩容, 扩容的新数组是原数组大小的 2 倍。

如果 Redis 正在做 bgsave(长久化) 时, 可能不会去扩容, 因为要缩小内存页的过多拆散(Copy On Write). 然而如果 全局哈希表 曾经十分满了, 元素的个数达到了数组长度的 5 倍时,Redis 会强制扩容。

12、Redis 相比 memcached 有哪些劣势

  1. Memcached 所有的值均是简略的字符串,redis 作为其替代者,反对更为丰盛的数据类
  2. Redis 的速度比 Memcached 快很多
  3. Redis 能够长久化其数据

13、有哪些方法能够升高 Redis 的内存应用状况呢?

当你的业务利用在 Redis 中存储数据很少时,你可能并不太关怀内存资源的应用状况。但随着业务的倒退,你的业务存储在 Redis 中的数据就会越来越多。

那在应用 Redis 时,怎么做能力更节俭内存呢?这里总结了 4 点倡议:

1) 管制 key 的长度

最简略间接的内存优化,就是管制 key 的长度。

在开发业务时,你须要提前预估整个 Redis 中写入 key 的数量,如果 key 数量达到了百万级别,那么,过长的 key 名也会占用过多的内存空间。

所以,你须要保障 key 在简略、清晰的前提下,尽可能把 key 定义得短一些。

例如,原有的 key 为 user123,则能够优化为 u:bk:123。

这样一来,你的 Redis 就能够节俭大量的内存,这个计划对内存的优化十分间接和高效。

2) 防止存储 bigkey

bigkey, 意思就是这个 key 的 value 值很大。除了管制 key 的长度之外,你同样须要关注 value 的大小,如果大量存储 bigkey,也会导致 Redis 内存增长过快。

除此之外,客户端在读写 bigkey 时,还有产生性能问题。

所以,你要防止在 Redis 中存储 bigkey,个别倡议是:

  • String:大小管制在 10KB 以下
  • List/Hash/Set/ZSet:元素数量管制在 1 万以下

3) 尽可能地都设置过期工夫

Redis 数据存储在内存中,这也意味着其资源是无限的。你在应用 Redis 时,要把它当做缓存来应用,而不是数据库。

所以,你的利用写入到 Redis 中的数据,尽可能地都设置 过期工夫 。采纳这种计划,能够让 Redis 中只保留常常拜访的 热数据,内存利用率也会比拟高。

4) 实例设置 maxmemory + 淘汰策略

尽管你的 Redis key 都设置了过期工夫,但如果你的业务利用写入量很大,并且过期工夫设置得比拟久,那么短期间内 Redis 的内存依旧会快速增长。

如果不管制 Redis 的内存下限,也会导致应用过多的内存资源。

对于这种场景,你须要提前预估业务数据量,而后给这个实例设置 maxmemory 管制实例的内存下限,这样能够防止 Redis 的内存继续收缩。

配置了 maxmemory,此时你还要设置数据淘汰策略,而淘汰策略如何抉择,你须要联合你的业务特点来决定。

14、Redis 中获取海量数据的正确操作形式?

有时候须要从 Redis 实例成千上万的 key 中找出 特定前缀的 key列表来手动解决数据,可能是批改它的值,也可能是删除 key。这里就有一个问题,如何从海量的 key 中找出满足特定前缀的 key 列表来?

比方咱们的用户 token 缓存是采纳了【user_token:userid】格局的 key,保留用户的 token 的值。这时候咱们想看下有多少用户在线。

在 Redis2.8 版本之前,咱们能够应用 keys 命令依照正则匹配失去咱们须要的 key。Redis 提供了一个简略暴力的指令 keys 用来列出所有满足特定正则字符串规定的 key。

keys user_token*

然而这个命令有一些毛病:

  • 没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件,当你看到满屏的字符串刷的没有止境时,你就晓得好受了。
  • keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,
  • 所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,程序执行所有指令,其它指令必须等到以后的 keys 指令执行完了才能够持续。
  • 倡议生产环境屏蔽 keys 命令

在满足需要和存在造成 Redis 卡顿之间到底要如何抉择呢?面对这个两难的抉择,Redis 在 2.8 版本给咱们提供了解决办法——scan 命令

相比于 keys 命令,scan 命令有两个比拟显著的劣势:

  • scan 命令的工夫复杂度尽管也是 O(N),但它是分次进行的,不会阻塞线程。
  • scan 命令提供了 limit 参数,能够管制每次返回后果的最大条数。

这两个劣势就帮忙咱们解决了下面的难题,不过 scan 命令也并不是完满的,它返回的后果有可能反复,因而须要客户端去重 这点十分重要。

15、如何应用 Redis 的性能更高?

1)master 敞开长久化

个别咱们在生产上采纳的长久化策略为 master 敞开长久化,slave 开 RDB 即可,必要的时候 AOF 和 RDB 都开启。

2) 不应用简单度过高的命令

Redis 是单线程模型解决申请,在执行简单度过高的命令时,会耗费更多的 CPU 资源,主线程中的其它申请只能期待,这时也会产生排队提早。

所以,你须要防止执行例如 sort、sinter、sinterstore、zunionstore、zinterstore 等聚合类命令。

对于这种聚合类操作,我倡议你把它放到客户端来执行,不要让 Redis 承当太多的计算工作。

3)执行 O(N) 命令时,关注 N 的大小

躲避应用简单度过高的命令,就能够居安思危了么?

答案是否定的。

当你在执行 O(N) 命令时,同样须要留神 N 的大小。

就好比下面说的应用 keys 命令,如果一次性能查问过多的数据,也会在网络传输过程中耗时过长,操作提早变大。

所以,对于容器类型(List/Hash/Set/ZSet),在元素数量未知的状况下,肯定不要无脑执行 LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。

在查问数据时,你要遵循以下准则:

  • 先查问数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
  • 元素数量较少,可一次性查问全量数据
  • 元素数量十分多,分批查问数据(LRANGE/HASCAN/SSCAN/ZSCAN)

4) 批量命令代替单个命令

当你须要一次性操作多个 key 时,你应该应用批量命令来解决。

批量操作相比于屡次单个操作的劣势在于,能够显著缩小客户端、服务端的来回网络 IO 次数。

所以我给你的倡议是:

  • String / Hash 应用 MGET/MSET 代替 GET/SET,HMGET/HMSET 代替 HGET/HSET
  • 其它数据类型应用 Pipeline,打包一次性发送多个命令到服务端执行

5) 防止集中过期 key

Redis 清理过期 key 是采纳定时 + 懈怠的形式来做的,而且这个过程都是在主线程中执行。

如果你的业务存在大量 key 集中过期的状况,那么 Redis 在清理过期 key 时,也会有阻塞主线程的危险。

想要防止这种状况产生,你能够在设置过期工夫时,减少一个随机工夫,把这些 key 的过期工夫打散,从而升高集中过期对主线程的影响。

6) 只应用 db0

只管 Redis 提供了 16 个 db,但我只倡议你应用 db0。

为什么呢?我总结了以下 3 点起因:

  • 在一个连贯上操作多个 db 数据时,每次都须要先执行 SELECT,这会给 Redis 带来额定的压力
  • 应用多个 db 的目标是,按不同业务线存储数据,那为何不拆分多个实例存储呢?拆分多个实例部署,多个业务线不会相互影响,还能进步 Redis 的拜访性能
  • Redis Cluster 只反对 db0,如果前期你想要迁徙到 Redis Cluster,迁徙老本高

16、如何解决 Redis 的并发竞争 Key 问题

这个也是线上十分常见的一个问题,就是多客户端同时并发写一个 key,可能原本应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,批改值之后再写回去,只有程序错了,数据就错了。

举荐一种计划:分布式锁(zookeeper 和 redis 都能够实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要应用分布式锁,这样会影响性能)

17、应用过 Redis 做异步队列么,你是怎么用的?

个别应用 list 构造作为队列,rpush 生产音讯,lpop 生产音讯。当 lpop 没有音讯的时候,要适当 sleep 一会再重试。

如果不想 sleep 呢?list 还有个指令叫 blpop,在没有音讯的时候,它会阻塞住直到音讯到来。

但如果是这样你发现 redis 作为音讯队列是不平安的,它不能反复生产,一旦生产就会被删除。同时做消费者确认 ACK 也麻烦所以个别在理论开发中个别很少用 redis 中音讯队列,因为当初曾经有 Kafka、RabbitMQ 等成熟的音讯队列了,它们的性能更加欠缺。

18、用 Redis 做延时队列,具体应该怎么实现?

提早队列能够应用 zset(有序列表)实现,咱们将音讯序列化成一个字符串作为列表的 value,这个音讯的到期解决工夫作为 score,而后用定时器定时去扫描,一旦有执行工夫小于或等于以后工夫的工作,就立刻执行。

19、应用 Redis 统计网站的 UV,应该怎么做?

UV 与 PV 不同,UV 须要去重。个别有 2 种计划:

1、用 BitMap。存的是用户的 uid,计算 UV 的时候,做下 bitcount 就行了。

2、用布隆过滤器。将每次拜访的用户 uid 都放到布隆过滤器中。长处是省内存,毛病是无奈得 到准确的 UV。然而对于不须要准确晓得具体 UV,只须要大略的数量级的场景,是个不错的抉择。

20、什么是热 Key 问题,如何解决热 key 问题

所谓热 key 问题就是,忽然有几十万的申请去拜访 redis 上的某个特定 key。那么,这样会造成流量过于集中,达到物理网卡下限,从而导致这台 redis 的服务器宕机。

而热点 Key 是怎么产生的呢?次要起因有两个:

  • 用户生产的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
  • 申请分片集中,超过单 Redi 服务器的性能,比方固定名称 key,Hash 落入同一台服务器,霎时访问量极大,超过机器瓶颈,产生热点 Key 问题。

那么在日常开发中,如何辨认到热点 key 呢?

  • 凭教训判断哪些是热 Key;
  • 客户端统计上报;
  • 服务代理层上报

如何解决热 key 问题?

  • Redis 集群扩容:减少分片正本,平衡读流量;
  • 将热 key 扩散到不同的服务器中;
  • 应用二级缓存,即 JVM 本地缓存, 缩小 Redis 的读申请。

参考

[1] Redis 最佳实际指南: https://mp.weixin.qq.com/s/Fz…

[2] Redis 经典面试题: https://mp.weixin.qq.com/s/fB…

[3] Redis 面试 20 题: http://www.gameboys.cn/articl…

关注公众号: 后端元宇宙。继续输入优质好文

正文完
 0