跳转到内容

Redis 缓存穿透、击穿、雪崩面试题复盘

约 10 分钟阅读发布于 2024/1/1

这是 Redis 面试里非常高频的一类问题:

什么是缓存穿透、缓存击穿和缓存雪崩?它们有什么区别?应该怎么解决?

这三个概念很容易混在一起,因为它们的结果都可能是“请求打到数据库,数据库压力变大”。但它们的根因完全不同。

可以先用一句话区分:

穿透:查的数据根本不存在。
击穿:一个热点 Key 过期。
雪崩:大量 Key 同时过期,或者 Redis 整体不可用。

缓存穿透指的是:请求的数据在缓存中不存在,在数据库中也不存在。

比如:

  • 请求一个不存在的用户 ID:id = -1
  • 请求一个已经删除的商品
  • 恶意攻击者构造大量非法参数

正常缓存流程一般是:

请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 写入 Redis -> 返回数据

但如果数据根本不存在,就会变成:

请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 数据库也没有 -> 返回空

问题在于:下一次同样的非法请求过来,Redis 里还是没有,于是又会打到数据库。

如果有人构造大量不存在的 ID,请求就会绕过缓存,持续打到数据库。

缓存穿透常见有两种解决方案:缓存空对象和布隆过滤器。

当数据库查不到数据时,也往 Redis 里写一个空值或特殊值,并设置较短过期时间。

cache-null.js
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 都没查到
|
全部打到数据库

这就是缓存击穿。

缓存击穿常见有两种方案:互斥锁和逻辑过期。

互斥锁的思路是:同一时间只允许一个线程去查数据库并重建缓存,其他线程等待或重试。

mutex-lock.js
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 里。

logical-expire-value.json
{
"data": {
"id": 1,
"name": "热门商品"
},
"expireAt": "2026-05-20T20:00:00+08:00"
}

请求到来时:

  • 如果逻辑时间没过期,直接返回数据。
  • 如果逻辑时间过期,当前线程尝试拿锁。
  • 拿到锁的线程异步重建缓存。
  • 当前请求先返回旧数据。
查到缓存
|
判断逻辑过期
|
已过期 -> 尝试加锁 -> 异步重建缓存
|
直接返回旧数据

它的优点是响应速度更稳,不会让大量请求等待数据库。

缺点是用户可能短时间看到旧数据,所以它更适合对实时性要求不那么高的热点数据。

缓存雪崩指的是:大量 Key 在同一时间过期,或者 Redis 服务不可用,导致大量请求同时打到数据库。

它和击穿的区别是:

  • 击穿是单个热点 Key。
  • 雪崩是大量 Key,或者缓存层整体出问题。

比如:

  • 批量导入缓存时设置了相同过期时间。
  • 活动开始前预热了一批商品缓存,过期时间完全一样。
  • Redis 宕机或网络故障。
  • Redis 集群大面积不可用。

雪崩的危害比击穿更大,因为它不是一个点,而是一片。

缓存雪崩通常要从多个层面解决。

不要让大量 Key 设置完全一样的过期时间。

random-expire.js
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 短时间抖动,本地缓存也能顶住一部分热点请求。

不过本地缓存也会带来一致性问题,所以一般只适合热点数据或允许短暂不一致的数据。

当数据库压力过大时,不能让所有请求继续堆积。

可以做:

  • 数据库访问限流。
  • 熔断降级。
  • 返回兜底数据。
  • 返回“服务器繁忙,请稍后再试”。

降级不是偷懒,而是在系统压力过大时保护核心链路。

如果雪崩原因是 Redis 宕机,就需要高可用架构。

常见方案:

  • Redis 主从复制。
  • 哨兵模式。
  • Redis Cluster。
  • 多机房或多可用区部署。

高可用解决的是缓存服务不可用的问题,过期时间随机解决的是大量 Key 同时失效的问题。两者关注点不同。

维度缓存穿透缓存击穿缓存雪崩
核心原因数据根本不存在热点 Key 过期大量 Key 同时过期或 Redis 宕机
Key 数量大量不存在 Key单个热点 Key多个或大部分 Key
数据库状态数据库里也没有数据库里有数据数据库里有数据
主要危害绕过缓存打数据库单个热点打爆数据库大量请求压垮数据库
主要方案缓存空对象、布隆过滤器互斥锁、逻辑过期过期随机、多级缓存、限流降级、高可用

记忆时可以抓住关键词:

穿透:不存在
击穿:热点
雪崩:大量

这道题可以这样回答:

缓存穿透是请求的数据在缓存和数据库中都不存在,请求会绕过缓存持续打到数据库,常用缓存空对象和布隆过滤器解决。缓存击穿是某个热点 Key 在过期瞬间被大量并发请求访问,导致请求同时打到数据库,常用互斥锁或逻辑过期解决。缓存雪崩是大量 Key 同时过期,或者 Redis 服务不可用,导致大量请求同时涌向数据库,常用过期时间加随机值、多级缓存、限流降级和 Redis 高可用解决。

如果面试官继续追问,可以补充方案取舍:

  • 缓存空对象简单,但要设置较短 TTL,避免缓存污染。
  • 布隆过滤器适合拦截大量非法请求,但存在误判为可能存在。
  • 互斥锁能保护数据库,但会增加等待时间。
  • 逻辑过期响应快,但可能返回旧数据。
  • 过期随机能打散失效时间,但不能解决 Redis 宕机。
  • Redis 高可用能提升可用性,但不能替代限流和降级。

这类 Redis 面试题的关键,是把三个概念的根因讲清楚。

缓存穿透:缓存没有,数据库也没有。
缓存击穿:缓存没有,但数据库有,而且是热点 Key。
缓存雪崩:大量缓存同时没有,或者 Redis 整体不可用。

只要先分清根因,再说解决方案,就不会混乱。

最后再补一句工程思维:缓存是为了保护数据库,但缓存系统本身也会失效。所以真正可靠的设计,通常不是只靠一个方案,而是缓存策略、限流降级、异步重建和高可用架构一起配合。