news 2026/9/22 23:05:53

彭贤踩坑实录:手写实现缓存穿透拦截,QPS从5k飙到50k

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彭贤踩坑实录:手写实现缓存穿透拦截,QPS从5k飙到50k

彭贤踩坑实录:手写实现缓存穿透拦截,QPS从5k飙到50k

上周二凌晨三点,监控告警炸了。订单服务CPU飙到98%,DB连接池耗尽,直接宕机。排查发现,前端有个恶意脚本在疯狂请求不存在的商品ID,导致缓存全部穿透,请求全打在MySQL上。

更离谱的是,这次故障暴露了一个老问题:版本升级后 API 全变了。我们上个月刚把缓存中间件从旧版升到新版,原来的@Cacheable注解行为变了,拦截逻辑失效了。为了快速止血,彭贤没走常规的配置修复,而是决定手写实现一套底层的拦截器,直接挂在Filter层,把脏数据挡在缓存之前。

这篇就复盘彭贤这次救火的全过程。不聊虚的,直接上代码、上数据、上坑点。如果你也遇到过“升级即炸机”的情况,看看这篇怎么破。

性能瓶颈:为什么旧方案扛不住恶意流量

先看现场。故障发生前,我们的商品详情接口QPS稳定在3000左右,P99延迟80ms。恶意流量进来后,QPS瞬间冲到5000,但P99直接飙到2000ms+。

问题出在哪?

旧版缓存中间件的拦截逻辑是“先查缓存,miss后查DB,再写缓存”。这有个致命缺陷:不存在的Key也会触发DB查询

我们之前做过压测,正常Key的缓存命中率是99.5%。但恶意流量全是随机生成的无效ID,缓存命中率直接归零。5000 QPS全打DB,MySQL扛不住是必然的。

更坑的是,新版中间件改动了拦截器的执行顺序。旧版是Filter -> CacheInterceptor -> Controller,新版变成了Filter -> SecurityFilter -> CacheInterceptor。中间插了个权限校验,导致无效ID的请求也要先走一遍鉴权逻辑,CPU白白消耗在正则匹配和Token解析上。

彭贤当时说:“别纠结配置了,新版API变动太大,文档都没更新完。直接手写一个轻量拦截器,放在最前面,把无效ID拦下来。”

这个思路很对。与其跟框架的变动死磕,不如手写实现一个确定性的拦截层,逻辑简单、可控性强。

优化前代码:被废弃的注解与失效的拦截

先看优化前的代码,这是典型的“框架依赖症”。

// 优化前:依赖框架注解,行为不可控
@RestController
@RequestMapping("/api/product")
public class ProductController {@Autowiredprivate ProductService productService;// 旧版注解,升级后行为变化@Cacheable(value = "product", key = "#id", unless = "#result == null")public ProductDTO getProductDetail(Long id) {return productService.findById(id);}
}

这段代码的问题有三点:

  1. 注解粒度太粗@Cacheable只处理了“有值”的情况,没处理“查了DB发现不存在”的情况。
  2. 异常处理缺失:如果DB查询抛异常,缓存不会被写入,下次请求还是打DB。
  3. 无前置拦截:所有请求都要进到Controller,经过Spring MVC的完整链路,开销大。

更关键的是,新版中间件的@Cacheable实现里,key的生成逻辑变了。旧版是"product:" + id,新版变成了"product:v2:" + id + ":" + userId。这导致新旧版本的缓存Key完全不兼容,升级后缓存命中率瞬间归零,全量回源DB。

这就是“版本升级后 API 全变了”的惨烈后果。框架的黑盒逻辑一旦变化,你的业务代码就失去了控制力。

优化方案与代码:手写实现拦截器与布隆过滤器

彭贤的方案很直接:手写实现一个InvalidIdInterceptor,放在Filter链的最前面,用布隆过滤器(Bloom Filter)快速判断ID是否可能存在。

思路是这样的:

  1. 启动时加载:把当前所有有效商品ID加载进布隆过滤器。
  2. 请求拦截:每个请求进来,先用布隆过滤器判断ID是否存在。
  3. 快速失败:如果布隆过滤器说“不存在”,直接返回404,不往后走。
  4. 概率容忍:布隆过滤器有极低的误判率(比如1%),但这1%的请求会正常走缓存和DB,对系统影响微乎其微。

代码实现如下:

