一文带你拿下Redis面试

5,350 阅读24分钟

背景

还记得我刚接触Redis的时候还是在一次网站首页的优化的时候,那时候接手的是一个老系统,简直就是一堆屎山,前端页面还是用JSP写的,笔者这边也是吐槽了很多遍,但是无可奈何啊,生活还是得继续;当时那个系统的首页的一些图表的数据加载完成大概需要10几秒,那时候的开发才不管你的什么2 8 5 原则呢,只要能把接口搞出来,其他的就再也不管了。后来我就想到了用Redis来缓解一下这个问题,当时我是这么设计的,把一些不怎么会经常变动的数据,即使有变动对业务不影响的数据存到了Redis里面,每隔1分钟刷新一下缓存的数据,这么一操作果然快了很多。好了废话不多说,笔者接下来就介绍一下我在面试中遇到关于Redis相关的一些问题。

谈谈你对Redis的了解

Redis是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库(NoSQL类型的数据库),并提供多种语言的 API。

优点

  • 读写性能优异,纯内存操作,每秒可以处理超过 10万次读写操作,支持数据持久化支持AOFRDB两种持久化方式
  • Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行

缺点

  • Redis的数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
  • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败
  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂

Redis 有哪些使用场景

适用的场景

  • 会话缓存:会话(Session)是存储在服务端的,但是可以设置存储的时候不以文件的方式存储,而是存到 Redis 中,而且 Redis 支持数据持久化,不用担心数据因为服务器重启导致 Session 数据丢失的问题。这样做的好处不只是提高获取会话的速度,也对网站的整体性能有很大的提升。
  • 数据缓存:Redis 支持多种数据结构,经常被用来做缓存中间件使用。缓存的数据不只是包括数据库中的数据,也可以缓存一些需要临时存储的数据,例如 token、会话数据等。
  • 排行榜: 利用 RedisSortSet(有序集合)实现
  • 消息队列: 除了 Redis 自身的发布/订阅模式,我们也可以利用 List 来实现一个队列机制
  • 计数器/限速器:利用 Redis 中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等;限速器比较典型的使用场景是限制某个用户访问某个 API 的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力
  • 发布、订阅功能:Redis 中提供了发布订阅相关的命令,可以用来做一些跟发布订阅相关的场景应用等。例如简单的消息队列功能等。

不适用的场景

数据量太大、数据访问频率非常低的业务都不适合使用 Redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源。

Redis为什么这么快

  • 绝大部分请求是纯粹的内存操作,所以非常快
  • 数据结构比较简单,对数据的操作也比较简单
  • 使用多路 I/O 复用模型,非阻塞 IO

Redis 有哪些常见的功能

  1. 数据缓存功能
  2. 分布式锁的功能
  3. 支持数据持久化
  4. 支持事务
  5. 支持消息队列

Redis 支持的数据类型有哪些

  • string 字符串

字符串类型是 Redis 最基础的数据结构,首先键是字符串类型,而且其他几种结构都是在字符串类型基础上构建的。
使用场景:缓存、计数器、共享 Session、限速。

  • Hash(哈希)

在 Redis中哈希类型是指键本身是一种键值对结构,如 value={{field1,value1},……{fieldN,valueN}}
使用场景:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷。所以常常用于用户信息等管理,但是哈希类型和关系型数据库有所不同,哈希类型是稀疏的,而关系型数据库是完全结构化的,关系型数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询开发困难且维护成本高。

  • List(列表)

列表类型是用来储存多个有序的字符串,列表中的每个字符串成为元素,一个列表最多可以储存 2 ^ 32 – 1 个元素,在 Redis 中,可以队列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下的元素等,列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。

使用场景:Redis 的 lpush + brpop 命令组合即可实现阻塞队列,生产者客户端是用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

  • Set(集合)

集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中不允许有重复的元素,并且集合中的元素是无序的,不能通过索引下标获取元素,Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。合理的使用好集合类型,能在实际开发中解决很多实际问题。

使用场景:如:一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及曾强用户粘度比较重要。

  • zset(sorted set:有序集合)

