Skip to content

Redis 面试题

基础

Redis 是单线程还是多线程?

Redis 是单线程模型。单线程指的是接收客户端请求-解析请求-数据读写-发送数据给客户端,这个过程是由一个主线程来完成的,这是单线程的原因。

但是 Redis 程序本身不是单线程的,Redis 在启动是,是会启动后台线程的:

  • Redis2.6 会启动两个后台线程,分别处理关闭文件、AOF 刷盘这两个任务
  • Redis4.0 之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key/flushdb async/flushall async等命令,会把这些删除操作交给后台线程执行,好处是不会导致 redis 主线程卡顿。因此我们要删除大 key 时,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致主线程卡顿,因此我们应该使用 unlink 命令异步删除大 key
  • Redis6.0 之后,采用了多个 I/O 线程处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时候会出现在网络的 I/O 处理上。但是对于命令的执行仍然使用单线程处理

Redis 大 key 会有什么问题?如何解决?

  • 客户端超时阻塞:由于 Redis 是单线程处理命令,操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端角度来看,就是很久都没有响应
  • 引发网络阻塞:每次获取大 key 的网络流量较大。如果一个 key 的大小是 1MB,QPS 1000,每秒产生 1000MB 流量,对于普通前兆网卡来说是灾难性的
  • 阻塞工作线程:如果使用 del 删除大 key,会阻塞工作线程
  • 内存分布不均:集群模型在 slot 分片均匀的情况下,会出现数据和查询倾斜的情况,部分有大 key 的节点占用内存太多,导致 QPS 降低

解决方式:

  • 拆分成多个小 key,这是最容易想到的办法,降低单 key 的大小,读取可以用 mget 批量读取
  • 设置合理的过期时间,为每个 key 设置合理的过期时间,以便在数据失效时能自动清理,避免大 key 长时间积累在内存
  • 启动内存淘汰策略。启动 Redis 的内存淘汰策略,例如 LRU,以便在内存不足时自动淘汰最近最少使用的数据,防止大 key 长时间占用内存
  • 数据分片。例如使用 Redis Cluster 将数据分散到多个实例,以减轻单个示例的负担,降低大 key 问题的风险
  • 删除大 key:使用 unlink 命令删除大 key,unlink 命令是 del 命令的异步版本,它可以在后台删除 key,避免阻塞 Redis 示例

Redis 为什么快?

  • Redis 的大部分操作都在内存中进行
  • 采用单线程避免多线程之间的竞争
  • 采用 I/O 多路复用处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。在 Redis 运行单线程的情况下,该机制允许在内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求和数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果

数据结构

Redis 数据结构有哪些?

Redis 常见的有 5 种数据结构:

  1. String(字符串):SDS。缓存对象、常规计数、分布式锁、共享 session 等
  2. Hash(哈希):压缩列表或哈希表 -> listpack。缓存对象、购物车
  3. List(列表):双向链表和压缩列表 -> quicklist。消息队列
  4. 问题 1:生产者需要自行实现全局唯一 ID
  5. 问题 2:不能以消费组形式消费数据
  6. Set(集合):哈希表或整数集合。聚合计算(交集、并集、差集),点赞、共同关注、抽奖
  7. ZSet(有序集合):压缩列表或跳表 -> listpack。排序场景,比如排行榜、电话和姓名等

后面新增了 4 种:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。

用什么结构实现延迟消息队列?

常见延迟消息场景:

  • 淘宝购物车未付款订单 24 小时后自动取消
  • 打车时长时间没有车主接单,平台取消订单并告诉你暂时没有车主接单
  • 点外卖时,如果商家 10 分钟还没有接单,就会自动取消接单

可以使用有序集合 ZSet 的方式来实现延迟队列,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

使用 add score1 value1 就可以一直向内存中生产消息,再利用 zrangebyscore 查询所有符合条件的所有待处理任务,通过循环执行队列任务即可。

ZSet 用过吗?

ZSet 可以实现排行榜功能。ZSet 的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 的底层数据结构
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 底层的数据结构
  • Redis7.0 中,压缩列表的数据结构已经废弃了,交由 listpack 数据结构来实现了

高可用

缓存雪崩是什么?如何解决?

缓存雪崩是指大量请求不经过缓存,直接查询数据库,导致数据库压力骤增,进而发生宕机等故障,从而造成连锁反应,造成整个系统崩溃。

造成缓存雪崩的原因和解决办法:

  • 大量缓存同时过期,缓存都未命中
    • 设置均匀过期时间:如果要给缓存设置过期时间,尽量避免将大量的数据设置为同一个过期时间。可以在设置缓存时间时,给过期时间添加一个随机数,这样就保证数据不会在同一时间过期
    • 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加一个互斥锁,保证同一时间只能有一个请求在构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么直接返回空值或默认值
    • 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,让缓存”永久有效“,并将更新缓存的工作由后台线程实时更新
  • redis 故障
    • 熔断机制:直接启用服务的熔断机制,暂停业务方对缓存服务的访问,直接返回错误,不再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,等 Redis 恢复正常后,再允许业务应用访问缓存服务。熔断机制保护了数据库的正常访问,但是暂停了业务访问缓存。
    • 构建 Redis 缓存高可用集群:通过主从节点的方式构建 Redis 缓存高可用集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机从而导致的缓存雪崩问题

Redis 缓存击穿、缓存穿透、缓存雪崩是什么?怎么解决?

  • 缓存击穿
    • 某个热点 key 过期了,有大量请求访问,无法从缓存中读取,直接访问数据库,数据库容易被高并发的请求冲跨
  • 缓存穿透
    • 用户访问的数据,既不在缓存中,也不在数据库中,导致请求发生时无法构建返回数据
  • 缓存雪崩
    • 大量缓存数据在同一时刻过期或者失效,或 Redis 宕机时,如果此时有大量请求过来,无法走 Redis 缓存,于是全部请求都直接访问数据库,从而导致数据库压力骤增,严重可能发生一系列连锁反应,造成整个系统崩溃