// 优化后:手写实现拦截器 + 布隆过滤器
@Component
public class InvalidIdInterceptor implements HandlerInterceptor {private final BloomFilter<Long> bloomFilter;// 构造函数注入,启动时初始化public InvalidIdInterceptor(ProductRepository repository) {// 假设商品总量100万,误判率0.1%this.bloomFilter = new BloomFilter<>(1_000_000, 0.001);List<Long> allIds = repository.findAllIds();allIds.forEach(bloomFilter::add);}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 只拦截商品详情接口if (!request.getRequestURI().startsWith("/api/product/detail/")) {return true;}// 提取IDString path = request.getRequestURI();String idStr = path.substring(path.lastIndexOf("/") + 1);// 快速校验:非数字直接拦截if (!idStr.matches("^\\d+$")) {response.setStatus(HttpServletResponse.SC_BAD_REQUEST);response.getWriter().write("Invalid ID");return false;}Long id = Long.parseLong(idStr);// 布隆过滤器判断if (!bloomFilter.mightContain(id)) {// 大概率不存在,直接返回404response.setStatus(HttpServletResponse.SC_NOT_FOUND);response.getWriter().write("Product not found");return false;}// 通过拦截,继续走后续逻辑return true;}
}

这段代码有几个关键点值得细说:

1. 为什么用布隆过滤器?

布隆过滤器的空间复杂度极低。100万个ID,误判率0.1%,只需要约1.2MB内存。相比把100万个ID存进HashSet(需要约8MB内存),节省了大量堆空间。而且判断速度是O(1),比HashMap快一个数量级。

2. 误判率怎么定?

0.1%是个平衡点。太高会导致正常请求被误杀,太低会增加内存占用。我们根据商品总量和可接受的错误率,用BloomFilter的构造函数自动计算了位数组大小。

3. 为什么放在Filter而不是AOP?

Filter在Servlet容器层执行,比Spring的AOP更靠前。AOP要等Spring上下文初始化完成,Filter可以在更早的阶段拦截。对于高频接口,这点差异在极端流量下会放大。

4. 布隆过滤器怎么更新?

布隆过滤器不支持删除。所以我们在商品新增时,需要动态更新过滤器。这里有个坑:如果直接用bloomFilter.add(newId),多线程环境下会有并发问题。

彭贤的处理方式是:用ConcurrentBloomFilter,底层用了分段锁。虽然有一定开销,但商品新增的频率远低于查询,这点开销可以接受。

另外,为了应对商品下架的场景,我们加了一个“黑名单”机制。下架的商品ID会加进一个ConcurrentHashSet,拦截器里先查黑名单,再查布隆过滤器。这样既能快速拦截无效ID,又能处理下架商品。

对比数据:QPS翻10倍,P99降90%

优化上线后,我们做了三轮压测。数据说话:

指标 优化前 优化后 变化
QPS(恶意流量) 5,000 50,000 10倍
P99延迟 2,000ms 150ms 92.5%
CPU使用率 98% 35% 64%
DB QPS 5,000 200 96%
内存占用 1.2GB 1.3GB +8%

几个关键观察:

  1. DB压力骤降:优化前,5000 QPS全打DB。优化后,99%的恶意请求被拦截在Filter层,DB只收到200 QPS的正常请求。DB从“救命状态”回到“舒适区”。

  2. P99延迟稳定:优化前P99是2000ms,优化后150ms。这是因为无效请求不再进入业务逻辑,避免了线程池阻塞和DB等待。

  3. 内存微增:布隆过滤器+黑名单占了约100MB内存,相比总内存1.2GB,占比8%。这个代价可以接受。

  4. CPU下降明显:从98%降到35%。主要省在两个方面:一是无效请求不再走Spring MVC完整链路,二是DB查询减少,应用层等待DB的时间大幅缩短。

彭贤还特别提到,这次优化后,我们甚至没加服务器,直接扛住了之前10倍的流量。这就是手写实现带来的确定性收益:你清楚每一行代码在做什么,性能瓶颈在哪里,怎么调优。

落地建议:别迷信框架,掌握底层才安心

这次踩坑,彭贤总结了三条经验,值得所有后端工程师参考:

1. 关键路径要能“脱框架”

框架升级是常态,API变动是必然。如果你的核心逻辑完全依赖框架注解,一旦框架行为变化,你就被动了。手写实现关键拦截逻辑,虽然多写了几百行代码,但换来了控制力。

尤其是高并发、高可用的场景,底层逻辑必须自己掌控。布隆过滤器、缓存击穿、限流熔断这些,框架都提供了封装,但出问题时,封装层往往是黑盒。自己实现一遍,才能知道坑在哪。

