Java高并发场景下的缓存穿透问题定位与解决方案
最近在负责公司电商系统的商品详情接口优化时,遇到一个让人头大的问题:线上高并发流量下,Redis缓存命中率突然下降,数据库压力暴增,接口响应时间飙升,甚至一度影响了核心交易链路。经过排查,发现竟然是“缓存穿透”在作怪。
这篇文章就来和大家聊聊我在真实项目中遇到缓存穿透的定位过程、解决思路,以及如何系统性地设计一个可复用的解决方案,避免大家踩同样的坑。
一、场景描述
我们的商品详情接口,每天有百万级别的访问量。正常情况下,商品数据会先查Redis缓存,没命中再查数据库,然后回填缓存。平时接口响应都很快,但某天突然报警:数据库QPS暴涨,CPU飙高,Redis命中率掉到50%以下。
一开始我们以为是热点商品流量太大,后来发现有大量请求是查找根本不存在的商品ID,比如一些乱七八糟的ID、爬虫批量请求、或者前端误传参数。
这些不存在的ID,Redis查不到,数据库也查不到,每次都要走完整的查询流程,数据库压力直接翻倍,缓存完全失效。
二、问题定位过程
说实话,刚开始我们团队也有点懵,毕竟Redis已经做了缓存,为什么还会有这么大压力?于是分几步排查:
1. 日志分析
通过接口日志和监控,发现大量请求的商品ID都不在正常商品范围内,甚至有些是负数、超长字符串,明显不是正常业务流量。
2. Redis命中率监控
Redis的命中率从90%掉到50%,说明很多请求根本没有缓存,直接落到数据库。
3. 数据库慢查询
数据库慢查询日志里,几乎全是“select * from goods where id = ?”且id根本不存在,导致大量无效查询。
4. 风控与爬虫排查
进一步分析发现,部分流量来自外部爬虫和恶意批量请求,专门在接口上撞不存在的ID,企图抓全量数据。
到这里基本确认,是“缓存穿透”问题:大量请求查找不存在的数据,缓存没命中,数据库也查不到,每次都要落库,造成压力。
三、缓存穿透原理简析
简单说,缓存穿透就是:请求的数据无论缓存还是数据库都没有,每次都要查数据库,缓存完全失效。
常见场景有:
- 用户请求不存在的ID(参数错误、爬虫、恶意攻击)
- 业务逻辑漏洞(前端未校验ID、批量拉取数据)
- 缓存只存有数据的key,没存空数据
在高并发场景下,穿透会导致数据库被“打穿”,严重时甚至雪崩。
四、系统性解决方案
针对这类问题,我们设计了一套系统性的防穿透方案,分为参数校验、缓存空值、布隆过滤器、限流防护等环节。
1. 参数校验与前置过滤
第一步就是在接口层做基础参数校验,比如商品ID必须为正整数、长度不能超限、前端传参要做基本校验。这样能拦掉一部分无效请求。
if (id <= 0 || String.valueOf(id).length() > 10) {throw new IllegalArgumentException("商品ID非法");
}
2. 缓存空值(Null Cache)
这是最直接有效的方案:当请求的ID不存在时,也把结果(比如null或空对象)写入缓存,设置较短的过期时间(比如2分钟),后续同样的请求就不会落到数据库。
Goods goods = redisTemplate.opsForValue().get("goods:" + id);
if (goods != null) {return goods;
}
// 查数据库
goods = goodsMapper.selectById(id);
if (goods == null) {// 空对象写缓存,防止穿透redisTemplate.opsForValue().set("goods:" + id, NULL_GOODS, 2, TimeUnit.MINUTES);return NULL_GOODS;
}
redisTemplate.opsForValue().set("goods:" + id, goods, 30, TimeUnit.MINUTES);
return goods;
这样即使有人反复请求不存在的ID,也只会查一次数据库,后续都走缓存。
3. 布隆过滤器(Bloom Filter)
对于大规模ID空间,空值缓存可能太多,可以引入布隆过滤器。在应用启动时,把所有合法商品ID加入布隆过滤器,请求时先判断ID是否可能存在,不存在直接返回错误。
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), expectedInsertions);
filter.put(10001L); // 初始化时加入所有商品IDif (!filter.mightContain(id)) {// 直接返回不存在,无需查库return NULL_GOODS;
}
布隆过滤器空间占用小,查询快,但有极小概率误判(即可能存在但实际不存在),一般业务可接受。
4. 接口限流与风控
对异常流量(比如同IP高频请求、爬虫批量撞库),可以加限流和黑名单机制。比如用Guava RateLimiter或Redis计数实现接口限流,超过阈值直接拒绝。
RateLimiter limiter = RateLimiter.create(100); // 每秒100次
if (!limiter.tryAcquire()) {throw new RuntimeException("接口限流");
}
对于恶意IP可以直接封禁,或者引入验证码、滑块验证等人机识别。
5. 监控与报警
最后,搭建监控体系,实时监控Redis命中率、数据库QPS、接口异常流量,一旦出现异常及时报警,快速定位问题。
五、优化效果与复盘
经过上述方案上线后,Redis命中率恢复到95%以上,数据库压力大幅下降,接口响应时间稳定在200ms以内。空值缓存和布隆过滤器配合,基本杜绝了穿透问题。限流和风控机制也让爬虫和恶意流量无处遁形。
最关键的是,整个方案代码量不大,易于复用,后续新接口只要加空值缓存和布隆过滤器即可,极大降低了维护成本。
六、常见坑与经验总结
- 只缓存有数据的key,容易被穿透。一定要缓存null或空对象,哪怕只缓存几分钟,也能挡住大部分无效请求。
- 布隆过滤器不是100%准确,有极小概率误判,业务允许的情况下可以用,否则还是要查库兜底。
- 限流要结合业务场景灵活调整,不能一刀切,避免影响正常用户体验。
- 监控和报警非常重要,及时发现异常流量,快速定位问题,防止雪崩。
- 参数校验是第一道防线,前端和接口都要做,能拦掉一半的无效请求。
七、后续优化方向
虽然缓存穿透问题解决了,但还可以继续优化,比如:
- 动态更新布隆过滤器,支持商品上下架实时同步;
- 用分布式缓存方案(如Redisson)提升高并发下的缓存一致性;
- 针对热点数据做预加载和预热,提高首屏响应速度;
- 持续完善风控和流量识别,防止新型爬虫和攻击手段。
八、结语
缓存穿透问题在高并发Java后端场景下非常常见,尤其是电商、内容分发、金融等行业。只有系统性地设计参数校验、空值缓存、布隆过滤器、限流风控等组合方案,才能真正“降本增效”,让系统稳定高效运行。