有序集合和集合有着必然的联系,它保留了集合不能有重复成员的特性,但不同得是,有序集合中的元素是可以排序的,但是它和列表的使用索引下标作为排序依据不同的是:它给每个元素设置一个分数,作为排序的依据。

使用场景:排行榜是有序集合经典的使用场景。例如:视频网站需要对用户上传的文件做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。

什么是缓存穿透,怎么解决

  • 现象与原因:

    就是指用户不断发起请求的数据,在缓存和DB中都没有,比如DB中的用户ID是自增的,但是用户请求传了-1,或者是一个特别大的数字,这个时候用户很有可能就是一个攻击者,这样的功击会导致DB的压力过大,严重的话就是把DB搞挂了。因为每次都绕开了缓存直接查询DB

  • 解决方案:

    方法一:在接口层增加校验,不合法的参数直接返回。不相信任务调用方,根据自己提供的API接口规范来,作为被调用方,要考虑可能任何的参数传值。

    方法二:在缓存中查不到,DB中也没有的情况,可以将对应的key的value写为null,或者其他特殊值写入缓存,同时将过期失效时间设置短一点,以免影响正常情况。这样是可以防止反复用同一个ID来暴力攻击。

    方法三:正常用户是不会这样暴力功击,只有是恶意者才会这样做,可以在网关NG作一个配置项,为每一个IP设置访问阀值。

    方法四:高级用户布隆过滤器(Bloom Filter),这个也能很好地防止缓存穿透。原理就是利用高效的数据结构和算法快速判断出你这个Key是否在DB中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

什么是缓存雪崩,该如何解决

  • 现象: 影响轻则,查询变慢,重则当请求并发更高时,出来大面积服务不可用。

  • 原因:同一时间缓存大面积失效,就像没有缓存一样,所有的请求直接打到数据库上来,DB扛不住挂了,如果是重要的库,例如用户库,那牵联就一大片了,瞬间倒一片。

  • 案例: 电商首页缓存,如果首页的key全部都在某一时刻失效,刚好在那一时刻有秒杀活动,那这样的话就所有的请求都被打到了DB。并发大的情况下DB必然扛不住,没有其他降级之类的方案的话,DBA也只能重启DB,但是这样又会被新的流量搞挂。

  • 解决方案:批量往redis存数据的时候,把每个key的失效时间加上个随机数,这样的话就能保证数据不会在同一个时间大面积失效。

什么是缓存击穿,该如何解决

  • 现象与原因:跟缓存雪崩类似,但是又有点不一样。雪崩是因为大面积缓存失效,请求全打到DB;而缓存击穿是指一个key是热点,不停地扛住大并发请求,全都集中访问此key,而当此key过期瞬间,持续的大并发就击穿缓存,全都打在DB上。就又引发雪崩的问题。

  • 解决方案:设置热点key不过期。或者加上互斥 锁。

Redis相比memcached有哪些优势

  1. memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型
  2. Redis的速度比memcached快很多
  3. Redis可以持久化其数据
  4. Redis支持数据的备份,即master-slave模式的数据备份。

什么是 Redis 事务,原理是什么

Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。

它的原理是先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。

Redis 事务的注意点有哪些

  • Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
  • Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。

Redis 为什么不支持回滚

Redis 的事务不支持回滚,但是执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。

Redis 是单线程还是多线程

其实说单线程的、多线程的都不对,或者说是不全面: 其实通常说的 Redis 是单线程,主要是指 Redis 对外提供键值存储服务的主要流程,即网络 IO 和键值对读写是由⼀个线程来完成的。除此外 Redis 的其他功能,比如持久化异步删除集群数据同步等,是由额外的线程执⾏的。

Redis 为什么用单线程

先聊聊大家熟悉的MySQL吧,它就是使用的多线程。MySQL 不会每有一个连接就创建一个线程,因为线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等,同时也会降低计算机的整体性能,这个正是多线程会遇到的难点。多线程还会存在共享资源竞争的问题,需要引入锁的机制来解决这个问题,这也是开销。多线程开发中并发访问控制是⼀个难点,需要精细的设计才能处理。如果只是简单地处理,比如简单地采⽤⼀个粗粒度互斥锁,只会出现不理想的结果。即便增加了线程,系统吞吐率也不会随着线程的增加而增加,因为大部分线程还在等待获取访问共享资源的互斥锁。而且,大部分采用多线程开发引入的同步原语保护共享资源的并发访问,也会降低系统代码的易调试性和可维护性。而正是因为这些问题,才让 Redis 采⽤了单线程模式。

