Redis 常见的面试专题确实非常高频,几乎每家大厂/中大型公司后端/架构岗面试都会深挖。下面我把你列的这些点(缓存击穿、缓存雪崩、缓存穿透、布隆过滤器、延时双删、持久化方式、Redis 分布式锁、I/O 多路复用)全部覆盖,结合2025–2026年的主流实践和面试话术,给出定义 + 成因 + 危害 + 解决方案 + 代码示例 + 面试加分点的完整梳理。
1. 缓存穿透(Cache Penetration)
定义:查询一个根本不存在的数据,缓存和数据库都没有命中,导致请求每次都打到数据库。
成因:恶意攻击(大量不存在的key)、业务代码bug(参数校验缺失)、爬虫。
危害:数据库瞬间压力暴增,甚至宕机。
解决方案(主流优先级排序):
- 缓存空对象(最常用,简单有效)
- 查询不到时缓存 “null” 或空字符串,设置短过期时间(30s~5min)
- 优点:实现简单
- 缺点:占用内存(可用短TTL缓解)、可能污染缓存
- 布隆过滤器(高并发、大数据量首选)
- 在缓存前加一层布隆过滤器,先判断key“一定不存在”就直接返回
- 优点:内存占用极小(1亿条数据约几十MB)、查询O(1)
- 缺点:有误判(假阳性)、不支持删除(Redis原生布隆支持有限,可用布谷鸟过滤器)
- 接口层参数校验 + 限流
面试话术:
“我一般优先用缓存空对象 + 短TTL,简单可靠;如果QPS很高且key空间很大,再引入Redis布隆过滤器(bloom filter module 或 redisson的R bloom filter),误判率控制在1%以内。”
2. 缓存击穿(Cache Breakdown / Hot Key失效)
定义:某个热点key突然失效,大量并发同时请求,瞬间全部打到数据库。
成因:热点数据刚好过期 + 高并发访问。
危害:数据库短时压力激增。
解决方案(主流三种):
- 互斥锁(分布式锁最常见)
- 缓存miss → 获取分布式锁 → 只有一个线程去查DB → 回写缓存 → 释放锁
- 其他线程自旋或sleep后重试
- 逻辑过期 / 永不过期 + 异步刷新
- key不设TTL,value里带一个逻辑过期时间
- 发现过期 → 异步线程去刷新 → 当前请求返回旧数据
- 热点key预加载 / Redis热点key自动发现 + 永不过期
代码示例(Redisson分布式锁):
String key = "hot:item:" + itemId;
RLock lock = redisson.getLock("lock:" + key);
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) { // 尝试获取锁
value = redisTemplate.opsForValue().get(key); // 双重检查
if (value == null) {
value = db.queryItem(itemId); // 查库
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
}
} finally {
lock.unlock();
}
}
return value;
面试加分:提到“Redisson看门狗机制自动续期,避免业务执行时间长导致锁提前释放”。
3. 缓存雪崩(Cache Avalanche)
定义:同一时间大量key同时失效(或Redis宕机),导致海量请求瞬间打到数据库。
成因:统一设置了相同的过期时间、Redis全挂。
危害:数据库直接崩。
解决方案(多层防御):
- 过期时间加随机值(最基础)
expire = 基础时间 + Random(0~300)秒,错开失效
- 多级缓存(本地 + Redis + 数据库)
- 限流 / 熔断 / 降级(Hystrix、Sentinel、Resilience4j)
- Redis高可用(主从 + 哨兵 / Cluster)
- 热点数据永不过期 + 定时任务刷新
面试话术:
“我项目里用的是随机过期 + Sentinel + 熔断降级三保险;极端情况下本地缓存兜底。”
4. 布隆过滤器(Bloom Filter)
定义:一种空间高效的概率型数据结构,用于判断一个元素是否可能存在于集合中。
核心思想:多个哈希函数 → 映射到bit数组的不同位 → 全1才可能存在。
优点:极省内存、查询O(k)(k为哈希函数个数)
缺点:有假阳性(一定不存在→一定不存在;可能存在→大概率存在)、不支持删除(原生)
Redis中的实现:
- Redis 6+ 自带
BF.ADDBF.EXISTS(ReBloom模块) - Redisson 也有 RBloomFilter
面试常问:误判率怎么算?(约 (1 – e^(-n*k/m))^k ,m=bit位数,n=元素数,k=哈希函数数)
5. 延时双删(延迟双删策略)
定义:更新数据库后,先删缓存 → 延迟(几百ms~几秒)再删一次缓存。
适用场景:Cache Aside 模式下,写操作导致读-写不一致。
为什么两次删?
- 第一次删:更新DB前删,防止旧数据被读
- 延迟第二次删:更新DB和删缓存之间有短暂时间差,可能有并发读把旧数据又写回缓存 → 延迟再删一次
代码示例(典型实现):
// 更新商品
public void updateItem(Item item) {
// 1. 先删缓存
redisTemplate.delete("item:" + item.getId());
// 2. 更新数据库
itemMapper.update(item);
// 3. 延迟双删(异步或定时任务)
Thread.sleep(500); // 或用DelayQueue / MQ 延迟
redisTemplate.delete("item:" + item.getId());
}
面试话术:
“延时双删是Cache Aside下比较折中的方案;如果追求最终一致性,可结合Canal + MQ监听binlog异步删缓存;强一致性场景用分布式锁 + 读写锁。”
6. Redis 持久化方式
| 方式 | 机制 | 优点 | 缺点 | 丢失数据窗口 | 恢复速度 | 生产推荐 |
|---|---|---|---|---|---|---|
| RDB | 定时快照(fork子进程) | 文件小、恢复快 | 可能丢数据 | 最后一次快照后 | 快 | 辅助 |
| AOF | 记录每条写命令 | 数据最安全(fsync策略) | 文件大、恢复慢 | 取决于fsync | 慢 | 主用 |
| 混合持久化 | AOF + RDB preamble | 恢复快 + 安全 | Redis 4.0+ | 折中 | 较快 | 首选 |
Redis 7+:Multi-part AOF(分片AOF),rewrite更高效。
面试话术:
“生产我基本用混合持久化:appendfsync everysec + aof-use-rdb-preamble yes,丢失最多1秒,恢复速度比纯AOF快很多。”
7. Redis 分布式锁
实现方式对比:
| 方案 | 实现命令 | 优点 | 缺点 / 风险 | 推荐场景 |
|---|---|---|---|---|
| setnx + expire | setnx + expire | 简单 | 非原子、过期后可能误删他人锁 | 学习演示 |
| Lua脚本 | set + px + nx | 原子性 | 业务执行时间 > 锁超时 → 误删 | 中小型项目 |
| Redisson | RedissonLock | 看门狗自动续期、可重入 | 依赖Redisson依赖 | 生产首选 |
| Redlock | 多节点多数派 | 更高可用 | 实现复杂、争议大(时间同步、网络分区) | 极高一致性需求 |
Redisson看门狗:默认每10s续期一次(锁超时时间1/3),线程存活就一直续。
8. I/O 多路复用(Redis为什么单线程还这么快)
核心:Redis 6前基本单线程(主线程处理命令),靠I/O多路复用 + 内存操作 + 高效数据结构实现高性能。
演进:
- Redis 6前:单线程 + select/poll/epoll/kqueue
- Redis 6+:多线程I/O(网络读写多线程,主逻辑仍单线程)
为什么快:
- 内存操作(非磁盘)
- I/O多路复用(单线程处理成千上万连接)
- 高效SDS、ziplist、跳表、哈希表等数据结构
- 渐进式rehash、无锁(单线程)
面试话术:
“Redis单线程模型避免了线程切换和锁竞争开销,结合epoll/kqueue实现了真正的非阻塞I/O,所以QPS能轻松上10w+。”
总结 & 面试应对建议
- 三大缓存问题:穿透(布隆/空值)、击穿(分布式锁/逻辑过期)、雪崩(随机过期+限流熔断)
- 一致性:延时双删(折中)、Canal+MQ(最终一致)、分布式锁(强一致)
- 高可用:持久化选混合、分布式锁用Redisson、I/O多路复用是性能基石
你最想深入哪个点?
A. Redisson分布式锁源码级剖析(看门狗 + 可重入 + 续期)
B. 布隆过滤器误判率计算 + Redis实现代码
C. 真实生产案例:如何处理线上缓存雪崩/击穿事故
D. Redis 7+ 新特性对这些问题的影响(Multi-part AOF、函数等)
E. 三大问题完整Java + Spring Boot + Redisson实现Demo
告诉我字母,我继续展开!