你有没有遇到过这种情况:明明加了 Redis,接口响应时间却越来越慢,监控里缓存命中率从 95% 一路掉到 60%,查日志又没报错?别急着怀疑配置或网络,很可能是缓存穿透在背后捣鬼。
缓存穿透不是“穿墙”,是“空枪打靶”
缓存穿透,说白了就是——用户查一个压根不存在的数据,比如请求 /user?id=999999999,而数据库里根本没这个 ID。缓存里当然也没有,于是每次请求都绕过缓存,直击数据库。一次两次没事,但要是被脚本批量刷(比如用随机 ID 狂扫),数据库瞬间变“热点”,缓存形同虚设。
这时候命中率自然往下掉:缓存里没值 → 不命中 → 去查库 → 再把空结果丢进去?不,很多人压根没存空值,下一次还是重来。等于缓存彻底“失能”,所有同类请求全走回源,命中率断崖下跌。
一个真实的小例子
某电商后台有个商品详情页,用 Redis 缓存 goods:12345。正常用户点进来的 ID 都是真实存在的,缓存稳稳撑住流量。可某天爬虫开始发 goods:1000000001、goods:1000000002……这些 ID 全是乱造的。Redis 查不到,DB 也查不到,后端没做空值缓存,也没布隆过滤器。结果一小时里,30 万次无效查询打穿数据库,缓存命中率从 92% 暴跌到 37%。
怎么拦住它?两个接地气的办法
① 空值缓存:查不到就存个占位符,比如 SET goods:1000000001 "null" EX 60,有效期设短点(60 秒够防误刷),既挡穿透,又不长期占内存。
② 布隆过滤器前置:启动时把所有合法商品 ID 过一遍布隆过滤器(Bloom Filter)。请求来了先 exists(goods:12345),返回 false 就直接拦截,连缓存都不碰。内存省、速度快,适合 ID 规则固定、总量可控的场景。
代码片段示意(伪代码):
if !bloomFilter.mightContain("goods:" + id) {
return "商品不存在"; // 直接返回,不查缓存也不查库
}
cacheValue = redis.get("goods:" + id);
if cacheValue != null {
return cacheValue;
} else {
dbValue = db.query("SELECT * FROM goods WHERE id = ?", id);
if dbValue == null {
redis.setex("goods:" + id, 60, "NULL"); // 存空值
} else {
redis.setex("goods:" + id, 3600, dbValue);
}
return dbValue;
}缓存穿透本身不直接改写命中率统计逻辑,但它让大量请求“注定不命中”,久而久之,命中率数字就真实地、难看地掉了下来——这不是指标出错,是系统正在发出求救信号。