当前位置: 首页 > backend >正文

Redis经典面试题

本篇文章简单介绍一些 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 thenredis.call("DECR", "stock")return 1
    elsereturn 0
    end
    
  • 错误处理:事务执行后需检查返回值,若 EXEC 返回 nil,需重试整个操作。
  • 性能考量:避免在事务中包含大量命令,减少客户端等待时间。

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

Redis 的性能优化方法包括:

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

相关文章:

  • 数据库实验10
  • 【经验总结】Ubuntu 22.04.5 LTS 将内核从5.15.0-140 升级到6.8.0-60后纽曼无线网卡无法使用解决措施
  • C++ 命令模式详解
  • R 语言科研绘图 --- 桑基图-汇总
  • Python网络爬虫:从入门到实践
  • uniapp-商城-51-后台 商家信息(logo处理)
  • 33号远征队 - SDKDump
  • Spring 必会之微服务篇(2)
  • 前端进化论·JavaScript 篇 · 数据类型
  • [学习]RTKLib详解:sbas.c与rtcm.c
  • Linux 阻塞和非阻塞 I/O 简明指南
  • [架构之美]linux常见故障问题解决方案(十九)
  • 数据结构与算法分析实验11 实现顺序查找表
  • vue注册用户使用v-model实现数据双向绑定
  • 202536 | KafKa生产者分区写入策略+消费者分区分配策略
  • 0.环境初始化
  • 基于Spring Boot + Vue的高校心理教育辅导系统
  • uniapp小程序中实现无缝衔接滚动效果
  • Mysql中in和exists的区别?
  • 神经网络极简入门技术分享
  • JavaScript基础-三元表达式
  • Nakama:让游戏与应用更具互动性和即时性
  • 用Python绘制动态彩色ASCII爱心:技术深度与创意结合
  • Ajax基础
  • ui组件二次封装(vue)
  • PyTorch API 7 - TorchScript、hub、矩阵、打包、profile
  • 互联网大厂Java求职面试:基于RAG的智能问答系统设计与实现-2
  • 如何删除网上下载的资源后面的文字
  • AI生成视频推荐
  • ragflow报错:KeyError: ‘\n “序号“‘