> 文章列表 > Redis 面试题总结

Redis 面试题总结

Redis 面试题总结

Redis

  • 1、说说什么是 Redis
  • 2、Redis 有什么优点和缺点
  • 3、Redis 的数据类型有哪些
  • 4、Redis 为什么设计成单线程的
  • 5、Redis 和 Memcached 的区别有哪些
  • 6、请说说 Redis 的线程模型
  • 7、为什么 Redis 单线程模型也能效率这么高
  • 8、Redis 有哪些使用场景
  • 9、什么是 Redis 事务
  • 10、Redis 事务的注意点有哪些
  • 11、Redis 有几种持久化方式
  • 12、说说 RDB 的优缺点
  • 12、说说 AOF 的 优缺点
  • 13、两种持久化方式该如何选择
  • 14、面试官追问那如果突然机器断电会怎样
  • 15、面试官追问 bgsave 的原理是什么
  • 16、Redis 有几种数据过期策略
  • 17、Redis 有哪几种数据淘汰策略
  • 18、一个字符串类型的值能存储最大容量是多少
  • 19、什么是主从复制
  • 20、主从复制原理
  • 21、为什么主从全量复制使用RDB而不是AOF
  • 22、主从复制方案有什么痛点
  • 23、什么是哨兵模式
  • 24、Redis 的哨兵有什么功能
  • 25、Sentinel 如何检测节点是否下线
  • 26、Sentinel 如何选择出新的 master
  • 27、如何从 Sentinel 集群中选择出 Leader
  • 28、如何使用 Redis Cluster 实现高可用
  • 29、说说 Redis 哈希槽的概念
  • 30、你知道有哪些 Redis 分区实现方案
  • 31、Redis 哨兵和集群的区别是什么
  • 32、什么是缓存穿透?怎么解决
  • 33、什么是缓存击穿怎么解决
  • 34、什么是缓存雪崩?怎么解决?
  • 35、如何保证缓存与数据库的数据一致性
  • 36、如何使用 Redis 实现分布式锁
  • 37、Redis 和 Zookeeper 实现的分布式锁有什么区别
  • 38、Redis Pipelining
  • 39、如何使用 Redis 实现分布式限流
  • 40、如何使用 Redis 实现消息队列
  • 41、缓存命中率表示什么
  • 42、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,全部找出来
  • 43、如何提高 Redis 命中率
  • 44、怎么优化 Redis 的内存占用
  • 45、对 Redis 进行性能优化,有些什么建议

1、说说什么是 Redis

Redis 是一个基于内存且支持持久化的高性能 key-value 数据库。具备以下三个基本特征:

  • 多数据类型:字符串、哈希表、列表、集合、有序集合,位图,HyperLogLogs,Geospatial
  • 持久化机制:RDB、AOF
  • 主从复制和哨兵两种部署模式,提供高可用
  • 集群部署,提供高并发

2、Redis 有什么优点和缺点

Redis 优点

  • 数据结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。
  • 支持数据持久化,支持 AOF 和 RDB 两种持久化方式。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
  • 读写性能优异, Redis 能读的速度是 110000 次 /s,写的速度是 81000 次 /s。
  • 支持事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。

Redis 缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

3、Redis 的数据类型有哪些

String、Hash、List、Sets、Zset、Bitmaps、HyperLogLogs、Geospatial。

4、Redis 为什么设计成单线程的

绝大部分请求是纯粹的内存操作(非常快速),采用单线程,避免了不必要的上下文切换和竞争条件。

5、Redis 和 Memcached 的区别有哪些

  • Redis 和 Memcache 都是将数据存放在内存中,都是内存数据库。不过 Memcache 还可用于缓存其他东西,例如图片、视频等等。
  • Memcache 仅支持 key-value 结构的数据类型,Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,hash 等数据结构的存储。
  • 存储数据安全– Memcache 挂掉后,数据没了;Redis 可以定期保存到磁盘(持久化)。
  • Memcache 的单个 value 最大 1m , Redis 的单个 value 最大 512m 。
  • Redis 原生就支持集群模式, Redis3.0 版本中,官方便能支持 Cluster 模式了, Memcached 没有原生的集群模式,需要依赖客户端来实现,然后往集群中分片写入数据。
  • Memcached 网络 IO 模型是多线程,非阻塞 IO 复用的网络模型,原型上接近于 nignx 。而 Redis 使用单线程的 IO 复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现类 epoll,kqueue 和 select ,更接近于 Apache 早期的模式。

6、请说说 Redis 的线程模型

Redis 内部使用文件事件处理器,这个事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事件来选择对应的事件处理器进行处理。

Redis 面试题总结

  • 多个 socket 。
  • IO 多路复用程序。
  • 文件事件分派器。
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

