Redis 缓存穿透、击穿、雪崩完整解决方案
一、缓存穿透
问题回顾
查询缓存、数据库都不存在的数据,请求直达MySQL,恶意/非法请求会持续压垮库。
方案1:接口层参数校验(前置拦截,首选)
原理
在网关/Controller 层提前过滤非法请求,不让无效请求进入缓存与数据库。
落地规则
- 基础格式校验:ID 必须大于0、手机号/邮箱正则校验、分页参数合法性校验;
- 黑名单拦截:拦截高频恶意IP、异常账号;
- 简单示例(伪代码)
// 商品ID必须为正整数
if (goodsId <= 0) {
return Result.fail("参数非法");
}
优缺点
- 优点:实现简单、零额外开销,从源头拦截;
- 缺点:仅能拦截格式错误,无法拦截合法但不存在的正常参数。
方案2:缓存空值 / 缓存占位符(业务通用,成本低)
原理
数据库查询结果为空时,主动往 Redis 写入空标识,并设置较短过期时间。后续相同请求直接返回,不再查库。
实操步骤
- 查 Redis,存在则直接返回;
- Redis 不存在 → 查询 MySQL;
- MySQL 无数据:
SET key "" EX 300(空值,过期5分钟); - MySQL 有数据:正常写入缓存并返回。
Redis 命令示例
# 数据不存在,缓存空值,5分钟过期
SET goods:-999 "" EX 300
优缺点
- 优点:开发简单、兼容所有业务,中小项目首选;
- 缺点:
- 大量不存在的Key会占用Redis内存;
- 若恶意批量生成不同无效Key,仍会引发穿透。
方案3:布隆过滤器(高并发大流量首选)
原理
提前把数据库所有合法主键/业务Key加载到布隆过滤器。请求进来先校验过滤器:
- 过滤器判断不存在 → 直接拦截,不查Redis、MySQL;
- 过滤器判断存在 → 正常走缓存+数据库流程。
落地流程
- 项目启动/定时任务:全量DB数据,将主键写入布隆过滤器;
- 新增数据:同步写入布隆过滤器;
- 请求链路:接口 → 布隆过滤器 → Redis → MySQL。
核心特性&坑点
- 存在误判:过滤器说“存在”,数据不一定真存在;但说“不存在”,一定不存在;
- 不支持删除:传统布隆过滤器无法单独删除某一条数据,数据删减场景需用计数布隆过滤器;
- 适合场景:数据固定、主键明确(商品ID、用户ID)的高并发系统。
优缺点
- 优点:内存占用极小、拦截效率极高,抵御大规模恶意穿透;
- 缺点:有误判、不支持删除、需要维护数据同步,复杂度高。
方案4:接口限流 & 封禁(防恶意攻击)
原理
对单个IP/账号单位时间内的请求做频率限制,高频异常请求直接封禁。
实现
网关层(Nginx/Spring Cloud Gateway)+ Redis 计数器限流,短时间大量查询不存在数据则拉黑IP。
二、缓存击穿
问题回顾
单个超高并发热点Key 正常存在,因TTL过期,瞬间海量并发同时击穿到数据库。
方案1:分布式互斥锁(通用方案,保证单线程查库)
原理
Key 失效后,只允许一个请求去查询数据库并更新缓存,其他请求等待或重试,避免并发打库。
Redis 实现(推荐 SET NX PX 原子命令)
# NX:不存在才创建(互斥);PX:锁超时时间(防止死锁)
SET lock:goods:123 random_str NX PX 5000
完整执行流程
- 客户端查询热点Key,Redis 不存在;
- 尝试获取分布式锁:
- 获取成功 → 查询MySQL → 写入Redis → 释放锁;
- 获取失败 → 休眠短暂时间后重试查询缓存;
- 锁设置超时时间,避免服务宕机导致死锁。
伪代码流程
public Goods getGoods(Long id) {
String key = "goods:" + id;
String lockKey = "lock:goods:" + id;
// 1.查缓存
String cache = redis.get(key);
if (cache != null) {
return parse(cache);
}
// 2.尝试加锁
boolean lock = redis.set(lockKey, uuid, "NX", "PX", 5000);
if (lock) {
try {
// 3.查数据库
Goods goods = db.query(id);
// 4.写入缓存
redis.set(key, JSON.toJSONString(goods), 3600);
return goods;
} finally {
// 5.释放锁
redis.del(lockKey);
}
} else {
// 6.加锁失败,短暂休眠后重试
Thread.sleep(100);
return getGoods(id);
}
}
优缺点
- 优点:实现简单、数据一致性强,适配绝大多数场景;
- 缺点:高并发下会有少量请求等待,轻微影响吞吐量。
方案2:逻辑过期(热点商品首选,无等待、高可用)
原理
Redis 中 Key 永久不删除(不设置TTL),在 Value 内部存储逻辑过期时间。
- 查询时先判断逻辑时间:未过期 → 直接返回数据;
- 已逻辑过期:异步线程更新缓存,当前请求直接返回旧数据,无阻塞、无击穿。
数据结构设计
{
"data": { "id":123, "name":"商品" },
"expireTime": 1789000000000 // 逻辑过期时间戳
}
执行流程
- 查询Redis,数据存在;
- 判断逻辑时间:未过期 → 直接返回;
- 已过期:通过互斥锁控制只有一个异步线程更新缓存,主线程返回旧数据;
- 用户始终有数据返回,不会打到数据库。
优缺点
- 优点:性能极高、无并发等待,完美解决热点Key击穿;
- 缺点:存在数据短暂不一致(容忍旧数据),金融、支付等强一致性场景禁用。
方案3:热点Key 永不过期(最简方案)
原理
对秒杀、首页、活动配置等绝对热点Key,不设置过期时间,由人工/定时任务主动更新缓存。
适用场景
运营可控、变更频率低的热点数据(首页轮播、活动规则)。
优缺点
- 优点:零代码改造、彻底杜绝击穿;
- 缺点:数据更新不及时,需额外维护更新逻辑。
三、缓存雪崩
问题回顾
两类诱因:① 大量Key集中过期;② Redis集群宕机/不可用。大面积缓存失效,请求批量压垮数据库。
场景一:大量Key集中过期 解决方案
方案1:过期时间随机打散(最基础、必做)
原理
在原有TTL基础上,叠加随机时间,错开批量过期峰值。
示例
原过期时间 1小时:
// 基础3600秒 + 0~600秒随机值
int ttl = 3600 + new Random().nextInt(600);
redis.set(key, value, ttl);
优缺点
- 优点:一行代码改造,成本最低,缓解集中过期;
- 缺点:只能缓解,无法彻底杜绝,需配合其他方案。
方案2:分层TTL 设计
核心业务、非核心业务使用不同过期档位,避免全域Key同时失效。
场景二:Redis 服务宕机/集群不可用 解决方案
方案1:Redis 高可用架构(基础保障)
- 主从 + 哨兵:主节点宕机,哨兵自动选从库升级为主节点,故障自动转移;
- Redis Cluster 集群:分片+主从,部分节点故障不影响整体服务。
核心:避免单机单点故障。
方案2:多级缓存(本地缓存 + 分布式缓存)
原理
架构叠加 JVM本地缓存(Caffeine/Guava) + Redis 两层缓存。
- 请求优先查本地缓存;
- 本地无 → 查Redis;
- Redis宕机,本地缓存依然兜底,请求不会直达数据库。
架构链路
请求 → 本地Caffeine缓存 → Redis → MySQL
优缺点
- 优点:大幅提升容错性、减轻Redis压力;
- 缺点:本地缓存存在数据不一致,集群节点数据不同步。
方案3:服务限流、熔断、降级(兜底防线)
基于网关/服务框架实现,Redis全面不可用时启动保护:
- 限流:限制数据库总请求量,防止压垮;
- 熔断:检测到MySQL压力过大,直接断开对DB的访问;
- 降级:返回默认兜底数据、静态页面、提示文案,放弃非核心业务。
常用组件:Sentinel、Hystrix、Gateway。
方案4:搭建异地/多机房缓存集群
超大型项目,做多机房Redis容灾,单机房故障切换至备用机房。
四、方案选型总结(生产落地建议)
1. 缓存穿透 选型优先级
- 普通业务:参数校验 + 缓存空值(性价比最高);
- 高并发/防恶意攻击:参数校验 + 布隆过滤器 + 限流。
2. 缓存击穿 选型优先级
- 强数据一致性:分布式锁;
- 高并发热点、允许短暂不一致:逻辑过期;
- 低变更热点配置:永不过期。
3. 缓存雪崩 选型优先级
- 预防集中过期:TTL随机打散(所有项目必加);
- 防Redis宕机:主从/Cluster高可用 + 多级缓存;
- 最终兜底:限流+熔断+降级。
五、面试标准作答(精简版)
- 缓存穿透:查询不存在数据,请求直达DB。处理:接口参数校验、缓存空值、布隆过滤器、IP限流。
- 缓存击穿:单个热点Key过期,并发打库。处理:分布式互斥锁、逻辑过期、热点Key不设过期。
- 缓存雪崩:批量Key集中过期或Redis宕机。处理:过期时间加随机值、Redis高可用、本地多级缓存、服务限流熔断降级。
关联文档
- 布隆过滤器 — Bloom Filter 原理与实现
- Redis 核心原理 — Redis 核心知识点
- Redis 基础 — Redis 基础入门