【Redis】 缓存三兄弟:穿透 / 击穿 / 雪崩,一文搞定定义、场景与实战方案
在 Redis 面试中,缓存穿透、缓存击穿、缓存雪崩是高频必问的 “缓存三兄弟”,也是后端开发中最容易踩坑的三大问题。
很多人能说出概念,却分不清场景;能背出解决方案,却讲不清原理和区别。
缓存穿透

缓存穿透是指大量请求查询根本不存在的数据,导致每次请求都无法命中缓存,直接穿透到数据库,最终压垮数据库的问题。
一、通俗理解
就像一家奶茶店,有人一直问“有没有卖不存在的隐藏款奶茶”,店员每次都要去仓库确认,最后被这些无效请求累垮。
二、专业定义
- 核心特征:查询的 key 在缓存和数据库中都不存在。
- 典型场景:恶意攻击(用大量不存在的 ID 刷接口)、业务逻辑漏洞(参数校验不严)。
- 风险:数据库被大量无效查询打满,影响正常业务。
三、解决方案(结合项目实战)
方案1:缓存空值(简单直接)
- 思路:对查询结果为空的 key,也在缓存中存一个空值(或占位符),并设置短过期时间(如 60 秒)。
- 优点:实现简单,快速见效。
- 缺点:会占用额外缓存空间,且空值过期后仍可能穿透。
public Object getData(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 缓存未命中,查数据库
Object data = dbMapper.queryData(key);
if (data == null) {
// 缓存空值,设置短过期时间
redisTemplate.opsForValue().set(key, "NULL", 60, TimeUnit.SECONDS);
return null;
} else {
redisTemplate.opsForValue().set(key, data, 30, TimeUnit.MINUTES);
return data;
}
}
方案2:布隆过滤器(高效过滤)

- 思路:提前将所有可能存在的 key 存入布隆过滤器,请求先过过滤器,不存在则直接返回。
- 优点:空间效率极高,能过滤 99% 以上的无效请求。

- 缺点:存在极小的误判率(过滤器说存在,实际可能不存在),但绝不会漏判。
// 初始化布隆过滤器,预期元素数量100万,误判率0.1%
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, 0.001);
// 项目启动时,将所有有效key加载到布隆过滤器
public void initBloomFilter() {
List<String> allKeys = dbMapper.getAllKeys();
bf.addAll(allKeys);
}
public Object getData(String key) {
// 先过布隆过滤器
if (!bf.mightContain(key)) {
return null; // 不存在,直接返回
}
// 后续流程:查缓存 -> 查数据库 -> 回写缓存
// ...
}
方案3:接口层校验(源头拦截)
- 思路:在 API 入口层对请求参数做合法性校验,比如 ID 范围、格式校验,直接拦截非法请求。
- 优点:从源头杜绝无效请求,成本最低。
- 实战:比如商品 ID 必须是正整数,且在系统已存在的 ID 范围内。
四、建议
- 简单场景:优先用缓存空值,快速解决问题。
- 高并发/恶意攻击场景:用布隆过滤器 + 参数校验,双重防护。
- 终极方案:多层防护,参数校验 + 布隆过滤器 + 缓存空值,从源头到缓存全链路拦截。
缓存击穿
一、什么是缓存击穿?

就像一家网红奶茶店,只有一款爆款奶茶(热点key)特别火,所有人都来买这款。结果某天这款奶茶的原料刚好用完了(热点key过期),瞬间所有顾客都堵在收银台问“能不能做”,收银台(数据库)被问得手忙脚乱,直接卡壳——这就是缓存击穿:单个热点key在过期的瞬间,大量并发请求直接打穿缓存,冲击数据库。
二、专业定义+核心场景
核心定义:缓存击穿是指某个高频访问的热点key,在其缓存有效期失效的瞬间,恰好有大量并发请求访问该key;由于缓存未命中,所有请求全部落到数据库上,导致数据库短时间内承受巨大压力,甚至宕机。
典型场景:
- 电商秒杀的商品详情key、首页热点数据key;
- 突发流量的热点内容(比如热搜词条、爆款商品);
- 缓存key设置了统一过期时间,且该key是全量请求的核心依赖。
三、解决方案(从易到难,结合实战+专业术语)
我们项目里针对不同场景用了不同方案,优先级从高到低:

