Redis缓存穿透、击穿、雪崩区别和解决
前言
用Redis做缓存,经常听到三个词:缓存穿透、缓存击穿、缓存雪崩。很多人搞不清区别,本文一次讲明白,附解决方法。
一、三个概念对比
| 问题 | 是什么 | 谁在访问 | 影响 |
|---|---|---|---|
| 缓存穿透 | 查一个不存在的数据,缓存没有,每次都去查数据库 | 恶意攻击/大量无效请求 | 数据库压力大 |
| 缓存击穿 | 一个热点key突然过期,大量请求同时打过去 | 正常用户 | 数据库瞬间压力大 |
| 缓存雪崩 | 大量key同时过期,或者Redis挂了 | 正常用户 | 数据库直接崩 |
二、缓存穿透
是什么
黑客故意查一个数据库里根本不存在的数据,缓存里也没有,每次请求都打到数据库。
例子: 查用户ID=-1,数据库里没有,缓存里也没有,每次都查数据库。
危害
数据库被大量无效请求打挂。
解决方法
方法1:缓存空值
数据库查不到,也往缓存里写一个空值,设个短过期时间。
// 先查缓存
$user = $redis->get("user:$id");
if ($user === false) {
// 查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
if (!$user) {
// 查不到也缓存,设60秒过期
$redis->setex("user:$id", 60, "null");
} else {
$redis->setex("user:$id", 3600, $user);
}
}
方法2:布隆过滤器
在缓存前面加一层布隆过滤器,快速判断这个ID存在不存在。
不存在的直接返回,不用查数据库。
方法3:接口校验
在接口层就做参数校验,比如用户ID必须大于0,不合法的直接拒绝。
三、缓存击穿
是什么
一个热点key(比如首页热点新闻),突然过期了,这时候刚好有大量用户同时访问,所有请求都打到数据库。
和穿透的区别: 这个key本来是存在的,只是过期了。
危害
数据库瞬间压力暴增。
解决方法
方法1:互斥锁
只让一个线程去查数据库,其他线程等一下。
$lockKey = "lock:user:$id";
// 尝试加锁
if ($redis->setnx($lockKey, 1)) {
// 加锁成功,查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
$redis->setex("user:$id", 3600, $user);
$redis->del($lockKey);
} else {
// 加锁失败,等一下再读缓存
sleep(100);
$user = $redis->get("user:$id");
}
方法2:热点key不过期
热点数据不设过期时间,后台定时更新。
方法3:提前预热
在热点数据快过期前,主动后台更新缓存。
四、缓存雪崩
是什么
大量key在同一时间过期,或者Redis整个挂了,所有请求都打到数据库。
和击穿的区别:
- 击穿:一个key过期
- 雪崩:大量key同时过期
危害
数据库直接被打挂,整个服务不可用。
解决方法
方法1:过期时间加随机值
不要所有key都设3600秒过期,加个随机数错开。
// 基础3600秒,加0-600秒随机
$expire = 3600 + rand(0, 600);
$redis->setex("user:$id", $expire, $data);
方法2:Redis集群
Redis挂了还有备用的,不要单点。
方法3:服务降级
数据库扛不住的时候,直接返回默认数据或者报错,不要让请求都打过去。
方法4:热点数据永不过期
核心数据不设过期,后台异步更新。
五、总结对比
| 问题 | 原因 | 解决 |
|---|---|---|
| 缓存穿透 | 查不存在的数据 | 缓存空值 + 布隆过滤器 |
| 缓存击穿 | 热点key过期 | 互斥锁 + 热点永不过期 |
| 缓存雪崩 | 大量key同时过期 | 过期时间加随机 + Redis集群 |
六、常见坑
坑1:把穿透和击穿搞混
- 穿透:数据本来就不存在
- 击穿:数据存在,但缓存过期了
坑2:缓存空值不设过期时间
如果不设过期时间,以后这个数据真的有了,缓存还是空的。
解决: 空值也要设过期时间(比如60秒)。
坑3:互斥锁死锁
加了锁没释放,其他线程一直等。
解决: 锁也要设过期时间。
坑4:随机值加太小
所有key还是差不多同时过期。
建议: 随机范围至少几百秒。
总结
记住:
- 穿透:查不存在的数据 → 缓存空值 + 布隆过滤器
- 击穿:热点key过期 → 互斥锁 + 热点永不过期
- 雪崩:大量key同时过期 → 过期时间加随机 + Redis集群
最简单的防雪崩:
$expire = 3600 + rand(0, 600);
$redis->setex($key, $expire, $data);
遇到问题加QQ23979811 协助处理
Redis 缓存穿透解决
Redis 缓存穿透解决
缓存穿透是常见问题,恶意请求能把数据库打挂。
1. 什么是缓存穿透
现象: 请求的数据在缓存和数据库中都不存在,每次请求都直接打到数据库。
危害:
- 恶意攻击时数据库压力大
- 正常请求被拖慢
- 数据库CPU飙升
2. 为什么会穿透
- 查询一个不存在的ID
- 缓存没有命中
- 数据库也查不到
- 请求直接穿透到数据库
3. 解决方案一:缓存空值
思路: 即使数据库查不到,也在Redis存一个空值,设置短过期时间。
function getUser($id) {
$key = "user:$id";
$user = $redis->get($key);
if ($user !== false) {
// 空值标记
if ($user === 'null') {
return null;
}
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id = $id");
if ($user) {
// 存在就缓存1小时
$redis->setex($key, 3600, json_encode($user));
} else {
// 不存在也缓存,5分钟过期
$redis->setex($key, 300, 'null');
}
return $user;
}
优点: 简单有效
缺点: 占内存,可能被恶意key刷爆
4. 解决方案二:布隆过滤器
思路: 在缓存前面加一层布隆过滤器,不存在的key直接拦截。
原理:
- 所有存在的数据先存到布隆过滤器
- 请求先过布隆过滤器
- 不存在直接返回,不查缓存和数据库
PHP 示例(用 Redis 的布隆过滤器模块):
# 加载 RedisBloom 模块
redis-cli BF.ADD user:1
redis-cli BF.EXISTS user:1
function getUser($id) {
$key = "user:$id";
// 先过布隆过滤器
if (!$redis->rawCommand('BF.EXISTS', 'user_filter', $id)) {
return null; // 一定不存在
}
// 布隆说可能存在,再查缓存
$user = $redis->get($key);
if ($user) {
return json_decode($user, true);
}
// 查数据库...
}
优点: 内存占用小,效率高
缺点: 有误判率,实现复杂
5. 解决方案三:参数校验
思路: 从源头过滤非法请求。
function getUser($id) {
// 参数校验
if ($id <= 0 || !is_numeric($id)) {
return null;
}
// 正常查询逻辑...
}
适用: ID类查询,不合法直接拦截
6. 三种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 占内存 | 数据量不大 |
| 布隆过滤器 | 高效 | 误判,复杂 | 数据量大 |
| 参数校验 | 最直接 | 只防非法 | ID类请求 |
7. 生产建议
1)组合使用
- 先做参数校验
- 再缓存空值
- 大流量用布隆过滤器
2)空值过期时间要短
- 5分钟到10分钟
- 不要太长
3)监控异常请求
- 发现大量404要警惕
- 及时加防护
4)接口限流
- 用 Nginx 或网关限流
- 防止恶意刷
8. 缓存雪崩和击穿
缓存穿透: 查不存在的数据
缓存击穿: 热点key过期,大量请求打数据库
缓存雪崩: 大量key同时过期
区分清楚: 三个问题现象类似但原因不同,解决方案也不同。
总结
Redis 缓存穿透解决要点:
- 查不存在的数据穿透到数据库
- 缓存空值是最简单方案
- 布隆过滤器适合大数据量
- 参数校验从源头拦截
- 空值过期时间要短
- 生产建议组合使用
- 注意和雪崩、击穿区分开
- 监控异常请求
- 接口加限流
缓存穿透是面试高频题,也是生产常见问题,一定要掌握。
Redis 缓存穿透解决
Redis 缓存穿透解决
缓存穿透是常见问题,大量请求查询不存在的数据,直接打到数据库。这篇讲怎么解决。
1. 什么是缓存穿透
用户查询一个不存在的数据,Redis 里没有,每次都去查数据库。恶意攻击的话,数据库扛不住。
2. 解决方案一:缓存空值
查询数据库也没结果,就缓存一个空值,设置短过期时间。
function getUser($id) {
$key = "user:$id";
// 先查缓存
$user = $redis->get($key);
if ($user !== false) {
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id = $id");
if ($user) {
// 缓存有数据的结果
$redis->setex($key, 3600, json_encode($user));
} else {
// 缓存空值,短时间过期
$redis->setex($key, 60, '');
}
return $user;
}
优点: 简单粗暴
缺点: 浪费内存,可能导致不一致
3. 解决方案二:布隆过滤器
在缓存前面加一层布隆过滤器,不存在的 key 直接拦截。
// 使用 RedisBloom 模块
// 添加到布隆过滤器
$redis->rawCommand("BF.ADD", "user_filter", "1001");
// 查询是否存在
$exists = $redis->rawCommand("BF.EXISTS", "user_filter", "1001");
if (!$exists) {
return null; // 直接返回,不查数据库
}
原理: 用二进制数组和多个哈希函数,判断一个 key 一定不存在或者可能存在。
优点: 内存占用小,效率高
缺点: 有一定误判率,不支持删除
4. 两种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 浪费内存 | 数据量小 |
| 布隆过滤器 | 高效省内存 | 有误差 | 数据量大 |
5. 其他注意事项
- 接口增加参数校验,不合法的 id 直接拒绝
- 增加接口访问限流,防止恶意攻击
- 黑名单机制,频繁查不存在的 IP 封掉
总结
缓存穿透解决思路:
- 缓存空值: 简单,适合小数据量
- 布隆过滤器: 高效,适合大数据量
- 加限流和校验: 从入口防护
生产环境推荐布隆过滤器 + 缓存空值组合使用。
Redis 缓存穿透解决
Redis 缓存穿透解决方案
缓存穿透是指查询一个数据库里根本不存在的数据,因为缓存里也没有,所以每次请求都打到数据库。如果有人恶意攻击,数据库可能被打挂。
什么是缓存穿透
正常流程:先查缓存,缓存有就直接返回,没有再查数据库,然后写入缓存。
如果查询的数据数据库里也没有,那缓存永远都不会命中,每次请求都会查数据库,这就是缓存穿透。
解决方案一:缓存空值
最简单的办法,数据库查不到也往缓存里写一个空值,设个短过期时间。
$data = $redis->get($key);
if ($data === null) {
$data = $db->query($sql);
if ($data === null) {
// 缓存空值,60秒过期
$redis->setex($key, 60, '');
} else {
$redis->setex($key, 3600, $data);
}
}
优点:简单直接
缺点:会占用更多缓存空间,大量空值缓存会浪费内存
解决方案二:布隆过滤器
在缓存前面加一层布隆过滤器,先判断数据是否存在,不存在直接返回。
布隆过滤器的原理是用一个大的位数组和多个哈希函数,把存在的数据标记为1。查询时如果所有哈希位置都是1,说明可能存在;只要有一个位置是0,说明一定不存在。
优点:内存占用小,效率高
缺点:有一定误判率(说存在可能不存在,说不存在一定不存在)
两种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 浪费内存 | 数据量小,穿透不严重 |
| 布隆过滤器 | 内存效率高 | 有误差 | 数据量大,穿透严重 |
总结
缓存穿透是常见的缓存问题,小项目用缓存空值就够了,大项目数据量大就上布隆过滤器。两种方案都能解决问题,根据实际场景选就行。