客户端与Redis通信的一次完整流程

  1. 在 redis 启动初始化的时候,redis会将连接应答处理器跟 AE_READABLE 事件关联起来,接着如果一个客户端跟 redis 发起连接,此时会产生一个 AE_READABLE 事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的 socket,同时将这个 socket 的 AE_READABLE 事件跟命令请求处理器关联起来。

  2. 当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在 socket 产生一个 AE_READABLE 事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从 socket 中读取请求相关数据,然后进行执行和处理。

  3. 接着 redis 这边准备好了给客户端的响应数据之后,就会将 socket 的 AE_WRITABLE 事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在 socket 上产生一个 AE_WRITABLE 事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入 socket,供客户端来读取。

  4. 命令回复处理器写完之后,就会删除这个 socket 的 AE_WRITABLE 事件和命令回复处理器的关联关系。

7、为什么 Redis 单线程模型也能效率这么高

  • C 语言实现,效率高。
  • 纯内存操作。
  • 基于非阻塞的 IO 复用模型机制。
  • 单线程的话就能避免多线程的频繁上下文切换问题。
  • 丰富的数据结构,并且对数据存储进行了一些优化,比如跳表。

8、Redis 有哪些使用场景

常见 Redis 的使用场景如下:

缓存数据

Redis 提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在 Redis 用在缓存的场合非常多。

排行榜

很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis 提供的有序集合数据类构能实现各种复杂的排行榜应用。

计数器

如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给 + 1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。

分布式会话

集群模式下,在应用不多的情况下一般使用容器自带的 session 复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以 Redis 等内存数据库为中心的 session 服务,session 不再由容器管理,而是由 session 服务及内存数据库管理。

分布式锁

分布式锁实现方案,常见有三种:数据库,Redis、zookeepr。Redis 就是其中之一。

如全局 ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用 Redis 的 setnx 功能来编写分布式的锁,如果设置返回 1 说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。

社交网络

点赞、踩、关注 / 被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis 提供的哈希、集合等数据结构能很方便的的实现这些功能。

最新列表

Redis 列表结构,LPUSH 可以在列表头部插入一个内容 ID 作为关键字,LTRIM 可用来限制列表的数量,这样列表永远为 N 个 ID,无需查询最新的列表,直接根据 ID 去到对应的内容页即可。

消息系统

消息队列主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis 提供了发布 / 订阅及阻塞队列功能,能实现一个简单的消息队列系统 。但 Redis 不是一个专业的消息队列。建议使用其他消息队列:Kafka、RocketMQ、RabbitMQ 等。

9、什么是 Redis 事务

可以一次性执行多条命令,本质上是一组命令的集合。一个事务中的所有命令都会序列化,然后按顺序地串行化执行,而不会被插入其他命令 。

Redis 的事务相关命令有:

  • DISCARD:取消事务,放弃执行事务块中的所有命令
  • EXEC:执行事务块中的命令
  • MULTI:标记一个事务的开始
  • UNWATCH:取消 WATCH 命令对所有 key 的监视
  • WATCH key [key…]:监视一个(或多个)key,如果在事务之前执行这个(或者这些)key 被其他命令所改动,那么事务将会被打断。

10、Redis 事务的注意点有哪些

不支持回滚,如果事务中有错误的操作,无法回滚到处理前的状态,需要开发者处理。

11、Redis 有几种持久化方式

Redis 提供了两种方式,实现数据的持久化到硬盘。

  • 【全量】RDB 持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork
    一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
  • 【增量】AOF 持久化,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

12、说说 RDB 的优缺点

优点

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

缺点

  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑。
  • 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

12、说说 AOF 的 优缺点

优点

  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。

缺点

  • 比起 RDB 占用更多的磁盘空间。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别 Bug,造成恢复不能。

13、两种持久化方式该如何选择

bgsave 做镜像全量持久化,AOF 做增量持久化。因为 bgsave 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。在 Redis 实例重启时,会使用 bgsave 持久化文件重新构建内存,再使用 AOF 重放近期的操作指令来实现完整恢复重启之前的状态。

一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用 RDB 持久化。

有很多用户都只使用 AOF 持久化,但并不推荐这种方式:因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的问题。

14、面试官追问那如果突然机器断电会怎样

取决于 AOF 日志 sync 属性的配置,如果不要求性能,在每条写指令时都 sync 一下磁盘,就不会丢失数据。但是在高性能的要求下每次都 sync 是不现实的,一般都使用定时 sync ,比如 1 秒 1 次,这个时候最多就会丢失 1 秒的数据。实际上,极端情况下,是最多丢失 2 秒的数据。因为 AOF 线程,负责每秒执行一次 fsync 操作,操作完成后,记录最后同步时间。主线程,负责对比上次同步时间,如果超过 2 秒,阻塞等待成功。

15、面试官追问 bgsave 的原理是什么

fork 和 cow 。fork 是指 Redis 通过创建子进程来进行 bgsave 操作。cow 指的是 copy on write ,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。这里 bgsave 操作后,会产生 RDB 快照文件。

16、Redis 有几种数据过期策略

