Redis经典面试题

发布于:2025-05-11 ⋅ 阅读:(12) ⋅ 点赞:(0)

本篇文章简单介绍一些 Redis 常见的面试题。

Redis 是什么?

Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

Redis 是一个开源的、基于内存的键值存储系统,通常用作数据库、缓存和消息中间件。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。

Redis 的数据类型有哪些?

Redis 支持以下主要数据类型:

  • 字符串(String):最基本的类型,可以存储字符串、整数或浮点数。
  • 哈希(Hash):键值对的集合,适合存储对象。
  • 列表(List):字符串列表,按照插入顺序排序,支持在头部或尾部插入元素。
  • 集合(Set):无序且唯一的字符串集合。
  • 有序集合(Sorted Set):类似于集合,但每个元素关联一个分数,用于排序。

除此之外还有 Geospatial、Hyperloglog、Bitmap 等

Redis 的常见应用场景有哪些?

Redis 的常见应用场景包括:

  • 缓存:加速数据访问,减轻数据库压力。
  • 会话存储:存储用户会话信息(可以使用哈希、String或者Sorted Set实现)。
  • 排行榜:使用有序集合实现实时排行榜(使用的是Sorted Set这个数据类型,Sorted Set 底层维护了一个 socre 字段,通过这个字段可以)。
  • 消息队列:使用列表或发布订阅功能实现消息队列(Redis 其实最开始就是用来做消息队列的,但是由于 Redis 实现的消息队列比较简单,而且Redis 的缓存太好用了,就渐渐的用的越来越少,其他的 MQ 也很好用)。

