Redis 为什么会发生缓存穿透、击穿、雪崩?
上一篇我们聊了《Redis 为什么不会每次修改都写磁盘?》,了解了 Redis 如何通过 AOF 刷盘策略,在数据安全和性能之间寻找平衡。
Redis 通过内存提供高速访问能力,通过持久化机制保证数据可以恢复。
但是在真实业务中,Redis 最大的挑战并不是"数据能不能保存"。
而是:当大量请求同时访问 Redis 时,系统还能不能稳定运行。
例如一个商品详情接口:
用户请求商品信息
↓
查询 Redis
↓
存在
↓
直接返回
不存在
↓
查询 MySQL
↓
写入 Redis
看起来非常简单。
但是,当请求量突然增加时,会出现三个非常经典的问题:
缓存穿透
缓存击穿
缓存雪崩
很多面试会问:
Redis 缓存三大问题是什么?
但真正需要理解的是:
为什么这些问题会发生?
因为它们背后反映的是缓存系统设计中的三个核心矛盾:
请求的数据根本不存在怎么办?
热点数据突然失效怎么办?
大量缓存同时失效怎么办?
Redis 为什么需要缓存?
在理解三个问题之前,需要先明确一个基础:
为什么业务系统需要 Redis?
假设一个商品查询:
select *
from product
where id = 1001;
如果每一次请求都访问 MySQL:
用户请求
↓
MySQL
↓
返回结果
当访问量增加时:
100 个请求
↓
MySQL
可能还能处理。
但是:
10 万请求
↓
MySQL
数据库压力会快速增加。
因为数据库不仅需要查询数据,还需要:
SQL 解析
索引查找
数据页读取
网络传输
事务处理
所以系统通常会增加 Redis:
第一次请求
↓
查询 MySQL
↓
写入 Redis
后续请求
↓
Redis
↓
直接返回
Redis 承担了大量热点访问,但是缓存并不是万能的。
它引入了新的问题。什么是缓存穿透?
缓存穿透指的是:
请求的数据,在缓存和数据库中都不存在,但是请求不断访问系统。
例如:
接口:GET /user/999999
Redis:不存在
MySQL:不存在
但是攻击者不断请求:
/user/999999
/user/999998
/user/999997
...
整个流程变成:
用户请求
↓
Redis查询
↓
没有数据
↓
查询MySQL
↓
没有数据
↓
返回不存在
问题在哪里?
Redis 没有缓存这个"不存在"。所以每一次请求都会穿过 Redis,直接访问数据库。
这就是缓存穿透。
为什么缓存穿透很危险?因为正常缓存流程:
请求
↓
Redis
↓
命中
↓
返回
数据库压力很小。但是缓存穿透:
大量请求
↓
Redis
↓
全部未命中
↓
MySQL
Redis 完全失去了保护数据库的作用。
如果请求量足够大:
大量不存在的数据
↓
绕过缓存
↓
打到数据库
↓
数据库压力过高
↓
服务异常
所以缓存穿透本质上是:缓存无法拦截无效请求。
如何解决缓存穿透?常见方案有三个。
方案一:缓存空对象
最简单的方法:即使数据库没有数据,也把不存在的结果缓存起来。
例如:
查询:user:999999
数据库:null
Redis 保存:user:999999 = null
下一次请求:
请求
↓
Redis
↓
发现是空值
↓
直接返回不存在
流程变成:
第一次请求
↓
Redis没有
↓
查询数据库
↓
保存空结果
第二次请求
↓
Redis命中空结果
↓
直接返回
这样可以避免大量相同不存在请求访问数据库。
但是有一个问题:
如果缓存大量不存在的数据:
user:100001
user:100002
user:100003
会占用 Redis 空间。
所以通常会设置较短过期时间。
例如:
空对象缓存 5 分钟
方案二:布隆过滤器
另一种方式是:
在 Redis 前增加一个过滤层。
例如:
请求
↓
布隆过滤器
↓
判断数据是否存在
↓
不存在
↓
直接返回
布隆过滤器特点:它可以快速判断:"这个数据一定不存在。"
例如:
商品 ID:
1001
1002
1003
加入布隆过滤器。
请求:9999
过滤器判断:不存在直接拒绝。
这样请求不会进入 Redis 和数据库。但是布隆过滤器有一个特点:可能误判存在。
也就是说:不存在的数据,一定能判断不存在。存在的数据,可能判断存在。
所以它适合作为第一层过滤。
什么是缓存击穿?
缓存击穿和缓存穿透很容易混淆。
缓存穿透:请求的数据根本不存在。
缓存击穿:请求的数据存在,但是突然失效。
例如:
某个热点商品:product:1001
平时:Redis
product:1001
↓
直接返回
但是某一天:缓存过期。
此时大量用户同时访问:
100 万请求
↓
Redis全部未命中
↓
同时查询MySQL
流程:
请求1
|
请求2
|
请求3
|
请求N
↓
Redis缓存失效
↓
同时访问数据库
这就是缓存击穿。
为什么缓存击穿比普通缓存失效更危险?
因为它通常发生在:
热点数据。
普通数据:一天访问几十次,缓存失效影响不大。
热点数据:一分钟几十万次
一旦失效:大量请求同时访问数据库。数据库瞬间承受缓存原本承担的压力。
所以缓存击穿的核心问题:不是数据不存在。而是大量请求同时发现缓存不存在。
如何解决缓存击穿?
方案一:互斥锁
最常见方案。第一个请求发现缓存不存在:
请求1
↓
Redis没有数据
↓
获取锁
↓
查询数据库
↓
写入缓存
其他请求:
请求2
↓
发现锁存在
↓
等待
最终:一个请求查询数据库,其他请求共享结果
流程:
请求1
|
↓
获取锁
|
↓
查询数据库
|
↓
写入Redis
请求2/3/4
|
↓
等待结果
这样可以避免数据库被瞬间打爆。
方案二:热点数据永不过期
对于一些极端热点数据:
例如:
首页配置
热门商品
活动信息
可以不设置过期时间。而是在后台主动更新。
例如:
定时任务
↓
刷新热点数据
↓
更新Redis
这样不会出现突然失效。
什么是缓存雪崩?缓存雪崩比缓存击穿更严重。
它指的是:
大量缓存数据在同一时间失效。
例如:
系统启动时:
10万个商品
↓
同时写入Redis
↓
过期时间都是30分钟
30分钟之后大量Key同时过期
结果:
请求
↓
Redis大量未命中
↓
MySQL压力暴涨
整个系统像发生雪崩一样。
所以叫:
缓存雪崩。为什么会出现缓存雪崩?
通常有两个原因。
原因一:大量 Key 同时过期
例如:
商品缓存
过期时间30分钟
如果所有数据都是:expire(key, 1800)
那么它们可能同时失效。
原因二:Redis 故障
除了 Key 过期:Redis 本身也可能出现问题。
例如:
Redis宕机
↓
所有请求访问数据库
↓
数据库压力暴涨
这也是缓存雪崩的一种。
如何解决缓存雪崩?
方案一:过期时间增加随机值
例如:
不要30分钟
而是30分钟 + 随机时间
例如:
30分钟
0~10分钟随机值
这样:
商品1
30分钟过期
商品2
35分钟过期
商品3
38分钟过期
避免大量 Key 同时失效。
方案二:缓存预热
系统启动之前提前加载热点数据。
例如:
服务启动
↓
加载热门商品
↓
写入Redis
↓
用户访问避免系统刚上线时大量请求打数据库。
方案三:增加降级机制
当 Redis 或数据库压力过高:系统需要保护自己。
例如:
Redis异常
↓
返回默认数据
↓
限制请求
避免整个服务不可用。
缓存穿透、击穿、雪崩有什么区别?
很多人容易混淆。
可以这样理解:
问题 原因 影响
缓存穿透 数据不存在 请求绕过缓存访问数据库
缓存击穿 一个热点 Key 失效 大量请求访问同一个数据
缓存雪崩 大量 Key 同时失效 大量请求冲击数据库
简单记:
穿透:没有这个数据
击穿:一个热点数据没了
雪崩:大量数据一起没了
Redis 为什么一定会遇到这些问题?
因为缓存系统本质上是在做一件事情:
用一个高速但不完全可靠的存储,保护一个慢速但可靠的存储。
Redis 越成功:访问量越大。缓存承担的压力越大。那么当缓存失效时,产生的影响也越大。所以缓存问题不是 Redis 的缺陷。而是所有缓存系统都会面对的问题。
真正优秀的缓存设计,不是保证缓存永远有效。
而是在缓存失效时:
不让无效请求击穿系统
不让热点数据拖垮数据库
不让大量失效造成雪崩
这也是 Redis 从一个简单的缓存工具,变成大型互联网系统基础组件的重要原因。
上一篇:《Redis 为什么不会每次修改都写磁盘?》
下一篇:《Redis 为什么不能当数据库?》