从零起步学习Redis || 第九章:缓存雪崩,缓存击穿,缓存穿透三大问题的成因及实战解决方案
前言
在高并发系统中,我们通常会使用 Redis 来做缓存,以减轻数据库压力、提高系统性能。然而,在实际开发中,如果缓存机制设计不当,就容易出现三种经典问题:
缓存雪崩(Cache Avalanche)
缓存击穿(Cache Breakdown)
缓存穿透(Cache Penetration)
这三种问题看起来类似,但根本原因和解决方案却不相同。本文将逐一分析它们的 成因、影响、解决方案及代码实践。
一、缓存雪崩(Cache Avalanche)
1. 概念
缓存雪崩是指:
在同一时间,大量缓存数据同时过期或缓存服务宕机,导致所有请求直接打到数据库,数据库压力骤增甚至崩溃。
2. 成因
所有缓存 key 设置了相同的过期时间(如都在凌晨 0 点失效);
Redis 宕机;
应用重启或缓存被批量清空。
示例:
商品信息缓存设置为 2 小时失效,2 小时后所有 key 同时过期,大量请求瞬间涌向数据库。
3. 解决方案
✅ 方案1:给过期时间加随机值(均匀平均过期时间)
避免同一时刻大量 key 同时失效。
int expire = 3600 + new Random().nextInt(600); // 1小时~1小时10分钟
redisTemplate.opsForValue().set("product:" + id, product, expire, TimeUnit.SECONDS);
✅ 方案2:缓存预热
在系统启动或流量高峰前,提前加载热点数据进缓存。
✅ 方案3:服务降级机制
当缓存不可用时,临时返回默认值、兜底数据或提示稍后再试。
✅ 方案4:多级缓存
例如:
一级:本地缓存(Caffeine、Guava)
二级:Redis
✅ 方案5:互斥锁
当大量业务线程请求Redis发现数据不存在(过期),使用互斥锁锁住一个线程(保证同一时刻只有一个线程去拉取数据到Redis),其他线程没有锁就无法请求,只能等待或者返回空值
✅ 方案6:后台更新缓存
数据直接设置为不过期,如果数据库有变化,再更新Redis中的数据
问题:不过期可能会出现内存淘汰,此时如何处理?
答:业务线程发现缓存数据失效后,用消息队列通知后台线程更新缓存
二、缓存击穿(Cache Breakdown)
1. 概念
缓存击穿是指:
某个热点 key 在失效的瞬间,有大量并发请求同时访问该 key,缓存未命中,导致瞬间大量请求打到数据库。
2. 成因
热点 key 过期(例如秒杀商品、首页 banner 等);
瞬间有大量请求访问该热点 key。
3. 解决方案
✅ 方案1:分布式锁(互斥锁)
防止多个线程同时去加载数据库。
String key = "product:" + id;
String lockKey = "lock:" + key;Object cache = redisTemplate.opsForValue().get(key);
if (cache == null) {if (tryLock(lockKey)) { // 抢到锁Object dbData = queryFromDB(id);redisTemplate.opsForValue().set(key, dbData, 60, TimeUnit.SECONDS);releaseLock(lockKey);} else {Thread.sleep(100); // 没抢到锁的稍等再查缓存return redisTemplate.opsForValue().get(key);}
}
return cache;
✅ 方案2:逻辑过期(永不过期策略)
缓存中保存数据和过期时间字段,过期后后台异步更新,而非直接删除缓存。
✅ 方案3:异步预刷新
使用定时任务在 key 即将过期时提前刷新。
三、缓存穿透(Cache Penetration)
1. 概念
缓存穿透是指:
查询一个 缓存和数据库中都不存在的 key,每次请求都绕过缓存直接打到数据库,导致数据库压力过大。
2. 成因
用户或攻击者请求非法或不存在的 key;
数据库查无此数据,缓存未保存任何结果;
下次相同请求又打到数据库。
示例:
攻击者请求
/product?id=-9999
,Redis 没有,数据库也没有。下次再请求又重复打 DB。
3. 解决方案
✅ 方案1:缓存空对象
查询结果为空时,也写入缓存(可设置较短 TTL)。
Object dbData = queryFromDB(id);
if (dbData == null) {redisTemplate.opsForValue().set("product:" + id, "NODATA", 60, TimeUnit.SECONDS);
} else {redisTemplate.opsForValue().set("product:" + id, dbData, 300, TimeUnit.SECONDS);
}
✅ 方案2:参数校验
对请求参数合法性进行检查,如 ID < 0 或非法字符直接拦截。
✅ 方案3:布隆过滤器(Bloom Filter)
在访问 Redis 之前,先通过布隆过滤器判断 key 是否可能存在。
不存在 → 直接拦截请求;
可能存在 → 再访问 Redis。
if (!bloomFilter.mightContain("product:" + id)) {return null; // 直接拦截
}
return redisTemplate.opsForValue().get("product:" + id);
四、对比总结
问题类型 | 现象 | 成因 | 解决方案 |
---|---|---|---|
缓存雪崩 | 大量缓存同时失效,DB 压力暴增 | 同一时间过期、Redis 宕机 | 随机过期时间、预热、降级、多级缓存 |
缓存击穿 | 热点 key 失效瞬间被并发访问 | 热点 key 过期 | 分布式锁、逻辑过期、异步刷新 |
缓存穿透 | 查询不存在数据反复访问 DB | key 不存在且无缓存 | 缓存空值、参数校验、布隆过滤器 |
五、结语
Redis 缓存的使用是高并发系统中提升性能的关键手段,但如果不处理好缓存的失效机制,系统反而可能因为缓存问题而“自爆”。