Redis 缓存穿透、击穿与雪崩:原理与解决方案
# Redis 缓存穿透、击穿与雪崩:原理与解决方案
用 Redis 做缓存几乎是后端项目的标配,但缓存用不好,反而会在高并发时把数据库拖垮。缓存穿透、击穿、雪崩是最常被问到、也最容易踩坑的三个问题,这篇文章把它们捋清楚。
## 1. 缓存穿透:查询一个根本不存在的数据
**现象**:请求查询一个数据库里也不存在的 key,缓存里自然也没有,每次请求都要打到数据库上。如果有人恶意用不存在的 id 疯狂请求,数据库很容易被打垮。
**解决方案**:
- **缓存空值**:查询结果为空时,也把这个空结果缓存起来(设置一个较短的过期时间),避免同一个不存在的 key 反复穿透到数据库。
```python
value = redis_client.get(key)
if value is None:
value = query_from_db(key)
if value is None:
redis_client.setex(key, 60, '') # 缓存空值,60 秒过期
else:
redis_client.setex(key, 3600, value)
```
- **布隆过滤器**:提前把所有合法的 key 放进布隆过滤器,请求先经过过滤器判断,不在里面的 key 直接拒绝,连缓存都不用查。
## 2. 缓存击穿:一个热点 key 突然过期
**现象**:某个访问量极高的 key(比如首页热门文章)缓存过期的瞬间,大量并发请求同时穿透到数据库,短时间内造成数据库压力骤增。
**解决方案**:
- **加互斥锁**:缓存失效时只让一个请求去查数据库并重建缓存,其他请求等待或读旧值。
```python
lock_key = f'lock:{key}'
if redis_client.set(lock_key, '1', nx=True, ex=10):
try:
value = query_from_db(key)
redis_client.setex(key, 3600, value)
finally:
redis_client.delete(lock_key)
else:
time.sleep(0.05)
value = redis_client.get(key) # 重试读取
```
- **热点数据不过期**:对特别热门的 key,直接不设置过期时间,靠后台任务异步更新。
## 3. 缓存雪崩:大量 key 集中在同一时刻过期
**现象**:如果给一批 key 设置了相同的过期时间,到了那个时间点,缓存集体失效,请求瞬间全部涌向数据库。
**解决方案**:
- **过期时间加随机值**:避免大量 key 在同一时刻集中失效。
```python
import random
expire_time = 3600 + random.randint(0, 300) # 基础 1 小时 + 0~5 分钟随机抖动
redis_client.setex(key, expire_time, value)
```
- **多级缓存**:本地缓存(如内存缓存)+ Redis 缓存,即使 Redis 整体不可用,也能兜底一部分请求。
- **限流 + 降级**:数据库压力过大时,通过限流保护数据库,必要时返回降级数据(比如旧数据或默认值)。
## 小结
| 问题 | 触发场景 | 核心思路 |
| --- | --- | --- |
| 缓存穿透 | 查询不存在的数据 | 缓存空值 / 布隆过滤器 |
| 缓存击穿 | 单个热点 key 过期 | 互斥锁 / 热点不过期 |
| 缓存雪崩 | 大量 key 同时过期 | 过期时间加随机 / 多级缓存 |
这三个问题看着相似,但触发条件和解法都不一样,面试也经常放在一起问。理解清楚各自的场景,才能在实际项目里对症下药。
评论区