主进程的其它线程

Redis 3.0 版本后,主进程中除了主线程处理网络 IO 和命令操作外,还有 3 个辅助 BIO 线程。这 3 个 BIO 线程分别负责处理,文件关闭、AOF 缓冲数据刷新到磁盘,以及清理对象这三个任务队列,从而避免这些任务对主 IO 线程的影响。Redis 在启动时,会同时启动这三个 BIO 线程,但是 BIO 线程只有在需要执行相关类型后台任务时才会唤醒,其他时间会休眠等待任务。 image.png

多进程

除了主进程,在以下场景如果需要进行重负荷任务的处理,Redis 会 fork 一个子进程来处理:

  • 收到 bgrewriteaof 命令:  Redis fork 一个子进程,然后子进程往临时 AOF文件中写入重建数据库状态的所有命令。写入完毕后,子进程会通知父进程把新增的写操作追加到临时 AOF 文件。最后将临时文件替换旧的 AOF 文件,并重命名。
  • 收到 bgsave 命令:  Redis 构建子进程,子进程将内存中的所有数据通过快照做一次持久化落地,写入到 RDB 中。
  • 当需要进行全量复制:  master 启动一个子进程,子进程将数据库快照保存到 RDB 文件。在写完 RDB 快照文件后,master 会把 RDB 发给 slave,同时将后续新的写指令都同步给 slave。

Redis6.0 多线程

多线程是 Redis6.0 推出的一个新特性。正如上面所说 Redis 是核心线程负责网络 IO ,命令处理以及写数据到缓冲,而随着网络硬件的性能提升,单个主线程处理⽹络请求的速度跟不上底层⽹络硬件的速度,导致网络 IO 的处理成为了 Redis 的性能瓶颈。
需要注意的是在 Redis6.0 中,多线程机制默认是关闭的,需要在 redis.conf 中完成以下两个设置才能启用多线程。

  • 设置 io-thread-do-reads 配置项为 yes,表示启用多线程。
io-threads-do-reads yes
  • 设置线程个数。⼀般来说,线程个数要小于 Redis 实例所在机器的 CPU 核数,  例如,对于⼀个 8 核的机器来说,Redis 官⽅建议配置 6 个 IO 线程。
io-threads 6

多线程流程

在 Redis6.0 中,主线程和 IO 线程协作处理流程如下: image.png 全部流程分为以下 4 阶段:

阶段一:服务端和客⼾端建立 Socket 连接,并分配处理线程

当有客⼾端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。然后主线程通过轮询方法把 Socket 连接分配给 IO 线程。

阶段二:IO 线程读取并解析请求

主线程把 Socket 分配给 IO 线程后,会进⼊阻塞状态等待 IO 线程完成客户端请求读取和解析。

阶段三:主线程执⾏请求操作

IO 线程解析完请求后,主线程以单线程的⽅式执⾏这些命令操作。

阶段四:IO 线程回写 Socket 和主线程清空全局队

主线程执行完请求操作后,会把需要返回的结果写入缓冲区。然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。

Redis 单点吞吐量有多少

单点 TPS 达到 8 万/秒,QPS 达到 10 万/秒。TPS 和 QPS 的意思:

  • QPS:应用系统每秒钟最大能接受的用户访问量。每秒钟处理完请求的次数,注意这里是处理完,具体是指发出请求到服务器处理完成功返回结果。可以理解在 Server 中有个 counter,每处理一个请求加 1,1s 后 counter=QPS。
  • TPS:每秒钟最大能处理的请求数。每秒钟处理完的事务次数,一个应用系统 1s 能完成多少事务处理,一个事务在分布式处理中,可能会对应多个请求,对于衡量单个接口服务的处理能力,用 QPS 比较合理。

