一文彻底搞懂 Redis 缓存穿透、击穿、雪崩与熔断机制
一文彻底搞懂 Redis 缓存穿透、击穿、雪崩与熔断机制
本文将以实际开发中最常见的缓存异常问题为核心,系统梳理三大缓存问题:穿透、击穿、雪崩,并引入熔断降级机制,帮助你构建高可用、高容错的缓存体系,适用于电商、门户、大数据接口等高并发场景。
一、背景与引子
在大规模系统架构中,Redis 被广泛用作高性能缓存中间件,承担着请求的第一道防线。然而随着业务量增长,单纯的缓存并不能解决所有问题,反而在某些场景下变成了隐形炸弹。如果某个设计疏忽,可能引发连锁反应,导致:
-
Redis 被击穿 → DB 瞬间崩溃
-
缓存雪崩 → 所有请求命中数据库,服务不可用
-
用户大量访问非法数据 → Redis / DB 双重压力
-
Redis 宕机 → 无熔断策略,系统直接返回 500
这些问题归根结底可以划分为三大核心问题 + 一种防御机制:
穿透、击穿、雪崩 + 熔断
接下来逐个深入分析。
二、缓存穿透(Cache Penetration)
2.1 什么是缓存穿透?
缓存穿透指的是用户请求的数据既不在缓存中,也不在数据库中,因此每次请求都会落到数据库。
场景举例:
-
攻击者发送大量随机 userId、productId
-
普通用户请求一个已被删除或不存在的 ID
-
接口参数未校验,允许传递负数、空串等非法值
2.2 结果影响
-
Redis 无命中
-
数据库压力倍增
-
可能被恶意刷崩(典型 DDoS)
2.3 解决方案
✅ 缓存空值
if (value == null) {
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
}
-
建议设置较短过期时间(如 5分钟)
-
注意不能对所有业务都缓存 null,否则缓存污染
✅ 参数校验拦截
if (id <= 0) {
return null; // 拦截非法参数
}
-
常用于控制层或网关层提前校验参数合法性
✅ 布隆过滤器(BloomFilter)
用于拦截不存在的 key,避免每次都访问 Redis/DB。
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 1_000_000);
filter.put(1L); // 初始化已有 ID
if (!filter.mightContain(id)) return null;
-
场景:商品ID、用户ID大规模数据量初始化
-
Guava 或 Redisson 都支持布隆过滤器
三、缓存击穿(Cache Breakdown)
3.1 什么是缓存击穿?
某个高并发热点 key在某一刻过期,造成大量请求同时落入数据库。
3.2 场景复现
比如“秒杀详情页”、“爆款商品页”,一个 key 的 TTL 正好到期时,Redis 失效,大量请求涌入 DB。
3.3 解决方案
✅ 互斥锁方案(mutex)
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isLock)) {
// 自旋重试
Thread.sleep(50);
return getFromCache(id);
}
try {
String dbData = queryDb(id);
redisTemplate.opsForValue().set(key, dbData, 30, TimeUnit.MINUTES);
} finally {
redisTemplate.delete(lockKey);
}
-
Redisson 提供更安全分布式锁
-
自旋+延迟策略防止请求风暴
✅ 逻辑过期(推荐)
数据未真正失效,但后台自动更新。
@Data
class RedisData {
private Object data;
private LocalDateTime expireTime;
}
-
请求先判断
expireTime是否过期 -
过期后开启线程池异步更新,不影响当前请求响应
if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {
threadPool.submit(() -> rebuildCache(id));
}
✅ 异步队列 + 延迟双删(高并发)
-
先删缓存 → 异步延迟删除 → 再写缓存
-
延迟补偿机制防止写缓存被其他线程“脏读”
四、缓存雪崩(Cache Avalanche)
4.1 什么是缓存雪崩?
在同一时间内,大量缓存 key 同时失效,导致数据库雪崩式崩溃。
4.2 场景
-
系统重启时未做预热
-
缓存 TTL 设置为同一时间段
-
Redis 整体宕机
4.3 解决方案
✅ 给 key 设置随机 TTL
int base = 60 * 30;
int offset = new Random().nextInt(60 * 10);
redisTemplate.opsForValue().set(key, value, base + offset, TimeUnit.SECONDS);
-
避免大规模 key 同时过期
✅ 数据预热
-
启动时从 DB 将热点数据 load 到 Redis
-
可使用初始化脚本/定时任务完成
✅ 本地缓存兜底(Caffeine)
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::queryDb);
小型数据量、高访问频率的接口,可加 JVM 层缓存兜底
✅ Redis 集群 + Sentinel 监控
-
使用哨兵或 Redis Cluster 提供高可用
-
若主节点挂掉可自动 failover
五、熔断与降级策略
5.1 为什么要做熔断?
一旦 Redis 故障,或者响应超时,系统没有任何兜底手段将会直接崩溃。
5.2 实践场景
-
Redis 无法连接或响应慢
-
网络波动引发访问阻塞
-
不做隔离 → 主业务模块被 Redis 拖死
5.3 解决方案
✅ Redis 操作加异常捕获
try {
String data = redis.get(key);
return data != null ? data : fallback();
} catch (Exception e) {
return fallback(); // 降级返回
}
✅ 限流熔断框架集成(Sentinel / Resilience4j)
-
设置失败比例阈值(如 50%)
-
设置半开恢复窗口(如 30 秒后尝试恢复)
-
可以对 Redis 封装成独立接口调用
✅ fallback 逻辑建议
-
对低一致性要求的接口返回兜底数据
-
高一致性建议返回“稍后再试”
六、总结与最佳实践建议
| 问题 | 触发条件 | 解决方式 | 实践建议 |
|---|---|---|---|
| 穿透 | 数据不存在 | 布隆过滤器、空值缓存 | 非法参数校验、定期清理 null |
| 击穿 | 热点数据过期 | 加锁/逻辑过期 | 热点数据隔离设计 |
| 雪崩 | 大量缓存同时失效 | TTL 随机化、本地缓存、预热 | Redis 分区集群化部署 |
| 熔断 | Redis 宕机/慢响应 | 异常捕获+fallback | 配合限流框架自恢复 |
七、结语:稳定性不是靠补丁,是靠设计
稳定性架构从来不是“问题发生后再补”,而是提前设计、预估极限、落地容灾方案。
缓存设计如果缺乏熔断/降级思维,可能成为系统不可承受之痛。
建议构建自己的:
-
缓存工具类封装统一接口
-
fallback 降级策略结合业务要求
-
异步更新/延迟双删机制集成线程池
-
Redis 状态监控告警机制(配合 Prometheus、Grafana)
更多推荐
所有评论(0)