假设我们设置了一批 key 只能存活一个小时,那么接下来一小时后,redis 是怎么对这批 key 进行删除的?

A:定期删除 + 惰性删除

定期删除:指的是 redis 默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果其就删除。假设 redis 里放了10万个 key,都设置了过期时间,每隔几百毫秒,就检查10万个key,那 redis 基本就挂了,cpu负载会很高,消耗在检查过期key上了。因此,实际上 redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。

问题:定期删除可能导致很多过期 key 到了时间并没有被删除掉。解决:惰性删除。

惰性删除:并不是key到时间就被删除掉,而是查询这个 key 的时候,redis再懒惰地检查一下

但是这实际上还有问题,如果定期删除漏掉了很多过期key,然后也没有及时去查,也就没走惰性删除,此时大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,怎么办?

A:走内存淘汰机制。

17、Redis 有哪几种数据淘汰策略

如果 redis 的内存占用过多,此时会进行内存淘汰,有如下一些策略:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错(没人用)。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key(少用)。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

18、一个字符串类型的值能存储最大容量是多少

512M

19、什么是主从复制

Redis 主机会一直将自己的数据复制给 Redis 从机,从而实现主从同步。在这个过程中,只有 master 主机可执行写命令,其他 salve 从机只能只能执行读命令,这种读写分离的模式可以大大减轻 Redis 主机的数据读取压力,从而提高了Redis 的效率,并同时提供了多个数据备份。主从模式是搭建 Redis Cluster 集群最简单的一种方式。
Redis 面试题总结

20、主从复制原理

Redis 的主从同步 (replication) 机制,允许 Slave 从 Master 那里,通过网络传输拷贝到完整的数据备份,从而达到主从机制。

  1. Slave 从服务器启动成功连接到 Master 后会发送一个 sync 命令,主动进行数据同步,此时称为全量复制,只要是重新连接 Master,全量复制将被自动执行。
  2. Master 主服务器接到命令后先进行数据的持久化,然后把持久化的文件发送给 Slave,拿到持久化文件后进行读取,完成数据同步。
  3. Master 继续将新的所有收集到的修改命令依次传给 Slave,完成同步,此时称为增量复制

通过 Redis 的复制功,能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。

21、为什么主从全量复制使用RDB而不是AOF

  • RDB文件存储的内容是经过压缩的二进制数据,文件很小。AOF文件存储的是每一次写命令通常会必RDB文件大很多。因此,传输RDB文件更节省带宽,速度也更快。
  • 使用RDB文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而AOF则需
    要依次执行每个写命令,速度非常慢。也就是说,与AOF相比,恢复大数据集的时候,RDB速度更快。

22、主从复制方案有什么痛点

主从复制方案下,master 发生宕机的话可以手动将某一台 slave 升级为master,Redis服务可用性提高。整个过程需要人工干预。人工干预大大增加了问题的处理时间以及出错的可能性。

这个时候你肯定在想:如果能够自动化地完成故障切换就好了!我们后面介绍的 Redis Sentinel(哨兵) 就可以帮
助我们来解决这个痛点。

另外,主从复制方案在高并发场景下能力有限。如果缓存的数据量太大或者并发量要求太高,主从复制就没办法满
足我们的要求了。

主从复制和 Redis Sentinel 这两种方案都不支持横向扩展来缓解写压力以及解决缓存数据量过大的问题。我们后
面介绍的 Redis Cluster(官方切片集群解决方案)就可以帮助我们来解决这个痛点。

23、什么是哨兵模式

在 Redis 主从复制模式中,因为系统不具备自动恢复的功能,所以当主服务器(master)宕机后,需要手动把一台从服务器(slave)切换为主服务器。在这个过程中,不仅需要人为干预,而且还会造成一段时间内服务器处于不可用状态,同时数据安全性也得不到保障,因此主从模式的可用性较低,不适用于线上生产环境。

Redis 官方推荐一种高可用方案,也就是 Redis Sentinel 哨兵模式,它弥补了主从模式的不足。Sentinel 通过监控的方式获取主机的工作状态是否正常,当主机发生故障时, Sentinel 会自动进行 Failover(即故障转移),并将其监控的从机提升主服务器(master),从而保证了系统的高可用性。

哨兵模式是一种特殊的模式,Redis 为其提供了专属的哨兵命令,它是一个独立的进程,能够独立运行。下面使用 Sentinel 搭建 Redis 集群,基本结构图如下所示:

在这里插入图片描述

24、Redis 的哨兵有什么功能

  • 集群监控,监控所有redis节点(包括sentinel节点自身)的状态是否正常。
  • 故障转移,如果 Master node 挂掉了,会自动转移到 Slave node 上,确保整个Redis系统的可用性。
  • 消息通知,通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave。.
  • 配置提供,如果故障转移发生了,通知 Client 客户端新的 Master 地址。

25、Sentinel 如何检测节点是否下线