Redis 持久化(Redis 保证数据不丢失

Redis 的持久化机制

Redis 提供了两种持久化机制:

  • RDB(Redis DataBase)持久化:快照方式持久化,将某一个时刻的内存数据,以二进制的方式写入磁盘;
  • AOF(Append Only File)持久化:文件追加持久化,记录所有非查询操作命令,并以文本的形式追加到文件中;
  • 混合持久化:RDB + AOF 混合方式的持久化,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
RDB 持久化:

优点:

  1. 速度快:相对于 AOF 持久化方式,RDB 持久化速度更快,因为它只需要在指定的时间间隔内将数据从内存中写入到磁盘上。
  2. 空间占用小:RDB 持久化会将数据保存在一个压缩的二进制文件中,因此相对于 AOF 持久化方式,它占用的磁盘空间更小。
  3. 恢复速度快:因为 RDB 文件是一个完整的数据库快照,所以在 Redis 重启后,可以非常快速地将数据恢复到内存中。
  4. 可靠性高:RDB 持久化方式可以保证数据的可靠性,因为数据会在指定时间间隔内自动写入磁盘,即使 Redis 进程崩溃或者服务器断电,也可以通过加载最近的一次快照文件恢复数据。

缺点:

  1. 数据可能会丢失:RDB 持久化方式只能保证数据在指定时间间隔内写入磁盘,因此如果 Redis 进程崩溃或者服务器断电,从最后一次快照保存到崩溃的时间点之间的数据可能会丢失。
  2. 实时性差:因为 RDB 持久化是定期执行的,因此从最后一次快照保存到当前时间点之间的数据可能会丢失。如果需要更高的实时性,可以使用 AOF 持久化方式。

所以,RDB 持久化方式适合用于对数据可靠性要求较高,但对实时性要求不高的场景,如 Redis 中的备份和数据恢复等。

AOF 持久化:

优点:

  1. 数据不容易丢失:AOF 持久化方式会将 Redis 执行的每一个写命令记录到一个文件中,因此即使 Redis 进程崩溃或者服务器断电,也可以通过重放 AOF 文件中的命令来恢复数据。
  2. 实时性好:由于 AOF 持久化方式是将每一个写命令记录到文件中,因此它的实时性比 RDB 持久化方式更好。
  3. 数据可读性强:AOF 持久化文件是一个纯文本文件,可以被人类读取和理解,因此可以方便地进行数据备份和恢复操作。

缺点:

  1. 写入性能略低:由于 AOF 持久化方式需要将每一个写命令记录到文件中,因此相对于 RDB 持久化方式,它的写入性能略低。
  2. 占用磁盘空间大:由于 AOF 持久化方式需要记录每一个写命令,因此相对于 RDB 持久化方式,它占用的磁盘空间更大。
  3. AOF 文件可能会出现损坏:由于 AOF 文件是不断地追加写入的,因此如果文件损坏,可能会导致数据无法恢复。

所以,AOF 持久化方式适合用于对数据实时性要求较高,但对数据大小和写入性能要求相对较低的场景,如需要对数据进行实时备份的应用场景。

混合持久化:

Redis 混合持久化是指将 RDB 持久化方式和 AOF 持久化方式结合起来使用,以充分发挥它们的优势,同时避免它们的缺点。

它的优缺点如下:

优点:

混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

缺点:

  1. 实现复杂度高:混合持久化需要同时维护 RDB 文件和 AOF 文件,因此实现复杂度相对于单独使用 RDB 或 AOF 持久化方式要高。
  2. 可读性差:AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
  3. 兼容性差:如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

所以,Redis 混合持久化方式适合用于,需要兼顾启动速度和减低数据丢失的场景。但需要注意的是,混合持久化的实现复杂度较高、可读性差,只能用于 Redis 4.0 以上版本,因此在选择时需要根据实际情况进行权衡。

Redis 集群

Redis 实现高可用

Redis 通过主从复制和哨兵机制实现高可用:

  • 主从复制:主节点将数据同步到从节点,从节点可以处理读请求,减轻主节点压力。
  • 哨兵机制:监控主从节点的状态,自动进行故障转移和主节点选举。
  • Redis Cluster:将数据分布在不同的服务区上,以此来降低系统对单主节点的依赖。

如果读写都存在一台机器上,那么单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。

主从复制

所以主从复制就是指:主节点用来写数据,其他从节点负责读数据,同时同步主节点的新的数据。

如果说主节点宕机了,那么就不能写数据了,还是可以继续读取数据的,如果从节点宕机了,还有其他从节点的话(一般都是一主多从的),还是可以继续读数据的。

哨兵

哨兵模式就是指,添加监视器,去监听各个节点是否宕机了,从节点宕机了没关系,只不过是其他从节点压力增大了(不会是读取功能失效)。主从同步存在一个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。 所以我们需要一个自动的工具——Redis Sentinel (哨兵模式) 来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复 (failover) 的能力。

如果是主节点宕机了,那么就会被哨兵监听到,随后该监听的哨兵就会发起 “选主” 操作,其余哨兵就会进行投票,票数多的选为新的主节点。

一般来说都是单数的哨兵,如果非要设置双数的哨兵,也可以让发起选主的哨兵的权重大一些,平票的问题也可以解决。

当然这个选主操作不是由首先发现的哨兵直接做出判断的(有可能做出一个误判),而是先进入一个 “ 主观 ” 下线,等到其他哨兵都确认了确实有问题了,就会将该节点标记为客观下线

如下图所示:

Cluster集群模式

单点存在的不足

  • Redis 单节点部署时面临的容量瓶颈、高可用性水平扩展问题,一主多从的部署写的能力还是会达到上限。
  • 单节点 Redis 的数据存储受限于单机内存(通常几十 GB),无法处理海量数据(如 TB 级缓存或日志存储)。单线程模型在高并发下易达到 CPU 上限,无法利用多核服务器资源。
  • 单节点无法通过添加机器分担负载,只能升级硬件(垂直扩展),成本高昂且有上限。

所以可以将多个 哨兵模式(不是多个哨兵)组合起来,该模式下存在多个主节点。

从上图可以看出 Redis 的主从同步只能有一个主节点,而 Redis Cluster 可以拥有无数个主从节点,因此 Redis Cluster 拥有更强大的平行扩展能力,也就是说当 Redis Cluster 拥有两个主从节点时,从理论上来讲 Redis 的性能相比于主从来说性能提升了两倍,并且 Redis Cluster 也有自动容灾恢复的机制。

Redis Cluster 是 Redis 官方提供的分布式解决方案,通过分片(Sharding)实现数据在多个节点之间的自动分片和负载均衡,解决了 Redis 单节点的容量和可用性瓶颈。

在Cluster 集群模式中引入了 哈希槽的概念:

  • 哈希槽(Hash Slot)
    Redis Cluster 将整个数据库分为16384 个哈希槽(Hash Slot),每个键通过CRC16(key) % 16384计算后映射到对应的槽。
  • 节点分配
    每个节点负责一部分哈希槽(例如,3 节点集群中每个节点约负责 5461 个槽)。节点动态添加 / 删除时,槽会自动迁移,无需重启集群。

Redis Cluster集群中,需要确保16384个槽对应的node都正常工作,如果某个node出现故障,它负责的slot也会失效,整个集群将不能工作。

Redis 关于脑裂问题

Redis 脑裂是指在分布式系统中,由于网络分区等原因,导致 Redis 集群中的部分节点与其他节点失去联系,从而形成多个独立的子集群,每个子集群都认为自己是主集群,进而引发数据不一致等问题。

例如图下所示:

这张图就很好的说明了问题:

  • 因为网络问题,导致原来的主节点(旧)并未同步新的主节点,旧的还不知道已经重新选主了;
  • 此时,原来连接了旧主节点的客户端还在往这里写数据,其他新连接的客户端向新节点写数据;等到网络恢复后,旧节点与新节点比较一下版本号,发现新的节点版本号比他大,它也就明白了已经进行了一次选主操作了;
  • 这个时候旧节点就降级为从节点,并向新的主节点进行请求同步数据,此时问题来了:那在网络故障的情况下,旧的客户端向 Redis 中写的数据怎么办呢?
  • 此时就发现数据丢失了。

在网络故障期间看似是有两个大脑,不同的客户端向不同的节点进行写数据。

脑裂的主要原因其实就是哨兵集群认为主节点出现“假故障”了,于是开始主从切换,选举出了新的主节点,这就导致短暂的出现了两个主节点。

redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个master对外提供写服务,一旦网络分区恢复,会将其中一个master变为从节点,这时会有大量数据丢失。

redis集群应对脑裂的解决办法应该是取限制原主库接收请求,Redis提供了两个配置项:

  • min-slaves-to-write:主库能进行数据同步的最少从库数量。
  • min-slaves-max-lag:主从进行数据复制时,从库给主库发送ACK消息的最大延迟秒数。

这两项必须同时满足,不然主节点会禁止写入操作,这就解决了因脑裂导致数据丢失的问题。

举个例子,我们把min-slaves-to-write设置为1,把min-slaves-max-lag设置为10。

如果Master节点因为某些原因挂了12s,导致集群判断主节点客观下线,开始主从切换。

同时,因为原Master宕机了12s,没有一个(min-slaves-to-write)从节点与主节点之间的数据复制在10s(min-slaves-max-lag)内,不满足配置要求,原Master就无法执行写操作了。

那么这样做就可以解决脑裂问题吗?

答案是不可以的,这两个配置项主要是针对脑裂时数据丢失问题进行防范,但并不能完全解决脑裂可能带来的所有问题,如脑裂发生后多个子集群之间的协调、数据冲突的处理等。还需要结合其他措施,如使用 Redis Sentinel 或 Redis Cluster 的自动故障转移机制等,来全面应对 Redis 脑裂问题。

按照官方文档所言,redis 并不能保证强一致性

Redis + Sentinel 集群,是最终一致性产品

对于要求强一致性的应用,更应该倾向于 传统关系型数据库 如mysql ,或者使用强一致性的 协调组件 Zookeeper。

缓存三兄弟

Redis 如何处理缓存穿透?

缓存穿透是指查询一个不存在的数据,导致请求直接到达数据库。

缓存穿透一般都是这几种情况产生的:

  • 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
  • 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
  • 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。

解决方法包括:

  • 布隆过滤器:预先将所有可能的键存储在布隆过滤器中,查询时先检查是否存在。
  • 缓存空值:对于查询不到的数据,缓存一个空值,并设置较短的过期时间。
  • 针对恶意用户,可以对其进行限制,限制操作次数

布隆过滤器:

布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。

Redis 如何处理缓存雪崩?

缓存雪崩是指大量缓存同时失效,导致请求直接到达数据库。

可能造成缓存雪崩的原因:

  • 大量缓存键同时过期:当缓存键设置了相同的过期时间,或者由于某种原因导致大量的键同时失效,会导致缓存雪崩。
  • 缓存服务器故障:当缓存服务器发生故障,无法提供服务时,请求将直接访问后端服务,导致压力集中在后端服务上。

解决方法包括:

  • 设置不同的过期时间:避免大量缓存同时失效。
  • 使用分布式锁:在缓存失效时,使用分布式锁控制数据库访问,防止数据库被压垮。
  • 设置熔断机制:在缓存失效的情况下,通过设置熔断机制,直接返回默认值或错误信息,避免请求直接访问后端服务,减轻后端服务的压力。
  • 实时监控和报警:监控缓存系统的状态和性能指标,及时发现异常情况,并通过报警机制通知运维人员进行处理,减少缓存雪崩的影响。
Redis 如何处理缓存击穿?

缓存击穿是指某个热点数据过期或失效时,同时有大量的请求访问该数据,导致请求直接访问数据库或后端服务,导致系统性能下降甚至崩溃的现象。

缓存击穿可能发生的原因包括:

  1. 热点数据失效:当某个热点数据过期时,此时大量请求访问该数据,导致缓存失效,请求直接访问数据库。
  2. 并发访问热点数据:在高并发环境下,大量的请求同时访问同一个热点数据,导致该热点数据在缓存失效期间被并发地访问,触发缓存击穿。

为了解决缓存击穿问题,可以采取以下策略:

  1. 设置热点数据永不过期或过期时间较长:对于一些热点数据,可以将其设置为永不过期,或者设置一个较长的过期时间,确保热点数据在缓存中可用,减少因为过期而触发的缓存击穿。
  2. 加互斥锁或分布式锁:在访问热点数据时,可以引入互斥锁或分布式锁,保证只有一个线程去访问后端服务或数据库,其他线程等待结果。当第一个线程获取到数据后,其他线程可以直接从缓存获取,避免多个线程同时访问后端服务,减轻压力。
  3. 限制并发访问:通过限制并发访问热点数据的请求量,可以控制请求的流量,避免过多请求同时访问热点数据

Redis 的分布式锁如何实现?

SETNX + EXPIRE(有原子性问题)

在 Redis 中实现分布式锁可以使用 SETNX 和 EXPIRE 命令来实现,SETNX 是 "SET if Not eXists" 的缩写,是一个原子性操作,用于在指定的 key 不存在时设置 key 的值。如果 key 已经存在,SETNX 操作将不做任何事情,返回失败;如果 key 不存在,SETNX 操作会设置 key 的值,并返回成功。而 EXPIRE 是设置锁的过期时间的,主要为了防止死锁的发生

SETNX 和 EXPIRE 一起使用可以实现分布式锁的功能,但存在锁误删的问题,比如线程 1 设置的过期时间为 5s,而线程 1 执行了 7s,那么在第 5s 之后锁过期了,那么其他线程就可以拥有这把锁了,之后线程 1 执行完业务,又执行了锁删除操作,那么此时锁就被误删了

如何解决呢?

在删除之前,先判断一下持有锁的持有者是非为它本身。

给每个锁的 value 中添加拥有者的标识,删除之前先判断是否是自己的锁,如果是则删除,否则不删除。当然删除时任然不是个原子操作,所以还是有问题,给它加个 lua 脚本来判断并删除锁,lua 脚本可以保证 redis 中多条语句执行的原子性,所以就可以解决此问题了。

还可以使用 Redission 框架来实现

Redisson 框架:封装了分布式锁、可重入锁、公平锁、红锁(RedLock)等功能,简化开发。

Redis 过期策略和内存淘汰策略

Redis 的过期策略有哪些?

Redis 的过期策略包括:

  • 定期删除:每隔一段时间随机检查一批键,删除其中过期的键。
  • 惰性删除:在访问键时检查其是否过期,如果过期则删除。

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

  • 假设 Redis当前存放非常多的key,并且都设置了过期时间,如果每隔一定时间去检查这些 全部的 key,那么CPU的负载就会特别高,甚至挂掉
  • 因此,redis 采用的是定期过期,每隔一段时间就随机抽取一定数量的 key 来进行检查和删除
  • 但是,最后可能会有很多已经过期了的key 没被删除,这个时候采用 惰性删除,当用户获取的时候,redis 进行检查一下,过期了就给他删掉
Redis 的内存淘汰策略有哪些?

Redis 提供了多种内存淘汰策略,包括:

  • noeviction:不淘汰数据,返回错误。
  • allkeys-lru:从所有键中淘汰最近最少使用的键。
  • volatile-lru:从设置了过期时间的键中淘汰最近最少使用的键。
  • allkeys-random:从所有键中随机淘汰键。
  • volatile-random:从设置了过期时间的键中随机淘汰键。
  • volatile-ttl:从设置了过期时间的键中淘汰剩余时间最短的键。

在 Redis 4.0 版本中又新增了 2 种淘汰机制:

  • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
  • allkeys-lfu:淘汰整个键值中最少使用的键值。

Redis 的事务机制如何工作?

  1. 开始事务(MULTI):客户端发送 MULTI 命令,标记事务的开始。
  2. 命令入队:后续发送的命令不会立即执行,而是进入队列等待。
  3. 执行事务(EXEC):客户端发送 EXEC 命令,Redis 按顺序执行队列中的所有命令。
  4. 返回结果:EXEC 返回所有命令的执行结果,顺序与入队时一致。

示例代码

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 "value1"
QUEUED
127.0.0.1:6379> SET key2 "value2"
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK

2. 事务的关键特性

原子性(Atomicity)
  • 单条命令原子性:Redis 保证单个命令的原子性(如 INCR 操作不会被打断)。
  • 事务级原子性:Redis 事务不支持回滚,若队列中某个命令失败(如类型错误),其他命令仍会继续执行。
    MULTI
    SET key1 "value1"
    INCR key1  # 错误:对字符串执行 INCR
    SET key2 "value2"
    EXEC  # 结果:key1 被设置,key2 也被设置,INCR 失败
    
隔离性(Isolation)
  • Redis 是单线程执行命令,事务执行期间不会被其他客户端命令打断,保证了事务的隔离性。

3. WATCH 机制(乐观锁)

Redis 提供 WATCH 命令实现乐观锁,用于实现 CAS(Compare-and-Swap)操作:

  • 原理:在 MULTI 前使用 WATCH 监视一个或多个键,若事务执行前这些键被其他客户端修改,则整个事务会被放弃(EXEC 返回 nil)。
  • 典型场景:实现分布式锁或计数器的原子递增。

示例:库存扣减(CAS 操作)

WATCH stock  # 监视库存键
GET stock    # 获取当前库存
# 判断库存是否足够,若足够则执行事务
MULTI
DECRBY stock 1
EXEC  # 若期间 stock 被修改,EXEC 返回 nil,需重试

4. 事务的局限性

  • 不支持回滚:Redis 认为 “错误通常由编程错误导致”,因此不支持事务回滚,简化了内部实现。
  • 串行执行:事务中的命令按顺序执行,无法并发,可能影响性能。
  • 不支持嵌套:Redis 事务不能嵌套,每个事务必须以 MULTI 开始,EXEC 结束。

5. 与其他数据库事务的对比

特性 Redis 事务 传统数据库(如 MySQL)
原子性 部分支持(不支持回滚) 完全支持(ACID)
隔离性 基于单线程保证 通过锁或 MVCC 实现
持久性 依赖持久化配置 通常通过 WAL 保证
嵌套事务 不支持 支持
复杂查询 仅支持简单命令组合 支持 SQL 复杂查询

6. 最佳实践

  • 替代方案:对于复杂事务需求,可考虑使用 Lua 脚本(Redis 保证脚本执行的原子性)。
    -- 原子性地实现库存扣减
    if redis.call("GET", "stock") > 0 then
        redis.call("DECR", "stock")
        return 1
    else
        return 0
    end
    
  • 错误处理:事务执行后需检查返回值,若 EXEC 返回 nil,需重试整个操作。
  • 性能考量:避免在事务中包含大量命令,减少客户端等待时间。

Redis 的性能优化有哪些方法?

Redis 的性能优化方法包括:

  • 使用合适的数据结构:根据场景选择最合适的数据结构。
  • 减少网络延迟:将 Redis 部署在靠近应用服务器的地方。
  • 批量操作:使用 MGETMSET 等命令减少网络请求次数。
  • 持久化优化:根据业务需求选择合适的持久化策略。
  • 根据业务需求设置合理的内存淘汰策略(maxmemory-policy),如 allkeys-lru(适合缓存场景)或 volatile-lru(适合带过期时间的键)。
  • 限制最大内存(maxmemory),防止 Redis 因内存溢出崩溃。

网站公告

今日签到

点亮在社区的每一天
去签到