Redis缓存穿透、击穿、雪崩的区别与对应解决方案
在使用 Redis 缓存架构时,系统通常遵循“先查缓存,缓存未命中则查数据库,回写缓存”的逻辑。然而,在高并发场景下,由于设计不当或异常流量,这种架构会出现三种严重的异常情况:缓存穿透、缓存击穿和缓存雪崩。这三种情况虽然都会导致数据库压力剧增,但其成因和表现形式截然不同。精准识别问题并实施对应的解决方案,是保障系统稳定性的关键。
一、缓存穿透
核心问题:查询一个根本不存在的数据。
由于缓存中没有该数据(通常是因为数据从未写入,或者已被主动清理),请求会直接穿透缓存层,直达数据库。如果此时遭受恶意攻击,例如大量请求查询 ID 为 -1 或不存在的 UUID,数据库会因为频繁处理无效查询而崩溃。
解决方案 1:缓存空对象
当数据库查询结果为空时,不要直接返回空,而是将这个“空结果”缓存起来。
- 接收查询请求,例如
product_id = 99999。 - 查询 Redis 缓存,发现未命中。
- 查询 数据库,确认数据不存在。
- 写入 Redis,将 key 对应的 value 设置为空值(如
NULL、""或特定占位符)。 - 设置 较短的过期时间(例如 30 秒到 5 分钟),防止缓存占用过多内存。
代码示例:
def get_product(product_id):
# 1. 查询缓存
value = redis.get(f"product:{product_id}")
if value is not None:
# 即使是空字符串,也直接返回,防止穿透到 DB
return None if value == "" else deserialize(value)
# 2. 查询数据库
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
# 3. 判断是否存在
if product:
redis.setex(f"product:{product_id}", 3600, serialize(product))
return product
else:
# 核心步骤:缓存空值
redis.setex(f"product:{product_id}", 60, "")
return None
解决方案 2:布隆过滤器
在访问缓存和数据库之前,先通过一个极其快速的算法判断数据是否可能存在。
- 初始化布隆过滤器,将所有合法的数据库主键 ID 加载到过滤器中。
- 接收查询请求。
- 判断布隆过滤器:
- 如果判断为不存在,直接返回空,拦截请求,绝不让其到达数据库。
- 如果判断为可能存在,才去查询 Redis 和数据库。
二、缓存击穿
核心问题:某个极度热点的 Key(如秒杀商品、微博热搜)突然过期,且此时有海量并发请求。
与“穿透”不同,“击穿”对应的数据是真实存在的。仅仅是那一瞬间,缓存失效了。成千上万的请求同时发现缓存没货,于是同时涌向数据库,瞬间将其打垮。
解决方案 1:互斥锁
只允许一个线程去查询数据库并重建缓存,其他线程必须等待。
- 发现缓存未命中。
- 尝试获取分布式锁(如 Redis 的
SETNX)。 - 判断获取结果:
- 获取成功:去数据库查询数据,写入缓存,释放锁。
- 获取失败:线程休眠一小段时间(如 50ms),然后重试查询缓存。
代码示例:
def get_product(product_id):
value = redis.get(f"product:{product_id}")
if value is not None:
return deserialize(value)
# 核心步骤:尝试加锁
lock_key = f"lock:product:{product_id}"
# setnx: 只有 key 不存在时才设置
is_locked = redis.setnx(lock_key, 1)
if is_locked:
# 加锁成功,设置过期时间防止死锁
redis.expire(lock_key, 10)
try:
product = db.query("SELECT * FROM products WHERE id = %s", product_id)
redis.setex(f"product:{product_id}", 3600, serialize(product))
return product
finally:
# 释放锁
redis.del(lock_key)
else:
# 加锁失败,等待并重试
time.sleep(0.05)
return get_product(product_id) # 递归重试
解决方案 2:逻辑过期
从物理上取消 Key 的自动过期时间,将过期时间存储在 Value 内部。
- 设置热点 Key 的 TTL 为
-1(永不过期)。 - 结构化存储 Value,包含
data(真实数据)和expire_time(逻辑过期时间戳)。 - 接收请求:
- 获取数据。
- 判断
current_time > expire_time:- 如果未过期,直接返回数据。
- 如果已过期,开启一个异步后台线程去更新数据库和缓存。
- 立即返回旧数据给用户(保证用户体验,不阻塞)。
三、缓存雪崩
核心问题:大量 Key 在同一时间集中过期,或者 Redis 缓存服务器宕机。
由于大量 Key 同时失效,原本落在缓存上的海量请求瞬间全部转发到数据库。这比“击穿”更严重,因为“击穿”通常只针对一个热点 Key,而“雪崩”是成千上万个 Key 同时失效。
解决方案 1:随机过期时间
避免批量设置的 Key 拥有相同的生命周期。
- 计算基础过期时间(例如 1 小时)。
- 追加一个随机值(例如 0 到 300 秒之间的随机数)。
- 设置 Key 的过期时间为
基础时间 + 随机值。
公式示例:
$$ T_{final} = T_{base} + \text{Random}(0, 300) $$
代码示例:
import random
base_expire = 3600 # 1小时
random_expire = random.randint(0, 300)
redis.setex(f"product:{product_id}", base_expire + random_expire, serialize(product))
解决方案 2:缓存高可用
防止 Redis 服务器单点故障。
- 搭建 Redis 哨兵模式或 Redis 集群。
- 配置主从复制,当主节点宕机时,哨兵自动选举新的主节点。
- 确保服务对 Redis 故障的降级处理(如限流熔断),即使 Redis 全部挂掉,服务也不应立刻全部崩溃,而是进行限流访问数据库。
四、核心区别对比表
为了方便快速诊断问题,请参考下表对比三种异常的核心特征。
| 异常类型 | 核心特征 | 数据库中的数据 | 并发量级 | 典型场景 |
|---|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 不存在 | 可大可小 | 恶意攻击、复杂查询误操作 |
| 缓存击穿 | 热点 Key 过期 | 存在 | 极高 (海量) | 秒杀、突发新闻、热门商品 |
| 缓存雪崩 | 大量 Key 同时失效 | 存在 | 极高 (海量) | 系统重启后批量加载、凌晨整点失效 |
五、排查与诊断步骤
当系统数据库 CPU 飙升,响应变慢时,按以下步骤进行排查。
- 查看 Redis 监控面板,观察
Hits(命中率)和QPS。 - 分析慢查询日志,确定是哪些 SQL 语句在频繁执行。
- 检查日志中的查询 Key:
- 如果查询了大量 ID 看起来像乱码或负数 -> 怀疑是缓存穿透。
- 如果查询集中在某一个特定的合法 ID,且该 Key 刚好过期 -> 怀疑是缓存击穿。
- 如果查询涉及的 ID 覆盖面极广,且时间点集中在整点或系统重启后 -> 怀疑是缓存雪崩。
- 实施上述对应的临时修复方案(如快速回滚代码、重启 Redis、或加黑名单限流)。

暂无评论,快来抢沙发吧!