Redis Sentinel 中有两个下线的概念:

  • 主观下线:sentinel节点认为某个Redis节点已经下线了(主观下线),但还不是很确
    定,需要其他sentinel节点的投票。
  • 客观下线:法定数量(通常为过半)的sentinel节点认定某个Redis节点已经下线(客 观下线),那它就算是真的下线了。

也就是说,主观下线当前的sentinel自己认为节点宕机,客观下线是sentinel整体达成一致认为节点宕
机。
Redis 面试题总结

默认情况下,Sentinel哨兵会以每秒一次的频率向所有与它创建命令连接的实例(包括主服务器、从服务器、其他Sentinel)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

如果对应的节点超过规定的时间(down-after-millisenconds)没有进行有效回复的话,就会被其认定为是主观下线(SDOWN)。注意!这里的有效回复不一定是PONG,可以是 -LOADING 或者 -MASTERDOWN。

Redis 面试题总结

如果被认定为主观下线的是 slave 的话,sentinel 不会做什么事情,因为 slave下线对Redis集群的影响
不大,Redis集群对外正常提供服务。但如果是master被认定为主观下线就不一样了,sentinel 整体还要
对其进行进一步核实,确保 master 是真的下线了。

所有 sentinel 节点要以每秒一次的频率确认 master 的确下线了,当法定数量(通常为过半)的 sentinel
节点认定 master 已经下线,master才被判定为客观下线(ODOWN)。这样做的目的是为了防止误判,
毕竟故障转移的开销还是比较大的,这也是为什么Redis官方推荐部署多个sentinel节点(哨兵集群)。

Redis 面试题总结

随后,sentinel 中会有一个 Leader 的角色来负责故障转移,也就是自动地从 slave 中选出一个新的
master 并执行完相关的一些工作(比如通知slave新的master连接信息,让它们执行replicaof成为新的
master的slave)。

如果没有足够数量的 sentinel 节点认定 master 已经下线的话,当 master 能对 sentinel 的PlNG命令进
行有效回复之后,master也就不再被认定为主观下线,回归正常。

26、Sentinel 如何选择出新的 master

故障恢复是指主机down掉需要从机来替代它工作,故障恢复选择条件依次为

  1. 选择优先级靠前的(优先级:在 redis.conf 中默认 slave-priority 100,值越小优先级越高)。
  2. 优先级相同时,选择偏移量最大的(偏移量:指从机获得主机数据最全的概率)。
  3. 偏移量也相同时候,选择runid最小的(runid:每个 redis 实例启动后都会随机生成一个 40 位的 runid)。

27、如何从 Sentinel 集群中选择出 Leader

我们前面说了,当 sentinel 集群确认有master客观下线了,就会开始故障转移流程,故障转移流程的第一步就是在sentinel集群选择一个 leader,让 leader 来负责完成故障转移。

如何选择出Leader角色呢?

大部分共识算法都是基于 Paxos 算法改进而来,在 sentinel 选举 leader 这个场景下使用的是 Raft 算法。

28、如何使用 Redis Cluster 实现高可用

Redis Cluster 是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。

Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。

29、说说 Redis 哈希槽的概念

Redis Cluster 没有使用一致性 hash ,而是引入了哈希槽的概念。

Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。因为最大是 16384 个哈希槽,所以考虑 Redis 集群中的每个节点都能分配到一个哈希槽,所以最多支持 16384 个 Redis 节点。

为什么是 16384 呢?

主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。

30、你知道有哪些 Redis 分区实现方案

虚拟槽哈希分区(Redis Cluster采用)
Redis 面试题总结

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

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

Redis 的集群的功能是为了解决单机 Redis 容量有限的问题,将数据按一定的规则分配到多台机器,对内存的每秒访问不受限于单台服务器,可受益于分布式集群高扩展性。

32、什么是缓存穿透?怎么解决

缓存穿透是指当用户查询某个数据时,Redis 中不存在该数据,也就是缓存没有命中,此时查询请求就会转向持久层数据库 MySQL,结果发现 MySQL 中也不存在该数据,MySQL 只能返回一个空象,代表此次查询失败。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给 MySQL 数据库造成很大压力,甚至于崩溃,这种现象就叫缓存穿透。

Redis 面试题总结

解决方案

1) 缓存空对象

当 MySQL 返回空对象时, Redis 将该对象缓存起来,同时为其设置一个过期时间。当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数库,最长不超过五分钟。

2)设置可访问的名单(白名单)

使用 bitmaps 类型定义一个可以访问的名单,名单 id 作为 bitmaps 的偏移量,每次访问和 bitmap 里面的 id 进行比较,如果访问 id 不在 bitmaps 里面,进行拦截,不允许访问。

3) 布隆过滤器

布隆过滤器判定不存在的数据,那么该数据一定不存在,利用它的这一特点可以防止缓存穿透。

