Redis 缓存穿透、击穿、雪崩面试题复盘
这是 Redis 面试里非常高频的一类问题:
什么是缓存穿透、缓存击穿和缓存雪崩?它们有什么区别?应该怎么解决?
这三个概念很容易混在一起,因为它们的结果都可能是“请求打到数据库,数据库压力变大”。但它们的根因完全不同。
可以先用一句话区分:
穿透:查的数据根本不存在。击穿:一个热点 Key 过期。雪崩:大量 Key 同时过期,或者 Redis 整体不可用。缓存穿透指的是:请求的数据在缓存中不存在,在数据库中也不存在。
比如:
- 请求一个不存在的用户 ID:
id = -1 - 请求一个已经删除的商品
- 恶意攻击者构造大量非法参数
正常缓存流程一般是:
请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 写入 Redis -> 返回数据但如果数据根本不存在,就会变成:
请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 数据库也没有 -> 返回空问题在于:下一次同样的非法请求过来,Redis 里还是没有,于是又会打到数据库。
如果有人构造大量不存在的 ID,请求就会绕过缓存,持续打到数据库。
穿透的解决方案
Section titled “穿透的解决方案”缓存穿透常见有两种解决方案:缓存空对象和布隆过滤器。
当数据库查不到数据时,也往 Redis 里写一个空值或特殊值,并设置较短过期时间。
async function getUser(id) { const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey); if (cached !== null) { return cached === 'NULL' ? null : JSON.parse(cached); }
const user = await db.queryUserById(id);
if (!user) { await redis.set(cacheKey, 'NULL', 'EX', 60); return null; }
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); return user;}这样下一次请求同一个不存在的 ID,就会被 Redis 拦住,不会继续打数据库。
它的优点是简单,缺点是如果恶意请求的非法 ID 极多,Redis 里会出现很多空值缓存。所以空值缓存的过期时间一般要短一些。
布隆过滤器适合在请求进入缓存和数据库之前,先判断这个数据是否可能存在。
请求 userId | v布隆过滤器判断 | | 不存在 -> 直接拒绝 | | 可能存在 v查 Redis / DB布隆过滤器的特点是:
- 如果它判断不存在,那就一定不存在。
- 如果它判断存在,只能说明可能存在。
所以它适合挡掉大量明显非法的请求。
面试里可以这样说:缓存空对象适合兜住少量不存在数据,布隆过滤器适合在入口处拦截大量非法 Key。
缓存击穿指的是:某一个热点 Key 在过期瞬间,大量并发请求同时打到数据库。
注意,击穿通常是单个 Key。
比如:
- 某个大促商品详情页
- 某个热门直播间信息
- 某个高访问量用户主页
- 某条热点新闻
这个 Key 平时在 Redis 里,所以数据库压力不大。
但它刚好过期时,大量请求同时进来,发现 Redis 没有,于是一起查数据库。
热点 Key 过期 |大量请求同时进入 |Redis 都没查到 |全部打到数据库这就是缓存击穿。
击穿的解决方案
Section titled “击穿的解决方案”缓存击穿常见有两种方案:互斥锁和逻辑过期。
互斥锁的思路是:同一时间只允许一个线程去查数据库并重建缓存,其他线程等待或重试。
async function getProduct(id) { const cacheKey = `product:${id}`; const lockKey = `lock:product:${id}`;
const cached = await redis.get(cacheKey); if (cached) return JSON.parse(cached);
const locked = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (!locked) { await sleep(50); return getProduct(id); }
try { const product = await db.queryProductById(id); await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600); return product; } finally { await redis.del(lockKey); }}这个方案可以保护数据库,但会让部分请求等待,接口耗时可能变长。
逻辑过期的思路是:Redis 里的数据不设置物理过期,或者设置很长的物理过期;真正的过期时间写在 value 里。
{ "data": { "id": 1, "name": "热门商品" }, "expireAt": "2026-05-20T20:00:00+08:00"}请求到来时:
- 如果逻辑时间没过期,直接返回数据。
- 如果逻辑时间过期,当前线程尝试拿锁。
- 拿到锁的线程异步重建缓存。
- 当前请求先返回旧数据。
查到缓存 |判断逻辑过期 |已过期 -> 尝试加锁 -> 异步重建缓存 |直接返回旧数据它的优点是响应速度更稳,不会让大量请求等待数据库。
缺点是用户可能短时间看到旧数据,所以它更适合对实时性要求不那么高的热点数据。
缓存雪崩指的是:大量 Key 在同一时间过期,或者 Redis 服务不可用,导致大量请求同时打到数据库。
它和击穿的区别是:
- 击穿是单个热点 Key。
- 雪崩是大量 Key,或者缓存层整体出问题。
比如:
- 批量导入缓存时设置了相同过期时间。
- 活动开始前预热了一批商品缓存,过期时间完全一样。
- Redis 宕机或网络故障。
- Redis 集群大面积不可用。
雪崩的危害比击穿更大,因为它不是一个点,而是一片。
雪崩的解决方案
Section titled “雪崩的解决方案”缓存雪崩通常要从多个层面解决。
过期时间加随机值
Section titled “过期时间加随机值”不要让大量 Key 设置完全一样的过期时间。
const baseTtl = 3600;const randomTtl = Math.floor(Math.random() * 300);
await redis.set(cacheKey, value, 'EX', baseTtl + randomTtl);这样可以把过期时间打散,避免某一秒大量 Key 同时失效。
可以在 Redis 前面加本地缓存,比如 Caffeine、Guava Cache 或进程内缓存。
请求 -> 本地缓存 -> Redis -> 数据库即使 Redis 短时间抖动,本地缓存也能顶住一部分热点请求。
不过本地缓存也会带来一致性问题,所以一般只适合热点数据或允许短暂不一致的数据。
服务降级和限流
Section titled “服务降级和限流”当数据库压力过大时,不能让所有请求继续堆积。
可以做:
- 数据库访问限流。
- 熔断降级。
- 返回兜底数据。
- 返回“服务器繁忙,请稍后再试”。
降级不是偷懒,而是在系统压力过大时保护核心链路。
Redis 高可用
Section titled “Redis 高可用”如果雪崩原因是 Redis 宕机,就需要高可用架构。
常见方案:
- Redis 主从复制。
- 哨兵模式。
- Redis Cluster。
- 多机房或多可用区部署。
高可用解决的是缓存服务不可用的问题,过期时间随机解决的是大量 Key 同时失效的问题。两者关注点不同。
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 核心原因 | 数据根本不存在 | 热点 Key 过期 | 大量 Key 同时过期或 Redis 宕机 |
| Key 数量 | 大量不存在 Key | 单个热点 Key | 多个或大部分 Key |
| 数据库状态 | 数据库里也没有 | 数据库里有数据 | 数据库里有数据 |
| 主要危害 | 绕过缓存打数据库 | 单个热点打爆数据库 | 大量请求压垮数据库 |
| 主要方案 | 缓存空对象、布隆过滤器 | 互斥锁、逻辑过期 | 过期随机、多级缓存、限流降级、高可用 |
记忆时可以抓住关键词:
穿透:不存在击穿:热点雪崩:大量面试时怎么回答
Section titled “面试时怎么回答”这道题可以这样回答:
缓存穿透是请求的数据在缓存和数据库中都不存在,请求会绕过缓存持续打到数据库,常用缓存空对象和布隆过滤器解决。缓存击穿是某个热点 Key 在过期瞬间被大量并发请求访问,导致请求同时打到数据库,常用互斥锁或逻辑过期解决。缓存雪崩是大量 Key 同时过期,或者 Redis 服务不可用,导致大量请求同时涌向数据库,常用过期时间加随机值、多级缓存、限流降级和 Redis 高可用解决。
如果面试官继续追问,可以补充方案取舍:
- 缓存空对象简单,但要设置较短 TTL,避免缓存污染。
- 布隆过滤器适合拦截大量非法请求,但存在误判为可能存在。
- 互斥锁能保护数据库,但会增加等待时间。
- 逻辑过期响应快,但可能返回旧数据。
- 过期随机能打散失效时间,但不能解决 Redis 宕机。
- Redis 高可用能提升可用性,但不能替代限流和降级。
这类 Redis 面试题的关键,是把三个概念的根因讲清楚。
缓存穿透:缓存没有,数据库也没有。缓存击穿:缓存没有,但数据库有,而且是热点 Key。缓存雪崩:大量缓存同时没有,或者 Redis 整体不可用。只要先分清根因,再说解决方案,就不会混乱。
最后再补一句工程思维:缓存是为了保护数据库,但缓存系统本身也会失效。所以真正可靠的设计,通常不是只靠一个方案,而是缓存策略、限流降级、异步重建和高可用架构一起配合。