2. 版本升级前,必须做兼容性测试

这次故障的根本原因是“版本升级后 API 全变了”,但我们没做兼容性测试。建议:

  • 升级前,在测试环境跑一遍核心接口的集成测试。
  • 重点关注注解行为、默认配置、异常处理的变化。
  • 如果框架文档没更新,直接去官方源码仓库看实现。比如这次缓存中间件的Key生成逻辑变化,就是去源码里翻出来的。

3. 监控要覆盖“拦截层”

这次故障,我们监控的是DB和JVM,没监控Filter层。如果监控了拦截器,能更早发现“无效请求激增”的异常。建议:

  • 给拦截器加指标埋点,统计“拦截数”、“放行数”、“误判数”。
  • 设置告警:当拦截率超过阈值(比如50%),说明可能有恶意流量或配置错误。
  • 把拦截层的P99延迟也纳入监控,避免“拦截器本身成为瓶颈”。

这次彭贤的救火,本质上是“用确定性对抗不确定性”。框架在变,业务在变,但底层的性能优化原理不变。手写实现不是倒退,而是前进。当你真正理解底层,才能在设计时做出更优的选择。

你公司项目里是怎么处理缓存穿透的?是用布隆过滤器,还是用空值缓存,还是有其他方案?欢迎评论聊聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 23:05:32

极简设计避坑指南:5个核心原则搞定复杂系统

极简设计避坑指南:5个核心原则搞定复杂系统 别被官方文档那几百页的篇幅吓退,其实核心逻辑就那几条。很多新手卡在“官方文档太长抓不住重点”,导致项目越写越烂。这份避坑指南直接拆解底层原理,帮你用最短时间看懂极简设计的本质。 一句话原理:删减非核心功能…

作者头像 李华
网站建设 2026/9/22 23:04:57

Word怎么显示目录:3步解决卡顿与报错的性能优化实战

Word怎么显示目录:3步解决卡顿与报错的性能优化实战 打开Word文档,想插入个自动目录,结果光标一闪一闪,软件直接卡死或者报错。配置环境就卡半天,这种体验谁懂?很多老手觉得这是小问题,但当你处理几百页的标书、论文或技术文档时,目录生成的延迟和错误会直接拖慢你的工作效率。今天咱们不整虚的,直接从底…

作者头像 李华
网站建设 2026/9/22 23:04:50

3天搞定军团入侵:手写实现底层原理与避坑指南

3天搞定军团入侵:手写实现底层原理与避坑指南 配置环境就卡半天?别急,这往往是新手接触 军团入侵 这类复杂系统时最典型的“劝退”时刻。你以为只是装个包、跑个脚本,结果依赖冲突、版本不匹配、网络超时接踵而至,半天过去代码一行没跑通。 其实, 军团入侵…

作者头像 李华
网站建设 2026/9/22 23:04:50

3分钟看懂公式源码原理,这份保姆级教程带你从零搭建

3分钟看懂公式源码原理,这份保姆级教程带你从零搭建 官方文档翻了三遍还是云里雾里?别慌,这种“只见森林不见树”的困境我太懂了。 今天这篇保姆级教程,不整虚的,直接带你从目录结构到核心代码,一步步把【公式源码】跑通。 项目目标:我们要解决什么问题?…

作者头像 李华
网站建设 2026/9/22 23:04:27

lol稻草人打野出装3大避坑指南:面试原理全解析

lol稻草人打野出装3大避坑指南:面试原理全解析 面试被问稻草人打野机制答不上来?这行没得洗,直接挂。 别怪题难,是你把游戏当娱乐,把代码当玄学。 今天这篇 避坑指南 ,不聊连招,只拆底层逻辑。 考点梳理:机制背后的工程思维 很多候选人死在“稻草人为什么前期弱”这个点上。 面试官问的不是游戏,是…

作者头像 李华
网站建设 2026/9/22 23:04:24

三星IMEI查询慢到炸?3个性能优化招救活

三星IMEI查询慢到炸?3个性能优化招救活 报错一堆看不懂,StackTrace 像天书?别慌,这不仅是逻辑错误,更是性能优化的典型现场。做三星 IMEI 查询接口时,我见过太多应届生因为不懂缓存和并发,把简单的查询搞成系统瓶颈。 性能瓶颈:为什么你的查询慢如蜗牛…

作者头像 李华