首先将用户可能会访问的热点数据存储在布隆过滤器中(也称缓存预热),当有一个用户请求到来时会先经过布隆过滤器,如果请求的数据,布隆过滤器中不存在,那么该请求将直接被拒绝,否则将继续执行查询。相较于第一种方法,用布隆过滤器方法更为高效、实用。其流程示意图如下:

Redis 面试题总结

缓存预热:是指系统启动时,提前将相关的数据加载到 Redis 缓存系统中。这样避免了用户请求的时再去加载数据。

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量 (位图) 和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

4) 进行实时监控

当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

33、什么是缓存击穿怎么解决

  1. Reids 中并没有出现大量的 key 过期,Redis 在正常的运行。
  2. Redis 的某个 key 过期了,并且同一时间大量的请求访问这个 key,也就是说这个 key 是热点 key。
  3. 数据库的压力瞬时增加。

key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。

Redis 面试题总结

解决方案

key 可能会在某些时间点被超高并发地访问,是一种非常 “热点” 的数据。

1)预先设置热门数据,改变过期时间

在 redis 高峰访问之前,把一些热门数据提前存入到 redis 里面,加大这些热门数据 key 的时长。现场监控哪些数据热门,实时调整 key 的过期时长。

2) 分布式锁

采用分布式锁的方法,重新设计缓存的使用方式,过程如下:

  • 上锁:当我们通过 key去查询数据时,首先查询缓存,如果没有,就通过分布式锁进行加锁,第一个获取锁的进程进入后端数据库查询,并将查询结果缓到Redis中。
  • 解锁:当其他进程发现锁被某个进程占用时,就进入等待状态,直至解锁后,其余进程再依次访问被缓存的 key。

Redis 面试题总结

34、什么是缓存雪崩?怎么解决?

缓存雪崩是指缓存中大批量的 key 同时过期,而此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉,这种现象被称为缓存雪崩。它和缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。

Redis 面试题总结
解决方案

1)构建多级缓存架构

nginx 缓存 + redis 缓存 + 其他缓存(ehcache 等)。

2)使用锁或队列

用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,该方法不适用高并发情况。

3)设置过期标志更新缓存

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。

4)将缓存失效时间分散开

比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

35、如何保证缓存与数据库的数据一致性

如何保证缓存和数据库一致性?很多人对这个问题依然有很多疑惑:

  • 到底是更新缓存还是删除缓存?
  • 选择先更新数据库再删除缓存,还是先删除缓存再更新数据库?
  • 为什么要引入消息队列保证一致性?
  • 延迟双删会产生哪些问题?到底要不要用?如何用?

引入缓存提高性能

我们从最简单的场景开始说起。

如果项目业务处于起步阶段,流量非常少,读写请求直接操作数据库即可,此时你的架构模型是这样

Redis 面试题总结
但是随着业务量的增长,你的项目请求量越来越大,如果每次都从DB中读取,那必然会产生性能问题。通常这个阶段会引入【缓存】来提高读写性能,架构模型就会发生转变:
Redis 面试题总结
目前主流的缓存中间件,当属Redis,不仅性能高,还支持多种数据类型,能更好的满足我们的业务需求。但是加入缓存之后,就会面临这样的一个问题:之前数据只存放在数据库中,从数据库读取,现在要放到缓存中读取,具体要存储什么呢?

最简单直接的方案就是【全量数据刷到缓存中】

  • 数据库的数据,全量刷到缓存中(不设置失效时间)
  • 写请求只更新数据库,不更新缓存
  • 启动一个定时任务,定时将数据库中的数据,同步更新到缓存中

Redis 面试题总结
这个方案的有点不必多说,所有请求都全部【命中】缓存,不需要经过数据库,性能非常高。但是缺点也很明显,主要体现以下两点。

  • 缓存利用率低,不是所有的缓存都是热点,不经常访问的数据长期存放在缓存中,会导致资源浪费
  • 数据不一致,因为采取的是【定时刷新缓存】的机制,导致缓存数据与数据库数据不一致(取决于定时刷新的频率)

所以,这个方案适用于【体量小】的业务,对数据一致性要求不高的业务场景。那么,针对体量很大的业务场景,怎么解决这两个问题呢?

缓存利用率和一致性问题

先来看第一个问题,如何提高缓存利用率的问题。说到这,想要缓存利用率【最大化】,我们很容易能想到的方案是,缓存中只保留最近访问或经常访问的【热点数据】。我们可以这样优化:

  • 写请求依旧只写数据库
  • 读请求先读缓存,如果缓存不在,则从数据库读取,并重建缓存
  • 同时,写入缓存中的数据,都设置失效时间(错开失效时间)

Redis 面试题总结
这样一来,缓存中不经常访问的key,随着时间的推移,都会时间【过期】淘汰掉,最终缓存中保留的,都是热点数据,从而缓存的利用率得以最大化。

再看数据一致性的问题。

