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

相关推荐
jessecyj2 分钟前
Spring boot整合quartz方法
java·前端·spring boot
苦瓜小生15 分钟前
【前端】|【js手撕】经典高频面试题:手写实现function.call、apply、bind
java·前端·javascript
报错小能手15 分钟前
深入理解 Linux 虚拟内存管理
开发语言·操作系统
山楂树の18 分钟前
【计算机系统原理】 组相联 Cache 地址划分与访问过程
缓存
和沐阳学逆向38 分钟前
我现在怎么用 CC Switch 管中转站,顺手拿 Codex 举个例子
开发语言·javascript·ecmascript
小仙女的小稀罕39 分钟前
听不清重要会议录音急疯?这款常见AI工具听脑AI精准转译
开发语言·人工智能·python
书到用时方恨少!1 小时前
Python random 模块使用指南:从入门到精通
开发语言·python
NGC_66111 小时前
Java 线程池:execute () 和 submit () 到底有什么区别?
java
cngm1101 小时前
解决麒麟v10下tomcat无法自动启动的问题
java·tomcat
色空大师1 小时前
【网站搭建实操(一)环境部署】
java·linux·数据库·mysql·网站搭建