1. 方案1:热点key永不过期 + 后台定时刷新(首选)
通俗解释:把爆款奶茶的原料提前备足,永远不缺货(key不过期),店员(后台线程)每隔一段时间检查原料库存,快用完了就补货(定时刷新缓存)。
专业实现:
- 对热点key不设置过期时间,避免过期瞬间的请求冲击;
- 启动独立的后台线程(比如Spring的@Scheduled),按固定频率主动查询数据库,更新Redis中的热点key值;
- 优点:零并发风险,实现简单;缺点:需提前识别热点key,少量内存占用(但热点key通常数量少,可接受)。
2. 方案2:互斥锁(分布式锁)控制缓存重建(通用方案)
通俗解释:奶茶原料用完后,只允许一个顾客(线程)去仓库补货(重建缓存),其他顾客排队等待,补货完成后所有人都能买到奶茶。
专业实现:
- 当请求发现缓存未命中时,先通过Redis的SETNX(或Redisson的RLock)获取分布式锁;
- 只有拿到锁的线程去数据库查询数据,更新缓存;
- 未拿到锁的线程等待一小段时间(比如50ms)后重试,此时缓存已被重建,直接命中缓存;
核心代码(Redisson实现):
public String getHotData(String key) {
// 1. 先查缓存
String value = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
return value;
}
// 2. 缓存未命中,加分布式锁
RLock lock = redissonClient.getLock("lock:cache:" + key);
try {
// 尝试加锁,等待1秒,锁过期30秒(防止死锁)
if (lock.tryLock(1, 30, TimeUnit.SECONDS)) {
// 3. 拿到锁后,再次查缓存(双重检查,避免重复重建)
value = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(value)) {
// 4. 查数据库,重建缓存(设置过期时间,兜底)
value = dbMapper.queryData(key);
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
return value;
} else {
// 5. 未拿到锁,重试(短休眠后查缓存)
Thread.sleep(50);
return getHotData(key);
}
} catch (Exception e) {
log.error("获取热点数据异常", e);
// 兜底:直接查数据库(避免服务不可用)
return dbMapper.queryData(key);
} finally {
// 释放锁(只释放当前线程持有的锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
四、总结
总结一下,缓存击穿的核心是“热点key过期+高并发”,我们项目里的选型原则是:
- 能提前识别的热点key(比如秒杀商品),用“永不过期+后台刷新”,最简单高效;
- 无法提前识别的热点key,用“分布式锁+双重检查”,通用且可靠;
- 超大规模热点场景,叠加布隆过滤器兜底;
同时我们会监控Redis的缓存命中率、数据库慢查询,提前预警热点key,从源头减少击穿风险。
核心加分点
- 结合实战:不说纯理论,而是讲“我们项目里用了XX方案”,体现落地能力;
- 双重检查:在加锁后再次查缓存,这是面试里的细节加分项(避免多线程等待锁后重复查库);
- 兜底思维:代码里考虑“释放锁的边界条件”“最终兜底查数据库”,体现健壮性;
- 监控预警:最后提到监控,体现全链路思维(不止解决问题,还能预防问题)。
缓存雪崩

缓存雪崩是指大量缓存Key在同一时间集中过期失效,或者Redis服务本身宕机,导致原本由缓存承载的请求瞬间全部压到数据库,从而引发数据库压力过载甚至宕机的问题。
一、通俗理解
就像景区里所有的检票闸机(缓存)同时坏了,所有游客(请求)瞬间都涌向唯一的人工窗口(数据库),导致窗口被挤爆,整个景区瘫痪。
二、专业定义
- 核心特征:大量Key同时失效或缓存服务不可用,导致请求雪崩式涌向数据库。
- 典型场景:
-
- 大量Key设置了相同的过期时间,导致集中过期。
- Redis集群故障、主从切换失败,导致缓存层不可用。
- 突发流量(如促销活动),放大了缓存失效的影响。
- 风险:数据库被瞬间打满,服务雪崩,影响整个系统的可用性。
三、解决方案(结合项目实战)

方案1:过期时间打散(最常用,解决集中过期)
- 思路:在设置Key的过期时间时,加上一个随机偏移量,避免大量Key在同一时间过期。
- 优点:实现简单,效果显著,是解决缓存雪崩最基础的手段。
// 基础过期时间30分钟,加上0-300秒的随机偏移
long baseExpire = 30 * 60;
long randomOffset = new Random().nextInt(300);
long finalExpire = baseExpire + randomOffset;
redisTemplate.opsForValue().set(key, value, finalExpire, TimeUnit.SECONDS);
方案2:多级缓存架构(兜底防护)
- 思路:构建“本地缓存(如Caffeine)+ 分布式缓存(Redis)”的多级架构。
- 流程:请求先查本地缓存 -> 再查Redis -> 最后查数据库。当Redis失效时,本地缓存可以继续提供服务,为数据库减压。
- 优点:提供了强大的兜底能力,即使Redis宕机,系统仍能提供降级服务。
方案3:服务熔断与降级(流量控制)
- 思路:使用Sentinel或Hystrix等组件,对数据库访问进行限流、熔断。当数据库压力超过阈值时,直接返回降级数据(如默认值、缓存的旧数据),防止数据库被打垮。
- 优点:从流量控制层面保护核心服务,保证系统在极端情况下的可用性。
方案4:缓存预热与永不过期(热点Key)
- 思路:
-
- 缓存预热:在系统启动或流量高峰前,主动将热点数据加载到缓存中。
- 永不过期:对核心热点Key不设置过期时间,而是通过后台定时任务主动更新。
- 优点:从源头避免热点Key过期,彻底杜绝因热点Key集中失效导致的雪崩。
方案5:Redis高可用部署(避免缓存服务宕机)
- 思路:采用Redis集群(Cluster)、主从复制(Master-Slave)+哨兵(Sentinel)模式,实现Redis的高可用。
- 优点:当某个节点故障时,能自动进行主从切换,保证缓存服务持续可用,从根本上避免因缓存服务不可用导致的雪崩。
四、对比缓存击穿
与缓存击穿的区别:核心差异在于影响的 Key 数量不同
- 缓存雪崩:针对多个 Key,是大量缓存同时失效引发的问题;
- 缓存击穿:针对单个热点 Key,是单个 Key 过期瞬间,大量并发请求打穿缓存引发的问题。
五、建议
- 预防为主:通过过期时间打散和热点Key永不过期,从源头减少缓存失效的概率。
- 兜底为辅:构建多级缓存和服务熔断降级,即使发生雪崩,也能保证核心服务可用。
- 架构保障:部署Redis高可用集群,避免单点故障,提升整个缓存层的可靠性。
更多推荐




所有评论(0)