请说明一下 Redis 的批量命令与 Pipeline 有什么不同

  • 原子性不同;批量命令操作需要保证原子性的,Pipeline 执行是非原子性的;
  • 支持的命令不同;批量命令操作是一个命令对应多个 key,Pipeline 支持多个命令的执行;
  • 实现方式的不同;批量命令操作是由 Redis 服务端实现的,而 Pipeline 是需要服务端和客户端共同实现的。

Redis 常用的业务场景有哪些

  • 对热点数据的缓存;因为 Redis 支持多种数据类型,数据存储在内存中,访问速度块,所以 Redis 很适合用来存储热点数据;
  • 限时类业务的实现;可以使用 expire 命令设置 key 的生存时间,到时间后自动删除 key。例如使用在验证码验证、优惠活动等业务场景;
  • 计数器的实现;因为 incrby 命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成。例如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等业务场景。
  • 排行榜的实现;借助 Sorted Set 进行热点数据的排序。例如:下单量最多的用户排行榜,最热门的帖子(回复最多)等业务场景;
  • 分布式锁实现;可以利用 Redis 的 setnx 命令进行。
  • 队列机制实现;Redis 提供了 list push 和 list pop 这样的命令,所以能够很方便的执行队列操作。

Redis 哨兵和集群的区别是什么

Redis 的哨兵作用是管理多个 Redis 服务器,提供了监控、提醒以及自动的故障转移的功能。哨兵可以保证当主服务器挂了后,可以从从服务器选择一台当主服务器,把别的从服务器转移到读新的主机。Redis 哨兵的主要功能有:

  • 集群监控:对 Redis 集群的主从进程进行监控,判断是否正常工作。
  • 消息通知:如果存在 Redis 实例有故障,那么哨兵可以发送报警消息通知管理员。
  • 故障转移:如果主机(master)节点挂了,那么可以自动转移到从(slave)节点上。
  • 配置中心:当存在故障时,对故障进行转移后,配置中心会通知客户端新的主机(master)地址。

Redis 的哨兵有什么功能

哨兵是 Redis 集群架构中非常重要的一个组件,主要功能如下:

  • 集群监控,负责监控 Redis Master 和 Slave 进程是否正常工作;
  • 消息通知,如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员;
  • 故障转移,如果 Master node 挂掉了,会自动转移到 Slave node 上;
  • 配置中心,如果故障转移发生了,通知 Client 客户端新的 Master 地址。

Redis 集群之间的复制方式是什么

需要知道 Redis 的复制方式前,需要知道主从复制(Master-Slave Replication)的工作原理,具体为:

  1. Slave 从节点服务启动并连接到 Master 之后,它将主动发送一个 SYNC 命令;
  2. Master 服务主节点收到同步命令后将启动后台存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后,Master 将传送整个数据库文件到 Slave,以完成一次完全同步;
  3. Slave 从节点服务在接收到数据库文件数据之后将其存盘并加载到内存中;
  4. 此后,Master 主节点继续将所有已经收集到的修改命令,和新的修改命令依次传送给 Slaves,Slave 将在本次执行这些数据修改命令,从而达到最终的数据同步。

整个执行的过程都是使用异步复制的方式进行复制。

什么是数据库缓存双写一致性

当一个数据需要更新时,因为不可能做到同时更新数据库和缓存,那么此时读取数据的时候就一定会发生数据不一致问题,而数据不一致问题在金融交易领域的系统中是肯定不允许的。

解决办法:读的时候先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。

什么是分布式锁,有什么作用

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在单机或者单进程环境下,多线程并发的情况下,使用锁来保证一个代码块在同一时间内只能由一个线程执行。比如 Java 的 Synchronized 关键字和 Reentrantlock 类。

分布式锁的作用是当多个进程不在同一个系统中,用分布式锁可以控制多个进程对资源的访问。

分布式锁可以通过什么来实现

  • 可以使用 Memcached 实现分布式锁:Memcached 提供了原子性操作命令 add,线程获取到锁。key 已存在的情况下,则 add 失败,获取锁也失败。
  • 也可以使用 Redis 实现分布式锁:Redis 的 setnx 命令为原子性操作命令。只有在 key 不存在的情况下,才能 set 成功。和 Memcached 的 add 方法比较类似。
  • 还可以使用 ZooKeeper 分布式锁:利用 ZooKeeper 的顺序临时节点,来实现分布式锁和等待队列。
  • 还有 Chubby 实现分布式锁:Chubby 底层利用了 Paxos 一致性算法,实现粗粒度分布式锁服务。