想要保证缓存和数据库【实时】一致,那就不能再使用定时任务刷新缓存的方案。所以,当数据发生更新时,我们不仅要操作更新数据库,还要一并操作缓存。具体的操作就是修改一条数据时,不仅要更新数据库,连带着缓存一起更新,或者删除相应的缓存再次访问时是会查询数据库后进行重建缓存。

但数据库和缓存都更新,有存在先后的问题,那对应的方案就有2个:

  1. 先更新缓存,后更新数据库
  2. 先更新数据库,后更新缓存

哪一个方案更好呢?

这里先不考虑并发的问题,正常情况下,无论谁先谁后,都可以让两者保持一致,但现在我们需要重点考虑【异常】情况。因为操作是分两步,那么就有可能在【第一个成功,第二个失败】的情况发生。

1、先更新缓存,后更新数据库

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但是数据库中是【旧值】,虽然此时请求可以命中缓存,拿到正确的值,但是一旦缓失,就会从数据库中读取到【旧值】,重建缓存也是这个旧值。这时用户就会发现自己之前的修改【又变回去了】,对业务造成影响。

2、先更新数据库,后更新缓存

如果更新数据库成功了,但是缓存更新失败了,那么此时数据库中是最新的值,缓存中是【旧值】。之后的读请求都读到的是旧数据,只有当缓存【失效】后,才能从数据库中得到正确的值。这时用户就会发现,自己刚刚修改了数据,但发现不生效,过一段时间后,数据才变更过来,对业务也会有影响。

可见,无论是谁先谁后,但凡后者发生异常,就会对业务造成影响。那么怎样解决这个问题呢?我们继续分析,除了操作失败问题,还有什么场景会影响数据一致性?

这里我们还要重点关注:并发问题

并发引起的一致性问题

假如我们采用【先更新数据库,在更新缓存】的方案,并且两步都可以成功执行的前提下,如果存在并发,会是什么情况呢?

有线程A 和线程 B 两个线程,需要更新【同一条数据】,会发生这样的场景:

  1. 线程A 更新数据库(X=1)
  2. 线程B 更新数据库(X=2)
  3. 线程B 更新缓存(X=2)
  4. 线程A 更新缓存(X=1)

最终 X 的值在缓存中是1 ,数据库中是2,发生不一致。也就是说,A虽然先于B发生,但B操作数据库和缓存的时间,却要比B的时间更短,执行时序发生【错乱】,最终导致这条数据结果不符合预期的。

同样的,采用【新更新缓存,在更新数据库】的方案,也会有类似的问题。

除此之外,我们从【缓存利用率】的角度来评估这个方案,也是不太难推敲的。这是因为每次数据发生变更,都更新缓存,但是缓存中的数据不一定会被马上读取,这就会导致缓存中存放了很多不常访问的数据,浪费缓存资源。

而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很可能先查数据库,经过一系列计算得出的值,才把这个值写到缓存中。由此可见,这种【更新数据库+更新缓存】的方案,不仅缓存利用率不到,还会造成服务器资源和性能的浪费。

所以此时我们需要考虑另外一种方案:删除缓存

删除缓存可以保证一致性吗

删除缓存对应的方案也有 2 种:

  1. 先删除缓存,再更新数据库
  2. 先更新数据库,再删除缓存

经过前面的分析我们可以知道,但凡【第二步】操作失败,都会导致数据不一致

1、先删除缓存,后更新数据库

如果有 2 个线程要并发【读写】数据,可能会发生以下场景:

  1. 线程A 要更新 X=2 (原值X=1)
  2. 线程A 先删除缓存
  3. 线程B 读缓存,发现不在,从数据库中读取到旧值(X=1)
  4. 线程A 将新值写入数据库(X=2)
  5. 线程B 将旧值写入缓存(X=1)

最终 X 的值在缓存中是1(旧值),在数据库中是2(新值),发生不一致。可见,先删除缓存,后更新数据库,当发生【读+写】并发时,还是存在数据不一致的情况。

2、先更新数据库,后删除缓存

依旧是两个线程【并发读写】数据 :

  1. 缓存中 X 不存在(数据库中 X=1)
  2. 线程A 读取数据库,得到旧值(X=1)
  3. 线程B 更新数据库(X=2)
  4. 线程B 删除缓存
  5. 线程A 将旧值写入缓存(X=1)

最终X的值在缓存中是1(旧值),在数据库中是2(新值),也发生不一致

这种情况理论来说是可能发生的,但实际中真有可能发生吗?其实概率很低,这是因为它必须满足 3 个条件:

  1. 缓存刚已失效
  2. 读写请求并发
  3. 更新数据库 + 删除缓存的时间(步骤3~4),要比读数据库 + 写缓存的时间短(步骤2和5)

仔细想一下,条件3发生的概率是非常低的。因为写数据库一般会先【加锁】,所以写数据库,通常是要比读数据库的时间更长的。这么看来,【先更新数据库 + 再删除缓存】的方案,是可以保证数据一致性的。

