你有没有遇到过这样的情况:明明加了 Redis,接口响应时间却越来越慢,监控里缓存命中率从 95% 一路掉到 60%,甚至更低?别急着怀疑 Redis 配置错了——很可能,是缓存穿透在悄悄捣鬼。
缓存穿透不是“穿墙”,是“查无此物”
缓存穿透,说白了就是:用户反复查一个根本不存在的数据。比如电商系统里,有人用脚本疯狂请求 /api/item?id=999999999,而这个 ID 在数据库里压根没建过商品。缓存里当然没有,于是每次请求都穿透到数据库,数据库查一圈返回 null,应用再把 null 写进缓存(如果逻辑没做拦截)或者干脆不写——下一次请求又来一遍。
命中率是怎么被拉低的?
缓存命中率 = 缓存命中的请求数 ÷ 总请求量。当大量无效 key 涌入,它们全都不在缓存中,自然全部未命中。哪怕你有 1000 个真实商品的请求,只要混进去 3000 个伪造的、不存在的 id 请求,命中率瞬间就掉到 25%。
更麻烦的是,这些请求还可能拖垮后端。数据库扛不住高频空查,连接池打满,慢查询堆积,连带真实请求也变慢——这时候你去看监控,会发现“缓存没少用,但好像没起啥作用”。
一个典型场景:用户注册页的手机号校验
假设注册接口要校验手机号是否已存在:
→ 前端传过来 phone=13800138000,缓存查无记录,查库也没,返回“可注册”,顺便把 phone:13800138000 → false 写进缓存(设 5 分钟过期);
→ 下一秒,黑产用工具扫号,发 10 万个随机手机号,比如 13800138001、13800138002……每个都是全新 key,缓存统统 miss,全打到数据库。
这时候你的缓存里塞满了“无效手机号 → false”的键值对,但它们几乎只用一次,还占内存、拖性能——命中率数字难看,实际收益为零。
怎么挡一挡?两个常用招数
1. 布隆过滤器(Bloom Filter)前置拦截
在请求到达缓存前,先过一遍布隆过滤器。它能快速告诉你:“这个 key 绝对不存在”或“可能存在”。虽然有极小误判率(说存在,其实没有),但绝不会漏判。把所有合法手机号哈希进过滤器,非法扫描号一来就被拦住,根本进不了缓存和数据库。
2. 空值缓存 + 随机过期时间
对确认不存在的数据,也往缓存里写个空值(比如 null 或字符串 "empty"),并设置较短过期时间(如 2 分钟)。同时加个随机偏移(比如 ±30 秒),避免大量空值同时过期引发雪崩:
String key = "user:phone:" + phone;
String cacheValue = redis.get(key);
if (cacheValue != null) {
return "empty".equals(cacheValue) ? null : cacheValue;
}
// 查库
User user = db.queryByPhone(phone);
if (user == null) {
// 写空值,过期时间 2 分钟 ± 30 秒
int expireSec = 120 + new Random().nextInt(60) - 30;
redis.setex(key, expireSec, "empty");
} else {
redis.setex(key, 3600, user.toJson());
}这两招不冲突,生产环境常一起上。布隆过滤器挡掉 99% 的恶意扫描,空值缓存兜底处理漏网之鱼。
缓存穿透本身不直接“破坏”已有缓存,但它靠制造海量无效查询,硬生生把命中率的分母撑大、分子拉低。就像往一锅浓汤里不停兑凉水——汤还在,只是越来越淡了。