Redis 缓存穿透、击穿与雪崩排查实战:从命中率判断到互斥重建与过期抖动
背景
线上用 Redis 做缓存的服务,最怕三种情况:缓存穿透、缓存击穿和缓存雪崩。它们的共同点是请求绕过缓存直接打到数据库,区别在于触发原因不同。本文按“现象、原因、处理”的顺序整理一套我自己排查时用的做法,示例以 Java 风格的伪代码和 redis-cli 命令为主,思路适用于任何语言。
一、先判断是哪一种
先看监控,不要急着改代码。可以用下面的命令观察命中率:
redis-cli info stats | grep -E "keyspace_hits|keyspace_misses"
命中率 = hits / (hits + misses)。三种问题的特征不同:
- 穿透:命中率长期偏低,数据库收到大量“查不到结果”的请求,参数多半是不存在的 ID。
- 击穿:命中率整体正常,但某一个热点 key 过期的瞬间,数据库并发查询突然飙高,随后恢复。
- 雪崩:大量 key 在同一时间段集中过期,或者 Redis 实例不可用,数据库压力持续升高。
二、缓存穿透:查询的数据根本不存在
典型场景是接口按 ID 查询,攻击者或爬虫不断传入不存在的 ID,缓存永远不命中,请求每次都落库。常用的处理方式有三个。
1. 参数校验。在接口层先拦掉明显非法的参数,比如负数 ID、超长字符串,这是成本最低的一层。
2. 缓存空值。查询数据库没有结果时,也写一个空值进缓存,并设置较短的过期时间:
value = redis.get(key)
if value == NULL_MARK: return None
if value is None:
row = db.query(id)
if row is None:
redis.setex(key, 60, NULL_MARK) # 空值只缓存 60 秒
return None
redis.setex(key, 3600 + random(0, 300), row)
return row
return value
空值的过期时间不要太长,否则数据后来真的新增了,用户会在一段时间内一直查不到。
3. 布隆过滤器。如果合法 ID 的集合比较固定,可以在应用启动时把全部 ID 加载进布隆过滤器,查询前先判断。它的特点是“说不存在就一定不存在”,存在少量误判但不会漏判。
三、缓存击穿:热点 key 过期的一瞬间
一个访问量很高的 key 到期后,在它被重新写入之前,所有并发请求都会发现缓存为空并同时去查数据库。解决思路是让同一时刻只有一个请求去重建缓存:
value = redis.get(key)
if value is None:
if redis.set(lock_key, token, nx=True, ex=10):
try:
value = db.query(id)
redis.setex(key, 3600, value)
finally:
if redis.get(lock_key) == token:
redis.delete(lock_key)
else:
sleep(0.05)
return get(id) # 稍后重试,读取别人写好的缓存
注意三点:锁一定要设过期时间,防止持有者异常退出后死锁;释放锁前要确认还是自己持有,否则会误删别人的锁,严格做法是用 Lua 脚本把判断和删除合成一步;重试要有次数上限,不要无限递归。对于极热的数据,也可以不设过期时间,改成后台定时任务主动刷新。
四、缓存雪崩:同时过期或实例故障
如果批量导入缓存时都设成了“3600 秒”,一小时后它们会同时失效。最简单的办法是在基础过期时间上加随机值,前面示例里的 random(0, 300) 就是这个作用。
实例故障引起的雪崩则要靠架构兜底:
- 使用主备或集群版本的 Redis 实例,开启自动故障切换,避免单点。
- 应用侧做限流和降级,数据库承受不了时直接返回兜底数据或提示稍后重试,而不是让请求排队把数据库压垮。
- 给数据库连接池设置上限和超时,宁可部分请求失败,也不要全部超时。
五、上线前的检查清单
- 所有写缓存的地方都带过期时间,并且带随机抖动。
- 查询不存在数据的路径有空值缓存或布隆过滤器。
- 热点 key 有互斥重建或后台刷新机制。
- 监控里配置了命中率、慢查询和数据库 QPS 的告警,告警阈值要结合业务平时的基线来定。
- 压测时专门模拟一次“清空缓存后立刻放量”的场景,看数据库和连接池的表现。
总结
穿透看参数、击穿看热点、雪崩看时间分布和高可用。排查时先用命中率和数据库 QPS 判断类型,再对症处理;日常开发时把过期抖动、空值缓存和互斥重建当成默认写法,大部分缓存事故都能提前避免。以上做法都是我在日常开发里踩坑后总结的,参数需要按自己业务的数据量和并发情况调整。
- 点赞
- 收藏
- 关注作者
评论(0)