Redis 缓存穿透、击穿、雪崩:成因、方案与实战指南
在高并发系统中,Redis 缓存是保护数据库的第一道城墙。但当这道城墙出现裂缝,数据库面临的可能不是压力,而是灭顶之灾。穿透、击穿、雪崩——三个听起来相似的名词,背后却是三种截然不同的灾难场景。
一、缓存穿透:无孔不入的幽灵请求
1.1 什么是缓存穿透?
一句话概括:请求的数据,缓存里没有,数据库里也没有。
Redis 查不到 → 请求穿透到数据库 → 数据库也查不到 → 返回空。整个过程中,缓存形同虚设,每一次请求都在无意义地消耗数据库资源。
典型场景:
- 恶意攻击:爬虫循环递增商品 ID(1001、1002、1003……),大量不存在的 Key 绕过缓存直击数据库
- 业务 Bug:前后端 Key 规则不一致,前端传了一个根本不存在的用户 ID
- 非法请求:
id = -1、id = 999999999这种明显越界的参数
1.2 三道防线,层层拦截
| 防线 | 方案 | 原理 | 优缺点 |
|---|---|---|---|
| 第一道:接口层校验 | 参数合法性检查 | ID 必须为正整数、长度在合理范围内,不合规直接返回 400 | ✅ 成本极低,拦截大量低级攻击 ❌ 无法拦截"格式合法但不存在"的 ID |
| 第二道:缓存空值 | 查库为空时,将空结果写入 Redis,设置短过期时间(2~5 分钟) | 后续相同请求直接命中空值缓存,不再穿透 | ✅ 实现极简 ❌ 大量随机 Key 攻击会占用 Redis 内存 |
| 第三道:布隆过滤器 | 启动时将所有合法 ID 存入布隆过滤器,请求先过滤再查缓存 | 过滤器判定"一定不存在"→ 直接拦截;判定"可能存在"→ 放行 | ✅ 内存占用极小,查询极快 ❌ 有误判率(约 1%),不支持删除 |
实战建议:
- 一般业务:缓存空值 + 接口校验 足够
- 对抗恶意攻击:必须上 布隆过滤器
- 布隆过滤器初始化示例(Redisson):
java
1RBloomFilter<Long> productFilter = redissonClient.getBloomFilter("product:bloom");
2productFilter.tryInit(1_000_000L, 0.01); // 预计100万元素,误判率1%
3
查询时先过滤:
java
1if (!bloomFilterService.mightContain(id)) {
2 return null; // 直接拦截,不查缓存不查库
3}
4
二、缓存击穿:一把钥匙毁掉一座城
2.1 什么是缓存击穿?
一句话概括:一个热点 Key 过期的瞬间,大量并发请求同时涌入数据库。
注意与穿透的区别:
| 缓存穿透 | 缓存击穿 | |
|---|---|---|
| 数据是否存在 | 缓存和数据库都不存在 | 缓存不存在,但数据库存在 |
| 影响范围 | 所有无效请求 | 集中在单个热点 Key |
| 典型例子 | 查不存在的 user_id=99999 | 爆款商品缓存过期,1000 人同时查询 |
核心矛盾: 热点 Key(如秒杀商品、首页推荐)访问量远超普通 Key,缓存失效后没有缓冲,所有请求瞬间打穿到数据库。
2.2 三种解法,按场景选用
方案一:分布式互斥锁(最常用)
缓存失效时,只允许一个线程去查库并重建缓存,其他线程等待。
java
1RLock lock = redissonClient.getLock("lock:product:" + productId);
2if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
3 try {
4 // 双重检查:防止其他线程已重建缓存
5 String cached = redisTemplate.opsForValue().get(key);
6 if (cached != null) return JSON.parseObject(cached, Product.class);
7
8 // 查库 + 回写缓存
9 Product product = productMapper.selectById(productId);
10 redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
11 return product;
12 } finally {
13 lock.unlock();
14 }
15} else {
16 Thread.sleep(50);
17 return getProductInfo(productId); // 递归重试
18}
19
⚠️ 隐患: 查库 + 重建缓存耗时过长时,其他线程会阻塞,高并发下吞吐量下降。禁止在高并发场景无脑使用!
方案二:热点数据"永不过期"
物理上不设置过期时间,逻辑上通过后台异步线程定时更新。
java
1// 缓存时不设 expire
2redisTemplate.opsForValue().set(key, JSON.toJSONString(product));
3
4// 后台定时任务每 29 分钟刷新
5@Scheduled(fixedRate = 29 * 60 * 1000)
6public void refreshHotCache() {
7 Product latest = productMapper.selectById(hotProductId);
8 redisTemplate.opsForValue().set(key, JSON.toJSONString(latest));
9}
10
✅ 性能最友好,无击穿风险
❌ 存在短暂数据不一致,适合非实时业务(如首页推荐)
方案三:缓存预热
系统启动或大促前,提前将热点数据加载到缓存,避免上线瞬间缓存失效。
适合热点数据明确的场景(电商首页、秒杀商品),配合方案二效果最佳。
三、缓存雪崩:万马奔腾式的系统崩溃
3.1 什么是缓存雪崩?
一句话概括:大量缓存同时失效,或 Redis 整体不可用,所有请求瞬间压垮数据库。
两大成因:
| 成因类型 | 典型场景 |
|---|---|
| 集中过期 | 批量设置相同过期时间(如所有商品统一 24 小时过期),到期瞬间集体失效 |
| 服务不可用 | Redis 单点故障、集群分片宕机、网络中断,缓存整体不可用 |
与击穿的区别:
| 缓存击穿 | 缓存雪崩 | |
|---|---|---|
| 失效 Key 数量 | 1 个热点 Key | 大量 Key 同时失效 |
| 影响范围 | 单点业务 | 全局业务 |
| 数据库压力 | 瞬间高 QPS | 持续性高压,可能直接宕机 |
3.2 "预防 + 加固 + 兜底" 三层防御体系
🛡️ 第一层:预防——打散过期时间(成本最低,效果最好)
核心思路:避免所有 Key 的过期时间完全一致。
java
1// ❌ 错误:所有商品统一 1 小时过期
2redis.setex("product:1001", 3600, data);
3redis.setex("product:1002", 3600, data);
4
5// ✅ 正确:基础时间 + 随机偏移
6int baseExpire = 3600;
7int randomExpire = new Random().nextInt(600); // 0~10 分钟随机
8redis.setex(key, baseExpire + randomExpire, value);
9
效果: 每个缓存的过期时间重复率大幅降低,集体失效概率趋近于零。
🛡️ 第二层:加固——Redis 高可用部署
| 部署模式 | 适用场景 | 特点 |
|---|---|---|
| 主从复制 + 哨兵 | 中小规模 | 主节点宕机后秒级切换 |
| Redis Cluster | 大规模高并发 | 分片存储,单分片故障不影响整体 |
| 多活集群(跨机房) | 核心金融/电商 | 异地容灾,机房级故障自动切换 |
⚠️ 铁律:禁止单机部署! 至少保证 1 主 2 从,开启 RDB + AOF 混合持久化。
🛡️ 第三层:兜底——限流 + 熔断 + 降级
即使前两层都失效,这是保护数据库的最后一道防线:
- 接口限流: 网关层限制单接口 QPS(如商品查询每秒最多 2000 次),超出直接返回"服务繁忙"
- 服务熔断: 监控数据库响应,错误率超阈值时自动熔断,返回缓存旧数据或默认值
- 降级策略: 核心业务返回旧缓存,非核心业务(评论、收藏)直接关闭,保住下单、查询等主链路
四、三种问题对比速查表
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 本质 | 查了不存在的数据 | 热点 Key 过期瞬间 | 大量 Key 集中失效 / Redis 宕机 |
| 数据状态 | 缓存❌ 数据库❌ | 缓存❌ 数据库✅ | 缓存❌ 数据库✅ |
| 影响范围 | 全局(所有无效请求) | 单点(一个热点 Key) | 全局(所有业务) |
| 核心方案 | 布隆过滤器 + 缓存空值 | 互斥锁 / 永不过期 | 随机过期 + 高可用 + 熔断 |
| 最致命场景 | 恶意攻击 | 秒杀活动 | Redis 集群宕机 |
五、最佳实践清单
- 组合使用,不要单打独斗: 布隆过滤器(拦截非法请求)+ 缓存空值(兜底重复查询)+ 互斥锁(重建缓存)是生产环境的标准三件套
- 过期时间必加随机值: 这是性价比最高的单一操作,能同时防范击穿和雪崩
- 热点数据单独处理: 识别 Top N 热点 Key,走"永不过期 + 后台异步更新"路线
- 监控必须到位: 实时监控 Redis 命中率(低于 90% 立即告警)、QPS、过期 Key 数量、数据库连接数
- 压测验证: 大促前模拟缓存集中过期、Redis 宕机场景,验证防御方案有效性
写在最后
缓存穿透、击穿、雪崩,本质上都是 "缓存失效后的流量过载" ,但触发点不同,解法也不同。
- 穿透防的是 "不该来的请求"
- 击穿防的是 "不该同时来的请求"
- 雪崩防的是 "不该一起失效的缓存"
理解这三个"不该",你就掌握了 Redis 缓存防御的核心逻辑。剩下的,就是根据业务场景选择合适的组合策略,然后——做好监控,定期压测,别等出了事再救火。