缓存常见问题
缓存雪崩、缓存穿透、缓存击穿详解
缓存雪崩、缓存穿透、缓存击穿,都是高并发系统里常见的缓存问题。它们都会导致请求绕过缓存、直接打到数据库,从而引发数据库压力过大,甚至系统故障。
一、三者分别是什么意思
1. 缓存穿透
缓存穿透是指:请求的数据根本不存在,所以缓存中没有,数据库中也没有,请求每次都会落到数据库。
比如反复请求一个不存在的商品 ID:
productId = -1productId = 999999999
由于这个数据本来就不存在,所以无论查多少次,缓存和数据库都查不到,最终每次都要访问数据库。
2. 缓存击穿
缓存击穿是指:某个热点 key 在失效的一瞬间,大量并发请求同时到来,因为缓存没有命中,这些请求会一起访问数据库。
比如:
- 首页热点商品
- 热门文章详情
- 秒杀商品库存
- 明星用户主页
这些数据访问量很高,如果它们的缓存刚好过期,就可能在短时间内把数据库打爆。
3. 缓存雪崩
缓存雪崩是指:大量缓存 key 在同一时间失效,或者缓存服务整体不可用,导致原本应该由缓存承接的请求,全部打到数据库。
比如:
- 一批缓存统一设置了相同过期时间
- Redis 整体宕机
- 缓存集群不可用
- 系统重启导致大量缓存丢失
这类问题影响范围最大,严重时可能引发系统级联故障。
二、三者的核心区别
| 问题类型 | 含义 | 数据是否存在 | 影响范围 | 典型场景 |
|---|---|---|---|---|
| 缓存穿透 | 查一个根本不存在的数据,缓存和数据库都没有 | 不存在 | 某类不存在的请求 | 恶意攻击、非法参数、异常请求 |
| 缓存击穿 | 某个热点 key 失效,大量并发同时访问数据库 | 存在 | 单个热点 key | 热门商品、热点文章、秒杀库存 |
| 缓存雪崩 | 大量 key 同时失效,或缓存整体挂掉 | 通常存在 | 大批 key 或整个缓存层 | 大量缓存同一时刻过期、Redis 宕机 |
三、缓存穿透的原因与解决方案
1. 原因
缓存穿透常见原因包括:
- 恶意请求不存在的 key
- 参数非法,业务没有做好校验
- 爬虫或机器人频繁访问异常数据
- 用户请求的数据本来就不存在
2. 解决方案
方案一:缓存空值
当数据库查询结果为空时,也把这个空结果缓存起来,比如缓存 null 或特殊标记值。
示例:
- 查
product:10001 - 数据库中不存在
- Redis 中写入:
product:10001 -> null - 设置一个较短 TTL,比如 60 秒或 300 秒
优点:
- 下次再请求同一个不存在的数据时,直接从缓存返回
- 能有效减少重复无效查询
注意:
- 空值缓存时间不要太长
- 否则后续如果该数据被新建,可能短时间读不到最新值
方案二:布隆过滤器
在系统中维护一个布隆过滤器,把所有可能存在的 key 放进去。请求到来时先判断:
- 如果布隆过滤器判定“一定不存在”,直接返回
- 如果布隆过滤器判定“可能存在”,再继续查缓存和数据库
布隆过滤器本质上做两件事:
1.准备一个很长的二进制位数组,初始值全是 0
2.准备多个哈希函数
当一个元素加入布隆过滤器时,比如 "user:1001":用多个哈希函数分别计算位置,假设算出位置是 3、8、15,就把位数组中这些位置设为 1。
查询时也一样,对 "user:1001" 再做同样的多个哈希计算,看 3、8、15 这些位置是不是都为 1。
结果分两种:
1.只要有一个位置是 0,就说明这个元素一定没加入过
2.如果所有位置都是 1,说明它可能加入过
为什么会“可能存在但其实不存在”?
因为不同元素经过哈希后,可能会把同一些位置置为 1
优点:
- 对恶意构造的大量不存在 key 非常有效
- 能从源头减少无效数据库访问
注意:
- 布隆过滤器会有误判率
- 它只能保证“不会把存在的数据误判为不存在”
- 适合 key 集合相对稳定的场景
方案三:参数校验
在请求入口做基础参数合法性校验,比如:
- ID 必须大于 0
- 手机号格式必须正确
- 参数长度、类型、范围合法
优点:
- 简单直接
- 很多非法请求可以在业务最前面拦截掉
方案四:限流与风控
对于异常流量可以增加:
- 接口限流
- 用户限流
- IP 黑名单
- 验证码
- 网关拦截
适用场景:
- 恶意穿透攻击
- 异常流量明显时
四、缓存击穿的原因与解决方案
1. 原因
缓存击穿通常发生在:
- 某个热点 key 访问量非常高
- 这个热点 key 恰好过期
- 大量并发请求同时访问
- 系统没有做并发保护
2. 解决方案
方案一:互斥锁 / 分布式锁
当缓存失效时,只允许一个线程去查数据库和重建缓存,其余线程等待或重试。
流程:
- 请求发现缓存失效
- 尝试获取锁
- 获取锁成功的线程查询数据库并回填缓存
- 其他线程等待后重新查缓存
优点:
- 避免大量请求同时打数据库
注意:
- 锁要设置过期时间,避免死锁
- 等待和重试策略要合理
- 分布式场景通常使用 Redis 锁
方案二:热点数据永不过期
对于极其热点的数据,不设置物理过期时间,而是由业务主动更新缓存。
优点:
- 不会因为缓存过期导致热点 key 被打穿
缺点:
- 需要额外机制保证数据更新
方案三:逻辑过期 + 异步刷新
缓存中不只存数据,还存一个“逻辑过期时间”。
请求到来时:
- 如果未过期,直接返回
- 如果已过期,先返回旧数据,同时异步触发缓存刷新
优点:
- 用户请求不会直接穿透数据库
- 对热点 key 很有效
缺点:
- 短时间内可能读到旧数据
- 适合允许最终一致性的业务
方案四:热点数据预热
在活动开始前、系统启动前,提前把热点数据加载到缓存中。
适用场景:
- 秒杀
- 大促活动
- 首页热点推荐
- 排行榜
五、缓存雪崩的原因与解决方案
1. 原因
缓存雪崩常见原因包括:
- 大量缓存设置了相同过期时间
- Redis 宕机
- 缓存集群异常
- 网络故障导致缓存层不可用
- 系统重启导致缓存大量失效
2. 解决方案
方案一:过期时间加随机值
不要让一批缓存同一时刻失效。
例如:
- 原本 TTL:
3600 - 改为:
3600 + random(1~300)
这样可以让不同 key 的过期时间分散开来,避免集中失效。
优点:
- 简单有效
- 是防止雪崩最常见的手段之一
方案二:缓存高可用
提升缓存服务本身的可用性,例如:
- Redis 主从复制
- Redis Sentinel
- Redis Cluster
- 持久化机制
- 多节点部署
- 多机房容灾
目标:
- 避免单点故障导致整个缓存层失效
方案三:限流、降级、熔断
当缓存异常时,必须保护数据库和后端服务。
常见措施:
- 限流
- 服务降级
- 熔断
- 返回默认值
- 返回静态页面
- 非核心接口暂时关闭
示例:
- 推荐服务降级为默认推荐
- 商品详情暂时展示基础信息
- 非核心接口直接提示“稍后重试”
方案四:多级缓存
构建多层缓存体系,例如:
- 本地缓存
- Redis 分布式缓存
- 数据库
这样即使 Redis 出现问题,本地缓存仍能承接一部分热点流量。
方案五:热点预热
提前将高频访问数据写入缓存,避免缓存冷启动带来的瞬时数据库压力。
方案六:核心数据不过期,后台异步更新
对于特别重要的热点数据,可以不依赖 TTL 自动过期,而是通过:
- 定时任务
- 消息通知
- 后台刷新
来主动更新缓存。
##* 六、三者对比总结
| 对比维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 数据是否存在 | 不存在 | 存在 | 一般存在 |
| 问题对象 | 不存在的数据 | 单个热点 key | 大量 key / 整个缓存层 |
| 并发特征 | 重复访问无效数据 | 高并发访问一个热点数据 | 大量请求同时失去缓存保护 |
| 危害 | 数据库被无效请求拖垮 | 热点数据库查询暴增 | 数据库和系统整体压力骤增 |
| 常见原因 | 恶意攻击、非法参数 | 热点 key 过期 | 批量过期、Redis 宕机 |
| 核心解决方案 | 空值缓存、布隆过滤器、参数校验 | 互斥锁、逻辑过期、热点预热 | TTL 打散、高可用、限流降级、多级缓存 |
七、一个形象的理解方式
可以把缓存理解成书店前台的信息牌,数据库理解成后面的仓库。
缓存穿透
有人一直问一本根本不存在的书。
- 前台信息牌没有
- 仓库里也没有
- 店员每次都去仓库查
- 白跑很多次
缓存击穿
一本特别热门的书,其前台信息牌刚好掉了。
- 大量顾客同时来问这一本到处都在抢的书
- 大家发现前台没有信息
- 店员们一起冲进仓库查同一本书
- 仓库瞬间被挤爆
缓存雪崩
书店所有书的信息牌在同一时间都掉了,或者前台系统直接坏了。
- 大量顾客同时来问不同的书
- 前台都查不到
- 所有人都去仓库找
- 整个书店陷入混乱