Redis 持久化数据和缓存怎么做扩容

如果 Redis 被当做缓存使用,使用一致性哈希实现动态扩容缩容。

如果 Redis 被当做一个持久化存储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。

否则的话(即 Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis 集群可以做到这样。

Redis 持久化机制有哪些

Redis 主要支持的持久化机制为 RDB(快照)和 AOF(追加文件)。

RDB 持久化是在指定时间间隔内保存数据快照到硬盘中。但 RDB 的持久化方式没有办法实现实时性的持久化。当应用使用 RDB 持久化后,如果 Redis 系统发生崩溃,那么使用 RDB 恢复数据时,恢复后的数据中,存在丢失最近一次生成快照之后更改的所有数据。所以 RDB 持久化不适用于丢失一部分数据也会对应用造成很大影响的备份中。

AOF 持久化是把命令追加到操作日志的尾部,然后保存所有历史操作。AOF 主要是解决数据持久化的实时性。Redis 服务器默认开启 RDB,关闭 AOF;要开启 AOF,需要在配置文件中配置:appendonly yes。AOF 持久化相对于 RDB 持久化的优点在于可以实时的对 Redis 缓存进行写入记录,保证快速恢复缓存时的完整性。

Redis 持久化机制 AOF 和 RDB 有哪些不同之处

RDB 和 AOF 的区别:

  • 持久化的方式不同:RDB 持久化是在指定时间间隔内保存数据快照到硬盘中(快照的方式)。AOF 持久化是把命令追加到操作日志的尾部,然后保存所有历史操作(追加文件)。
  • 恢复的数据安全性不同:RDB 恢复数据时,恢复后的数据中,存在丢失最近一次生成快照之后更改的所有数据;AOF 持久化的数据实时性和安全性更高。
  • 缓存恢复的速度不同:RDB 产生的文件紧凑压缩高,读取 RDB 文件恢复时速度比 AOF 快;由于 AOF 实时追加写命令,所以 AOF 的缓存文件体积比较大,但是可以通过重写(rewrite)压缩 AOF 持久化文件体积。

请介绍一下 RDB 持久化机制的优缺点

它的优点有:

  • RDB 是在某个时间点上的数据快照,非常适合使用在全量备份的情况,生成的 RDB 是一个紧凑压缩的二进制文件。快照产生的 RDB 文件可以拷贝到远程机器或文件系统中,用于灾难恢复;
  • RDB 的加载恢复数据速度快于 AOF 的恢复方式。

它的缺点有:

  • 因为 bgsave 每次运行都要执行 fork 操作创建子进程,操作比较繁琐,如果实时存储快照会导致成本过高,所以 RDB 只能在特定条件下进行一次持久化,从而容易出现数据丢失的情况;
  • RDB 文件是一个特定二进制格式保存的文件,Redis 的版本更新过程中,有对 RDB 的版本格式修改,会出现老版本的 RDB 文件无法兼容新版本的 RDB 格式问题。

请介绍一下 AOF 持久化机制的优缺点

它的优点有:

  • AOF 持久化可以保证数据非常完整,故障恢复时相对 RDB 持久化丢失的数据最少;
  • 由于 AOF 是可以实时对缓存命令追加到 AOF 文件的末尾,所以可以对历史操作的缓存命令进行处理。

它的缺点有:

  • 由于 AOF 持久化是不断对 AOF 文件进行追加记录的,会导致 AOF 文件体积很大,极端的情况下可能会出现 AOF 文件用完硬盘的可用空间;但是 Redis 2.4 版本以后支持 AOF 自动重写,有效的解决 AOF 文件过大的问题;
  • 当 AOF 文件体积很大时,会出现恢复速度慢,对性能影响大的问题;
  • 当开启 AOF 后,对 QPS 会有一定影响,相对 RDB 来说,写 QPS 会下降。