所以,我们应该采用这种方案(【先更新数据库 + 再删除缓存】)来操作数据库和缓存。嗯,解决了并发问题,我们继续来看前面遗留的,第二步执行失败,导致数据不一致的问题

如何保证两步都执行

通过前面的分析,无论是更新缓存还是删除缓存,只要第二步出现失败,就会导致数据库和缓存的结果不一致。

保证第二步成功执行,就是解决问题的关键,程序在执行过程中发生异常,最简单的解决办法是:重试

但这并不意味着,只要执行失败(出现异常),我们重试就可以了。实际情况往往没那么简单,失败后立即重试的问题在于:

  • 立即重试很大概率还会失败
  • 重试次数设置多少才合理
  • 重试会一直占用这个线程资源,无法服务其他客户端请求

由此可见了,虽然我们想通过重试的方式解决问题,但是这种【同步重试】的方案依旧不严谨。那么另一种更好的方案是:异步重试

异步重试,其实就是把重试请求放到【消息队列】中,然后由专门的消费者来进行重试,直到成功。或者更直接的做法,为了避免第二次执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。这里你可能会疑惑,写队列也有可能会失败,而且引入消息队列,这又会增加了更多的维护成本,增加项目复杂度,这样做是否值得?

这是个好问题,抛开项目复杂度,我们思考这样一个问题:如果在执行失败的线程中一直重试,还没等执行成功,此时如果项目重启了,那么重试的请求就丢失了,这一条数据就不一致了。所以,我们必须将重试或者第二步骤放到另一个服务中,这个服务用【消息队列】最为合适,因为消息队列的特性,可以满足我们的需求:

  • 消息队列可靠性:写到队列中的消息,成功消费之前不会丢失(重启也不担心)
  • 消息队列保证消息成功投递:消费者从队列拉取消息,成功消费后才会删除(message_id),否则还会继续投递消息给消费者(符合重试场景)

至于写队列失败和消息队列成本维护问题:

  • 写入队列失败:操作缓存和写消息队列,同时失败的概率是非常小的
  • 维护成本:达到一定量级,我们项目中都会使用消息队列,维护成本并没有增加很多

所以,引入消息队列来解决第二个步骤失败重试的问题,是比较合适的,这时候的架构就变成了这样:

Redis 面试题总结
如果不想在应用中去写消息队列,是否有更简单的方案,同时又可以保证一致性呢?方案还是有的,这就是近几年比较流行的解决方案:订阅数据库变更日志,再操作缓存

具体来说就是,业务在修改数据时,只需修改数据库,无需操作缓存。那什么时候操作缓存呢,这个就与数据库的【变更日志】有关当一条数据发生改变时,MySQL就会产生一条binlog(变更日志)我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。

Redis 面试题总结
订阅变更日志,目前也有比较成熟的开源中间件,例如阿里的canal,使用这种方案的有点在于:

  • 无需考虑写消息队列失败情况:只要写MySQL成功,Binlog肯定会有
  • 自动投递到下游队列:canal自动把数据库变更日志【投递】给下游消息队列

当然,于此同时,我们需要投入经历去维护canal的高可用和稳定性。

到这里,可以得出的结论,想要保证数据和缓存一致性,推荐采用【先更新数据库,再删除缓存】的方案,并且配合【消息队列】或【订阅变更日志】的方式来做

主从延迟和延迟双删问题

「读写分离 + 主从复制延迟」情况下,缓存和数据库一致性的问题。

在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」其实也会导致不一致:

线程 A 更新主库 X = 2(原值 X = 1)
线程 A 删除缓存
线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
从库「同步」完成(主从库 X = 2)
线程 B 将「旧值」写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。

看到了么?这 2 个问题的核心在于:缓存都被回种了「旧值」。

那怎么解决这类问题呢?

最有效的办法就是,把缓存删掉。

但是,不能立即删,而是需要「延迟删」,这就是业界给出的方案:缓存延迟双删策略。

线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。

这两个方案的目的,都是为了把缓存清掉,这样一来,下次就可以从数据库读取到最新值,写入缓存。

但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?

  • 问题1:延迟时间要大于「主从复制」的延迟时间
  • 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间

但是,这个时间在分布式和高并发场景下,其实是很难评估的。很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。所以你看,采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。

所以实际使用中,我还是建议你采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。

可以做到强一致性吗

如果想让缓存和数据到达到【强一致性】,其实很难做到的。这往往要 牺牲性能

一旦我们使用缓存,就必然会出现一致性的问题,性能与一致性,无法做到保持平衡。势必会向一方倾斜。如果非要达到强一致性,那就必须在完成更新操作之前,不能有任何请求处理,这在实际高并发的场景中是不可取的。

虽然也可以使用【分布式锁】来实现,但是加锁与释放的过程,也会降低其性能,有时候甚至会超过引入缓存带来的性能提升。

所以,我们既然决定使用缓存,就必须容忍【一致性】问题,我们只能尽可能地降低出现问题的概率

