Redis面试常见题

缓存穿透

定义:查询一个不存在的数据,DB查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库(如果有人恶意攻击,会击垮数据库)。

image-20251218215248730

解决办法:

  1. 缓存空数据,查询返回的数据为空,仍然把这个空结果进行缓存

    优点:实现简单。

    缺点:消耗内存,可能发生数据不一致的问题

  2. 使用布隆过滤器

    优点:内存占用较少,没有多余key

    缺点:实现复杂,存在一定误判

image-20251218220222405

布隆过滤器

布隆过滤器(Bloom Filter) 是一种非常节省空间的概率型数据结构,主要用于判断 “一个元素是否在一个集合中”。它的核心特点是:高效、省空间,但存在一定的误报率。

  1. 核心原理

布隆过滤器的本质是一个 位数组(Bit Array) 和一系列 无偏哈希函数(Hash Functions)

  • 初始化:开始时,位数组的所有位都设为 0

  • 添加元素:当一个元素被加入集合时,通过多个哈希函数计算出多个索引值,并将位数组中对应这些索引的位置全部设为 1

  • 查询元素:查询时,同样用这些哈希函数计算索引。

    • 如果这些位置中有任何一个为 0,那么该元素一定不在集合中。

    • 如果这些位置全部为 1,那么该元素可能在集合中(也可能是因为其他多个元素凑巧把这些位置都染红了,这就是“误报”)。

为啥会误判?

image-20251218221344994

缓存击穿

定义:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

image-20251218221629918

解决方案:

  1. 互斥锁:互斥地读取redis缓存,使得每次只能有一条请求,它没有找到数据后跨过缓存去数据库找数据,进行缓存重建。

sequenceDiagram participant C as Client (客户端) participant R as Redis (缓存) participant L as Lock (分布式锁) participant DB as Database (数据库) C->>R: 1. 查询缓存 (Key) R-->>C: 返回空 (缓存失效/击穿) C->>L: 2. 尝试获取分布式锁 alt 获取锁成功 C->>R: 3. 二次检查 (Double Check) Note over C,R: 防止在拿锁期间其他线程已更新缓存 alt 缓存依然为空 C->>DB: 4. 查询数据库 DB-->>C: 返回数据 C->>R: 5. 回写缓存 (带过期时间) else 缓存已有数据 Note right of R: 直接使用新产生的缓存数据 end C->>L: 6. 释放分布式锁 C-->>C: 7. 返回结果给业务层 else 获取锁失败 C->>C: 8. 休眠等待 (如 50ms) C->>R: 9. 重试:重新查询缓存 R-->>C: 返回数据 (此时前序线程通常已填好) C-->>C: 10. 返回结果 end

  1. 逻辑过期:会有一条请求拿了锁之后去开一个新线程尝试重建内存,请求不会等待,直接返回逻辑过期的数据

sequenceDiagram participant C as Client (客户端) participant R as Redis (缓存) participant L as Lock (互斥锁) participant T as New Thread (后台线程) participant DB as Database (数据库) C->>R: 1. 查询缓存 (Key) R-->>C: 返回数据 (包含逻辑过期时间) rect rgb(240, 240, 240) Note over C, R: 判定逻辑:当前时间 > 数据中的过期时间 C->>L: 2. 尝试获取分布式锁 (非阻塞) alt 获取锁成功 C-->>T: 3. 开启后台异步线程 T->>DB: 4. 查询数据库 (重建数据) DB-->>T: 返回最新数据 T->>R: 5. 写入缓存 (重置逻辑过期时间) T->>L: 6. 释放分布式锁 C-->>C: 7. 获取锁的请求:立即返回旧数据 else 获取锁失败 C-->>C: 8. 未拿到锁的请求:立即返回旧数据 end end

总结

特性

互斥锁方案 (Mutex)

逻辑过期方案 (Logical)

核心思路

没数据就锁住,让一个线程去查,其他人排队等待

缓存永不过期,发现快过期了就锁住,派个后台线程去更新。

一致性

强一致性。用户拿到的永远是数据库中最新的数据。

弱一致性。更新期间,用户会拿到逻辑上已过期的旧数据。

可用性/性能

较好。但由于存在阻塞和重试,高并发下响应时间会有抖动。

极好。完全非阻塞,用户请求立即返回,响应速度最快。

优点

1. 保证数据最新。 2. 不需要额外的内存字段。 3. 实现逻辑相对直观。

1. 性能极高,无等待时间。 2. 用户体验极佳,不会因为重建缓存而卡顿。