解决方案:

  • 缓存击穿:
    • 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么返回空值或默认值
    • 不给热点数据设置过期时间,有后台异步更新缓存,或者在热点数据准备过期前,提前通知后台线程更新缓存以及重新设置过期时间
  • 缓存穿透:
    • 非法请求限制:当有大量恶意请求访问不存在的数据时,也会发生缓存穿透,因此在 API 入口处我们要判断请求参数是否合理,请求参数是否含有非法值,请求字段是否存在,如果判断出是恶意请求就直接返回错误
    • 缓存空值或默认值:发现缓存穿透现象时,如果请求正常,可以单独构建一个空值或默认值的缓存
    • 可以设置布隆过滤器:在写入数据库时,使用布隆过滤器做个标记,在用户请求到来时,业务确认缓存失效后,通过布隆过滤器判断数据库中是否有该记录,有才查询,没有就不用查询

多级缓存如何保证数据一致性?

  • 方案一:通过 redis 过期时间更新缓存,MySQL 数据库更新不会触发 redis 更新,只有当 redis key 过期后才会重新加载
    • 缺点 1:数据不一致的时间较长,会造成脏数据
    • 缺点 2:完全依赖过期时间,过期时间太短缓存更新太频繁,太长容易有长时间的更新延迟
  • 方案二:在方案一的基础上,用 key 的过期时间做兜底,在更新 MySQL 的同时也会更新 redis。
    • 缺点:如果 MySQL 更新成功,redis 更新失败,则会出现和方案一一样的问题
  • 方案三:在方案二的基础上,使用消息队列,异步更新 redis
    • 缺点 1:解决不了时序的问题,如果多个业务实例同时更新同一条数据,数据更新的先后顺序可能会乱
    • 缺点 2:引入 MQ,增加了系统的复杂度
  • 方案四:将 MySQL 和 redis 更新放在同一个事务中,保证强一致性
    • 缺点 1:更新时任何环节出问题,都将导致回滚或撤销
    • 缺点 2:如果网络出现超时,不仅可能造成数据回滚或撤销,还可能发生并发问题
  • 方案五:订阅 MySQL 的 binlog 日志来更新 redis,把我们搭建的 mq 消费服务,作为 mysql 的一个 slave,订阅 Binlog,解析出更新的内容,再更新 redis
    • 缺点:单独搭建一个同步服务,引入 binlog 同步机制,成本较大

Redis 分片集群是如何分片的?有什么好处?

当 Redis 的缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster)方案,它将数据分布在不同的服务器上,以此来降低对单主节点的依赖,从而提高 Redis 的读写性能。

Redis Cluster 使用哈希槽(Hash Slot)来处理数据和节点的映射关系。

在 Redis Cluster 方案中,一个切片集群有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步:

  • 根据键值对的 key,按照 CRC16 算法[1]计算一个 16bit 的值
  • 用 16bit 的值对 16384 取模,得到 [0,16384) 范围内的模数,每个模数代表相应编号的哈希槽,通过哈希槽找到所属的节点

Redis 持久化方式有哪些?

Redis 的读写操作都是在内存中执行,所以性能很好,但是一旦 Redis 重启,内存中的数据就会丢失,为了保证数据不丢失,Redis 通过持久化方式将数据保存到磁盘,这样 Redis 重启后就能从磁盘中恢复数据。有两种方式:

  • RDB 文件:RDB 是内存快照,将某个时刻的内存数据,以二进制的方式写入内存
  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写到一个日志里
  • 混合持久化方式:Redis4.0 新增的方式,集成了 AOF 和 RDB 的优点

RDB 文件如何实现的?

RDB 文件保存的是某个时刻的内存里的实际数据,而 AOF 文件保存的是写操作的命令。在故障恢复时,RDB 的恢复效率比 AOF 高一些,因为直接将 RDB 文件写入内存中就可以,不用像 AOF 那样额外执行写操作命令。

有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE[2]。他们的区别在于是否在主线程执行:

  • SAVE 命令在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • BGSAVE 命令会单独生成一个子进程来生成 RDB 文件,可以避免阻塞主线程

AOF 文件如何实现持久化?

AOF 持久化是通过保存 Redis 数据库的写命令来记录数据库状态的。有以下三步:

  1. 命令追加:服务在执行完一个写操作命令后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾
  2. 如客户端向服务端发送命令 set key value,会将以下协议追加到 aof_buf 缓冲区末尾
  3. 3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
  4. 文件写入 + 文件同步
  5. Redis 服务器进程就是一个事件循环,所以每次写命令执行完后,都会调用 flushAppendOnlyFile() 函数,考虑是否要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里
  6. flushAppendOnlyFile 函数的行为由服务器配置 Redis.conf 的 appendfsync 选项值决定,不同值的行为如下:
    • always:将 aof_buf 缓冲区中的所有内容写入并同步 AOF 文件
    • everysec:将 aof_buf 缓冲区中的所有内容写入 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟,则再次同步 AOF 文件,即将 AOF 文件中的命令执行,回写到硬盘。这个同步操作由一个专门的线程执行
    • no:将 aof_buf 缓冲区中的所有内容写入 AOF 文件,但同步的时机由操作系统决定

参考

  1. Cyclic redundancy check:https://en.wikipedia.org/wiki/Cyclic_redundancy_check
  2. Redis 设计与实现,黄健宏