前言
我们引入 Redis 的初衷,是让它作为“防弹衣”挡在 MySQL 数据库前面,拦截大部分的读取请求。
但在高并发场景下,如果这层防弹衣破了(缓存失效)或者没防住(查询不存在的数据),海量的流量就会瞬间打到脆弱的数据库上,导致数据库 CPU 飙升甚至宕机。这就是我们常说的缓存穿透、缓存击穿、缓存雪崩。
这三个词听起来很像,但成因和解决方法完全不同。今天一文帮你彻底厘清。
1. 缓存穿透 (Cache Penetration)
比喻: 你的防弹衣上全是洞,子弹直接穿过去打中身体。
- 现象:
用户想要查询一个数据,Redis 里没有,数据库里也没有。
结果就是:每次请求都会穿过 Redis,去查一遍数据库,然后返回空。当有恶意攻击者利用脚本疯狂查询不存在的 ID(如 id=-1)时,数据库会被瞬间打垮。 - 核心原因: 查询了根本不存在的数据。
- 解决方案:
- 方案 A:缓存空值 (Cache Null Value)当数据库没查到时,我们也往 Redis 里存一个 null 或空字符串,并设置一个较短的过期时间(比如 30秒)。
- 优点: 实现简单。
- 缺点: 消耗内存(如果攻击者随机生成无数个 ID)。
- 方案 B:布隆过滤器 (Bloom Filter) —— 推荐
在访问 Redis 之前,先加一道屏障。布隆过滤器能通过极小的内存判断“某样东西一定不存在或可能存在”。
如果布隆过滤器判定 ID 不存在,直接拦截,根本不让请求碰 Redis 和数据库。
2. 缓存击穿 (Cache Breakdown)
比喻: 防弹衣上某一个点(比如心脏部位)破了个洞,狙击手盯着这个点打。
- 现象:
某一个热点 Key(比如微博热搜第一名的 ID),在不停地扛着大并发。
突然,这个 Key 的过期时间到了。
就在缓存失效的这一瞬间,成千上万的请求同时涌入,发现 Redis 没数据,全部冲向数据库。数据库瞬间被单一的热点数据压垮。 - 核心原因: 单一热点 Key 过期 + 高并发。
- 解决方案:
- 方案 A:互斥锁 (Mutex Lock)当发现 Redis 没数据时,不要所有人一窝蜂去查数据库。大家排队,只让第一个人去查,查到了写回 Redis,后面的人直接读 Redis。
- 代码逻辑: 使用
SETNX获取锁。
- 方案 B:逻辑过期 (Logical Expiration)
物理上不设置过期时间(永不过期),但在 Value 内部存一个时间戳。查询时检测到时间戳过期了,异步开启一个线程去更新数据,当前请求先返回旧数据。
3. 缓存雪崩 (Cache Avalanche)
比喻: 雪崩发生了,整片山坡的雪同时塌下来。
- 现象:
Redis 中大量的 Key 在同一时刻集体过期。
或者,Redis 节点直接宕机了。
结果是:原本被 Redis 拦截的海量请求,瞬间全部转移到数据库,导致数据库像被雪崩淹没一样挂掉。 - 核心原因: 大量 Key 同时过期 或 Redis 故障。
- 解决方案:
- 方案 A:随机过期时间 (Random TTL)在设置过期时间时,不要都设为 1小时。而是在 1小时的基础上,加上一个随机数(比如 1-10 分钟)。这样 Key 就会分散过期,不会扎堆。
- 公式:
Expire_Time = Base_Time + Random(Value)
- 方案 B:高可用架构
部署 Redis 哨兵(Sentinel)或 集群(Cluster),防止单点故障导致全盘皆输。 - 方案 C:限流与降级
在网关层做限流,或者当数据库压力过大时,直接开启降级模式(返回默认值或“系统繁忙”),保住数据库一命。
总结:一张表看懂区别
| 问题 | 关键特征 | 根本原因 | 核心解决方案 |
| 缓存穿透 | 查不存在的数据 | 数据源和缓存都没有 | 布隆过滤器、缓存空对象 |
| 缓存击穿 | 单个热点 Key 失效 | 局部并发过大 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 Key 同时失效 | 整体并发压力转移 | 随机过期时间、Redis高可用 |
口诀记忆:
- 穿透:查无此人。
- 击穿:盯着一点打。
- 雪崩:全线崩盘。