缺点

1. 有死锁风险(虽概率低)。 2. 线程阻塞会消耗 CPU 资源,性能略低。

1. 存在数据不一致窗口。 2. 实现复杂(需代码维护逻辑时间、开启异步线程)。

优先选择“互斥锁”的场景:

  • 对数据一致性要求极高:例如金融账户余额、库存精准扣减、活动起止时间等。

  • 并发量大但可控:虽然有阻塞,但在配置好连接池和超时时间的情况下,系统可以承受。

优先选择“逻辑过期”的场景:

  • 用户体验高于实时性:例如热点新闻浏览、社交平台大 V 的主页、商品详情页的非核心信息。这些场景下,用户多看几秒钟“半分钟前”的数据通常没关系,但如果页面转圈圈(阻塞等待)则无法接受。

  • 极高并发请求:当瞬间流量大到排队重试会导致系统线程耗尽(雪崩隐患)时,逻辑过期这种“立即返回”的策略更为安全。

缓存雪崩

定义:缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  1. 给不同的Key的TTL添加随机值(就不会突然大量key同时失效了)

  2. 利用Redis集群提高服务的可用性(哨兵模式、集群模式)

  3. 给缓存业务添加降级限流策略(ngxin或spring cloud gateway,这个可以保底,三个都能用这个尝试解决

  4. 给业务添加多级缓存(Guava或Caffeine)

缓存一致性

缓存一致性解决方案

Redis持久化

  • RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

  • AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

RDB的执行原理

  • bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据(复制页表)。完成fork后读取内存数据并写入 RDB 文件。

  • fork采用的是copy-on-write技术:

    • 当主进程执行读操作时,访问共享内存;

    • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

image-20251218224226319

AOF

  • AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

  • AOF的命令记录的频率也可以通过redis.conf文件来配

  • 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

Redis AOF 配置项

# 是否开启AOF功能,默认是no
appendonly yes

# AOF文件的名称
appendfilename "appendonly.aof"

# 表示每执行一次写命令,立即记录到AOF文件
# appendfsync always

# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec

# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
# appendfsync no

AOF 刷盘策略对比表

配置项

刷盘时机

优点

缺点

Always

同步刷盘:每执行一次写命令立即调用 fsync 写入磁盘。

可靠性最高,几乎不丢失数据。

性能影响大,受磁盘 I/O 限制明显。

everysec

每秒刷盘:每隔一秒由异步线程执行一次 fsync

性能适中,兼顾了安全与效率。

在宕机情况下,最多丢失 1 秒的数据。

no

操作系统控制:由操作系统(OS)决定何时将缓冲区数据同步到磁盘。

性能最好,对业务响应无额外延迟。

可靠性最差,掉电时可能丢失大量数据。

bgrewriteaof命令触发设置

# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100

# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

默认选择:在生产环境下,Redis 官方默认推荐使用 everysec,因为它在性能和可靠性之间取得了极佳的平衡。

配合 RDB 使用:通常建议将 AOF 与 RDB 快照配合使用,以获得更快的故障恢复速度和更高的数据安全性。

总结

RDB 与 AOF 持久化方式对比

对比维度

RDB (Redis Database)

AOF (Append Only File)

持久化方式

定时对整个内存做快照。

记录每一次执行的命令。

数据完整性

不完整,两次备份之间会丢失数据。

相对完整,取决于刷盘策略。

文件大小

会有压缩,文件体积小。

记录命令,文件体积很大。

宕机恢复速度

很快。

慢。

数据恢复优先级

低,因为数据完整性不如 AOF。

高,因为数据完整性更高。

系统资源占用

高,大量 CPU 和内存消耗(Fork 进程)。

低(主要是磁盘 IO),但重写时占用 CPU/内存。

使用场景

容忍数分钟数据丢失,追求快启动。

对数据安全性要求较高。

Redis过期策略

惰性删除

  • **定义:**设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key

  • 优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查

  • 缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

定期删除

  • **定义:**每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

  • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数

  • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

  • 缺点:难以确定删除操作执行的时长和频率。

数据的淘汰策略

当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:对全体key ,随机进行淘汰。

  • volatile-random:对设置了TTL的key ,随机进行淘汰。

  • allkeys-lru: 对全体key,基于LRU算法进行淘汰

  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰

数据的淘汰策略使用建议

  • 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。

  • 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。

  • 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。

  • 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。

Redisson

  • 底层是setnx和lua脚本(保证原子性)

  • 获取锁:SET lock value NX EX 10 ,注意:NX是互斥、EX是设置超时时间

  • *释放锁:*DEL key

  • 超时事件可以自己设置控制;也可以靠看门狗帮忙做续期

  • redisson实现的分布式锁-可重入(同一线程可以获取同一把锁,会记录重入次数,重入次数到0才会删除这个锁)

image-20251218225925952

redisson实现的分布式锁

注意:主从一致性问题,Redisson无法解决主从一致性问题。

为啥会出现主从一致性问题?

Redis 的主从复制是异步的。这就导致了一个潜在的“锁丢失”风险:

  1. 客户端 A 在 Redis 主节点(Master)上成功获取了锁。

  2. 主节点在将这个锁的数据同步给从节点(Slave)之前,突然宕机了。

  3. 从节点被哨兵提升为新的主节点。

  4. 客户端 B 请求获取同一个锁,由于新主节点上没有该锁的数据,客户端 B 也能获取成功

此时,系统中同时有两个客户端持有同一把锁,分布式锁的互斥性宣告失效。

如何解决?

Redisson 提供了两种主要的思路来应对这个问题:

  1. Wait 机制(不常用)

    Redisson 允许在发送命令后使用 WAIT 命令,要求主节点必须将数据同步到指定数量的从节点后才返回成功。虽然提高了可靠性,但牺牲了性能,且在极端网络分区下仍有风险。

  2. 红锁算法(Redlock)—— 不推荐

    红锁不再依赖“主从架构”,而是要求部署 N 个完全独立的 Redis 节点(通常为 5 个,这些节点之间没有任何主从关系)。

    1. 获取时间戳:客户端获取当前系统时间。

    2. 轮询加锁:客户端尝试在 N 个节点上依次申请加锁。加锁时使用很短的超时时间(比如几十毫秒),防止在某个宕机的节点上耗时太久。

    3. 过半成功原则:只有当客户端在大多数($N/2 + 1$)节点上加锁成功,且总耗时小于锁的有效期时,才认为最终获取锁成功。

    4. 计算有效期:实际锁的有效时间 = 初始有效时间 - 加锁消耗的时间。

    5. 失败释放:如果获取锁失败,客户端必须向所有节点发起解锁请求(即使有些节点加锁没成功)。

但是红锁有如下缺点,所有目前官方不推荐使用红锁来解决:

  1. 运维成本:维护 5 个独立的 Redis 实例非常麻烦。

  2. 性能损耗:多次网络交互会导致延迟增加。

  3. 实际场景:大多数业务场景下,主从切换导致的瞬时锁丢失可以通过业务幂等性或数据库唯一索引来兜底。

若要强一致性,可以使用zookeeper这种CP模式的分布式锁:

Redis 分布式锁的核心设计目标是 AP(可用性),而 ZooKeeper 的核心设计目标是 CP(一致性)。在 ZK 中,只要写入成功,就意味着超过半数节点已同步,且 Leader 宕机后,只有数据最新的节点才能当选新 Leader,因此不会出现类似 Redis 主从异步复制导致的锁丢失问题。

ZooKeeper 实现分布式锁的原理

ZK 实现分布式锁主要依赖其 临时顺序节点(Ephemeral Sequential Nodes)监听机制(Watcher)

核心步骤:

  1. 创建父节点:在 ZK 中创建一个持久节点 /MyLock

  2. 创建临时顺序节点:每个尝试加锁的客户端,都会在 /MyLock 下创建一个临时顺序节点。

    • 客户端 A 创建节点 /MyLock/lock-0000001

    • 客户端 B 创建节点 /MyLock/lock-0000002

  3. 判断最小节点:客户端获取 /MyLock 下的所有子节点,判断自己创建的节点序号是否是最小的。

    • 如果是最小的,则获得锁

    • 如果不是最小的,则等待锁

  4. 监听前一个节点关键点在于: 没拿到锁的客户端不会轮询,而是向它序号前一个的节点注册一个 Watcher 监听。

  5. 释放锁:当持有锁的客户端操作完成或意外断开连接(Session 超时)时,该临时节点会自动删除。

  6. 触发回调:ZK 通知监听了该节点的后续客户端。后续客户端再次检查自己是否为最小节点,如果是,则获取锁。

为什么 ZK 能保证强一致性?

  • 顺序性:ZK 保证所有的更新操作都是全局有序的。

  • 非异步丢失:ZK 使用 ZAB 协议,只有过半节点写入成功才会返回确认。

  • 自动释放:由于使用的是“临时节点”,如果客户端所在的服务器挂了,ZK 感知到 Session 断开会立刻删除节点,从而自动释放锁,避免死锁。

  • 无羊群效应:每个节点只监听前一个节点,当锁释放时,只会唤醒一个客户端,不会造成全集群的惊群效应(Thundering Herd)。

Redis主从复制

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。

全量同步

sequenceDiagram participant M as Master participant S as Slave S->>S: 1. 执行 replicaof 命令,建立连接 S->>M: 2. 请求数据同步 (发送 replid, offset) rect rgb(240, 240, 240) Note right of M: 3. 判断是否是第一次同步 (检查 replid 是否一致) end M->>S: 4. 如果是第一次,返回 Master 的数据版本信息 (replid, offset) S->>S: 5. 保存版本信息 M->>M: 6. 执行 bgsave,生成 RDB 文件 par 并行处理 M->>M: 9. 记录 RDB 期间的所有命令到 repl_baklog and 发送文件 M->>S: 7. 发送 RDB 文件 end S->>S: 8. 清空本地数据,加载 RDB 文件 M->>S: 10. 发送 repl_baklog 中的命令 S->>S: 11. 执行接收到的命令

关键步骤解析:

  • Replication Id (replid): 数据集的标记。如果 Slave 的 replid 与 Master 不一致,说明是第一次同步或需要全量同步。

  • Offset (偏移量): 随着 repl_baklog 中数据的增多而增大。如果 Slave 的 offset 小于 Master,说明数据落后,需要更新。

  • RDB 生成与传输: Master 在后台生成快照(bgsave),并将其发送给 Slave。这是最耗时的阶段。

  • repl_baklog: 在生成和传输 RDB 文件的过程中,新产生的写命令会被缓存在这个日志中,确保 Slave 在加载完 RDB 后能补全这段时间的增量数据。

增量同步

sequenceDiagram participant M as Master participant S as Slave S->>S: 1. 重启 (或其他原因触发重连) S->>M: 2. 发送 psync replid offset rect rgb(240, 240, 240) Note right of M: 3. 判断请求 replid 是否一致 end M->>S: 4. 不是第一次同步,返回 CONTINUE S->>S: 5. 保存/更新版本信息 M->>M: 6. 去 repl_baklog 中获取 offset 后的数据 M->>S: 7. 发送 offset 后的命令 S->>S: 8. 执行命令

流程要点分析:

  • 触发条件:主要用于 Slave 节点重启或网络闪断后恢复连接的情况。

  • psync 命令:Slave 会主动告知 Master 自己目前的 replid 和已经同步到的 offset(偏移量)。

  • 判断逻辑:Master 检查 replid 是否匹配。如果一致,且请求的 offset 仍在 Master 的 repl_baklog(积压缓冲区)范围内,则执行增量同步。

  • 效率优势:相比全量同步(需要生成和传输巨大的 RDB 文件),增量同步只传输断开期间缺失的少量写命令,极大地降低了带宽消耗和系统压力。

Redis哨兵模式(其实不常用)

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复

  • 监控:Sentinel 会不断检查您的master和slave是否按预期工作

  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主

  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

  • Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

image-20251219095218481

哨兵选主规则

  • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点

  • 然后判断从节点的slave-priority值,越小优先级越高

  • 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高

  • 最后是判断slave节点的运行id大小,越小优先级越高。

redis集群(哨兵模式)脑裂

  • 集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失

  • **解决:**我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

image-20251219095737658

Redis分片集群结构

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决

  • 海量数据存储问题

  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点(自动路由)

image-20251219100602764

分片集群结构-数据读写

  • Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

image-20251219101209368

Redis是单线程的,为什么还那么快

  • redis是纯内存操作,执行速度非常快

  • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题

  • 使用I/O多路复用模型,非阻塞IO

  • Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度, I/O多路复用模型主要就是实现了高效的网络请求

I/O模型

  • 阻塞IO:阻塞IO模型中,用户进程在两个阶段都是阻塞状态。

  • 非阻塞IO:非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。

  • IO多路复用

阻塞IO

image-20251219102712418

非阻塞IO

image-20251219103247300

IO多路复用

  • 概念:是利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。

  • 实现:不过监听Socket的方式、通知的方式有多种实现

    • select

    • poll

    • epoll

  • 差异

    • select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认

image-20251219104107834

Redis网络模型:就是使用I/O多路复用结合事件的处理器来应对多个Socket请求

  • 连接应答处理器

  • 命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件

  • 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

Redis网络模型

image-20251219105115582