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必须为正整数、长度不能超限、前端传参要做基本校验。这样能拦掉一部分无效请求。

java 复制代码
if (id <= 0 || String.valueOf(id).length() > 10) {
    throw new IllegalArgumentException("商品ID非法");
}

2. 缓存空值(Null Cache)

这是最直接有效的方案:当请求的ID不存在时,也把结果(比如null或空对象)写入缓存,设置较短的过期时间(比如2分钟),后续同样的请求就不会落到数据库。

java 复制代码
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是否可能存在,不存在直接返回错误。

java 复制代码
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), expectedInsertions);
filter.put(10001L); // 初始化时加入所有商品ID

if (!filter.mightContain(id)) {
    // 直接返回不存在,无需查库
    return NULL_GOODS;
}

布隆过滤器空间占用小,查询快,但有极小概率误判(即可能存在但实际不存在),一般业务可接受。

4. 接口限流与风控

对异常流量(比如同IP高频请求、爬虫批量撞库),可以加限流和黑名单机制。比如用Guava RateLimiter或Redis计数实现接口限流,超过阈值直接拒绝。

java 复制代码
RateLimiter limiter = RateLimiter.create(100); // 每秒100次
if (!limiter.tryAcquire()) {
    throw new RuntimeException("接口限流");
}

对于恶意IP可以直接封禁,或者引入验证码、滑块验证等人机识别。

5. 监控与报警

最后,搭建监控体系,实时监控Redis命中率、数据库QPS、接口异常流量,一旦出现异常及时报警,快速定位问题。


五、优化效果与复盘

经过上述方案上线后,Redis命中率恢复到95%以上,数据库压力大幅下降,接口响应时间稳定在200ms以内。空值缓存和布隆过滤器配合,基本杜绝了穿透问题。限流和风控机制也让爬虫和恶意流量无处遁形。

最关键的是,整个方案代码量不大,易于复用,后续新接口只要加空值缓存和布隆过滤器即可,极大降低了维护成本。


六、常见坑与经验总结

  1. 只缓存有数据的key,容易被穿透。一定要缓存null或空对象,哪怕只缓存几分钟,也能挡住大部分无效请求。
  2. 布隆过滤器不是100%准确,有极小概率误判,业务允许的情况下可以用,否则还是要查库兜底。
  3. 限流要结合业务场景灵活调整,不能一刀切,避免影响正常用户体验。
  4. 监控和报警非常重要,及时发现异常流量,快速定位问题,防止雪崩。
  5. 参数校验是第一道防线,前端和接口都要做,能拦掉一半的无效请求。

七、后续优化方向

虽然缓存穿透问题解决了,但还可以继续优化,比如:

  • 动态更新布隆过滤器,支持商品上下架实时同步;
  • 用分布式缓存方案(如Redisson)提升高并发下的缓存一致性;
  • 针对热点数据做预加载和预热,提高首屏响应速度;
  • 持续完善风控和流量识别,防止新型爬虫和攻击手段。

八、结语

缓存穿透问题在高并发Java后端场景下非常常见,尤其是电商、内容分发、金融等行业。只有系统性地设计参数校验、空值缓存、布隆过滤器、限流风控等组合方案,才能真正"降本增效",让系统稳定高效运行。

相关推荐
Algebraaaaa2 分钟前
为什么C++主函数 main 要写成 int 返回值 | main(int argc, char* argv[]) 这种写法是什么意思?
开发语言·c++
三木水24 分钟前
Spring-rabbit使用实战七
java·分布式·后端·spring·消息队列·java-rabbitmq·java-activemq
java1234_小锋37 分钟前
一周学会Matplotlib3 Python 数据可视化-绘制饼状图(Pie)
开发语言·python·信息可视化
别来无恙14943 分钟前
Spring Boot文件下载功能实现详解
java·spring boot·后端·数据导出
optimistic_chen44 分钟前
【Java EE初阶 --- 网络原理】JVM
java·jvm·笔记·网络协议·java-ee
weixin_456904271 小时前
Java泛型与委托
java·spring boot·spring
悟能不能悟2 小时前
能刷java题的网站
java·开发语言
IT古董2 小时前
【第四章:大模型(LLM)】05.LLM实战: 实现GPT2-(6)贪婪编码,temperature及tok原理及实现
android·开发语言·kotlin
北执南念2 小时前
Java多线程基础总结
java