总结

  • 将要提高应用性能,可以引入缓存来解决
  • 引入缓存后,就要考虑缓存和数据库一致性的问题,建议,更新数据库,删除缓存
  • 更新数据库 + 删除缓存的方案,在并发场景下无法保证缓存与数据保持一致性,且存在缓存资源浪费和机器性能浪费的情况。
  • 并发场景下的延迟双删策略,这个延迟时间很难评估,所以推荐【先更新数据库,再删除缓存】的方案
  • 在【先更新数据库,再删除缓存】的方案下,为了保证两步都能执行成功,需要配合【消息队列】或【订阅变更日志】的方案来做,其本质是通过重试的方式保证数据一致性。
  • 在【先更新数据库,再删除缓存】方案下,【读写分离 + 主从延迟】也会导致缓存和数据库不一致,解次问题的方案是【延迟双删】,凭借经验发送【延迟消息】到队列中,延迟删除缓存,同时要也要控制主从库延迟(可以通过暂时剔除延迟高的节点,延迟低的时候再将节点加入集群),尽可能降低不一致发生的概率。

36、如何使用 Redis 实现分布式锁

使用 redis 实现分布式锁的思路:

  1. setnx(String key,String value)

若返回 1,说明设置成功,获取到锁;

若返回 0,说明设置失败,已经有了这个 key,说明其它线程持有锁,重试。

  1. expire(String key, int seconds)

获取到锁(返回 1)后,还需要用设置生存期,如果在多少秒内没有完成,比如发生机器故障、网络故障等,键值对过期,释放锁,实现高可用。

  1. del(String key)

完成业务后需要释放锁。释放锁有 2 种方式:del 删除 key,或者 expire 将有效期设置为 0(马上过期)。

在执行业务过程中,如果发生异常,不能继续往下执行,也应该马上释放锁。

如果你的项目中 Redis 是多机部署的,那么可以尝试使用 Redisson 实现分布式锁,这是 Redis 官方提供的 Java 组件。

37、Redis 和 Zookeeper 实现的分布式锁有什么区别

实现方式的不同,Redis 实现为去插入一条占位数据,而 ZK 实现为去注册一个临时节点。

遇到宕机情况时,Redis 需要等到过期时间到了后自动释放锁,而 ZK 因为是临时节点,在宕机时候已经是删除了节点去释放锁。

Redis 在没抢占到锁的情况下一般会去自旋获取锁,比较浪费性能,而 ZK 是通过注册监听器的方式获取锁,性能而言优于 Redis。

没有谁是最好的。

对于性能要求很高的建议使用 Redis 来实现,否则,建议使用 Zookeeper 来实现。

38、Redis Pipelining

Redis Pipelining可以一次发送多个命令,并按顺序执行、返回结果,节省RTT(Round Trip Time)。

39、如何使用 Redis 实现分布式限流

限流的目的是通过对并发访问 / 请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。

Redis 限流的实现方式有 3 种,分别是:

  • 基于 Redis 的 setnx 的操作,给指定的 key 设置了过期实践。
  • 基于 Redis 的数据结构 zset,将请求打造成一个 zset 数组。
  • 基于 Redis 的令牌桶算法,输出速率大于输入速率,就要限流。

40、如何使用 Redis 实现消息队列

Redis 的 list (列表) 数据结构常用来作为异步消息队列使用,使用 rpush/lpush 操作入队列,使用 lpop 和 rpop 来出队列。rpush 和 lpop 结合 或者 lpush 和 rpop 结合。

Redis 面试题总结
客户端是通过队列的 pop 操作来获取消息,然后进行处理。处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。

41、缓存命中率表示什么

缓存命中: 可以同缓存中获取到需要的数据
缓存不命中:缓存中无法获取所需数据,需要再次查询数据库或者其他数据存储载体。

缓存命中率 = 缓存中获取数据次数 / 获取数据总次数

42、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,全部找出来

使用 keys 指令可以扫出指定模式的 key 列表。

  • 对方接着追问:如果这个 Redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?
  • 这个时候你要回答 Redis 关键的一个特性:Redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

使用 SCAN 命令获取所有 key

SCAN 0 遍历返回结果,使用 MATCH 参数匹配前缀

SCAN 0 MATCH “prefix: *” 其中 prefix 为前缀,星号表示匹配任意字符

43、如何提高 Redis 命中率

  • 缓存预加载
  • 增加缓存存储量
  • 调整缓存存储数据类型
  • 提升缓存更新频次

44、怎么优化 Redis 的内存占用

可以通过以下六种方式来对 Redis 的内存优化:

  • redisObject 对象
  • 缩减键值对象
  • 共享对象池
  • 字符串优化
  • 编码优化
  • 控制 key 的数量

45、对 Redis 进行性能优化,有些什么建议

  • Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件。
  • Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
  • 尽量避免在压力很大的主库上增加过多的从库。
  • 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3… 。
  • Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。