彭贤踩坑实录:手写实现缓存穿透拦截,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);} }这段代码的问题有三点:注解粒度太粗:@Cacheable只处理了“有值”的情况,没处理“查了DB发现不存在”的情况。 异常处理缺失:如果DB查询抛异常,缓存不会被写入,下次请求还是打DB。 无前置拦截:所有请求都要进到Controller,经过Spring MVC的完整链路,开销大。更关键的是,新版中间件的@Cacheable实现里,key的生成逻辑变了。旧版是product: + id,新版变成了product:v2: + id + : + userId。这导致新旧版本的缓存Key完全不兼容,升级后缓存命中率瞬间归零,全量回源DB。 这就是“版本升级后 API 全变了”的惨烈后果。框架的黑盒逻辑一旦变化,你的业务代码就失去了控制力。 优化方案与代码:手写实现拦截器与布隆过滤器 彭贤的方案很直接:手写实现一个InvalidIdInterceptor,放在Filter链的最前面,用布隆过滤器(Bloom Filter)快速判断ID是否可能存在。 思路是这样的:启动时加载:把当前所有有效商品ID加载进布隆过滤器。 请求拦截:每个请求进来,先用布隆过滤器判断ID是否存在。 快速失败:如果布隆过滤器说“不存在”,直接返回404,不往后走。 概率容忍:布隆过滤器有极低的误判率(比如1%),但这1%的请求会正常走缓存和DB,对系统影响微乎其微。代码实现如下: // 优化后:手写实现拦截器 + 布隆过滤器 @Component public class InvalidIdInterceptor implements HandlerInterceptor {private final BloomFilterLong bloomFilter;// 构造函数注入,启动时初始化public InvalidIdInterceptor(ProductRepository repository) {// 假设商品总量100万,误判率0.1%this.bloomFilter = new BloomFilter(1_000_000, 0.001);ListLong 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%几个关键观察:DB压力骤降:优化前,5000 QPS全打DB。优化后,99%的恶意请求被拦截在Filter层,DB只收到200 QPS的正常请求。DB从“救命状态”回到“舒适区”。P99延迟稳定:优化前P99是2000ms,优化后150ms。这是因为无效请求不再进入业务逻辑,避免了线程池阻塞和DB等待。内存微增:布隆过滤器+黑名单占了约100MB内存,相比总内存1.2GB,占比8%。这个代价可以接受。CPU下降明显:从98%降到35%。主要省在两个方面:一是无效请求不再走Spring MVC完整链路,二是DB查询减少,应用层等待DB的时间大幅缩短。彭贤还特别提到,这次优化后,我们甚至没加服务器,直接扛住了之前10倍的流量。这就是手写实现带来的确定性收益:你清楚每一行代码在做什么,性能瓶颈在哪里,怎么调优。 落地建议:别迷信框架,掌握底层才安心 这次踩坑,彭贤总结了三条经验,值得所有后端工程师参考: 1. 关键路径要能“脱框架” 框架升级是常态,API变动是必然。如果你的核心逻辑完全依赖框架注解,一旦框架行为变化,你就被动了。手写实现关键拦截逻辑,虽然多写了几百行代码,但换来了控制力。 尤其是高并发、高可用的场景,底层逻辑必须自己掌控。布隆过滤器、缓存击穿、限流熔断这些,框架都提供了封装,但出问题时,封装层往往是黑盒。自己实现一遍,才能知道坑在哪。 2. 版本升级前,必须做兼容性测试 这次故障的根本原因是“版本升级后 API 全变了”,但我们没做兼容性测试。建议:升级前,在测试环境跑一遍核心接口的集成测试。 重点关注注解行为、默认配置、异常处理的变化。 如果框架文档没更新,直接去官方源码仓库看实现。比如这次缓存中间件的Key生成逻辑变化,就是去源码里翻出来的。3. 监控要覆盖“拦截层” 这次故障,我们监控的是DB和JVM,没监控Filter层。如果监控了拦截器,能更早发现“无效请求激增”的异常。建议:给拦截器加指标埋点,统计“拦截数”、“放行数”、“误判数”。 设置告警:当拦截率超过阈值(比如50%),说明可能有恶意流量或配置错误。 把拦截层的P99延迟也纳入监控,避免“拦截器本身成为瓶颈”。这次彭贤的救火,本质上是“用确定性对抗不确定性”。框架在变,业务在变,但底层的性能优化原理不变。手写实现不是倒退,而是前进。当你真正理解底层,才能在设计时做出更优的选择。 你公司项目里是怎么处理缓存穿透的?是用布隆过滤器,还是用空值缓存,还是有其他方案?欢迎评论聊聊。

相关新闻

一文搞懂build命令底层逻辑,面试不再挂

一文搞懂build命令底层逻辑,面试不再挂

一文搞懂build命令底层逻辑,面试不再挂 面试被问“build命令到底做了什么”,如果你只能答出“打包文件”,面试官的眼神通常会瞬间冷下来。很多开发者以为 build…

2026/9/22 23:06:24 阅读更多 →
例如避坑指南

例如避坑指南

3大Python版本升级深坑:源码解析带你避开API变动陷阱 刚把项目从 Python 2.7 升到 3.11,或者从 3.8 跳到 3.12,代码一跑就崩?别慌,这太正常了。很多转岗做后端或自动化的朋友,接手旧项目时最常遇到的噩梦就是…

2026/9/22 23:06:24 阅读更多 →
Win7系统下载避坑指南:面试必问的环境配置实战与底层逻辑

Win7系统下载避坑指南:面试必问的环境配置实战与底层逻辑

Win7系统下载避坑指南:面试必问的环境配置实战与底层逻辑 配置环境就卡半天,这大概是很多开发者最崩溃的瞬间。明明照着教程一步步来,结果系统蓝屏、驱动缺失、激活失败,时间全耗在了无关紧要的等待上。更扎心的是,面试官随口一问“你本地开发环境怎…

2026/9/22 23:06:24 阅读更多 →

最新新闻

Kornia RandomTransplantation 的 MPS 后端空轴过滤 Bug 修复解析(4160)

Kornia RandomTransplantation 的 MPS 后端空轴过滤 Bug 修复解析(4160)

计算机视觉人工智能深度学习图像处理 【免费下载链接】kornia 🐍 Geometric Computer Vision Library for Spatial AI 项目地址: https://gitcode.com/gh_mirrors/ko/kornia 点击查看 免费下载 导读 本文围绕 Kornia 版本迁移记录 changelog.d/migrati…

2026/9/24 2:58:15 阅读更多 →
Mosquitto 1.4.2 版本剖析:Broker 与客户端库关键缺陷修复详解

Mosquitto 1.4.2 版本剖析:Broker 与客户端库关键缺陷修复详解

后端消息队列消息路由 【免费下载链接】mosquitto Eclipse Mosquitto - An open source MQTT broker 项目地址: https://gitcode.com/gh_mirrors/mos/mosquitto 点击查看 免费下载 Mosquitto 1.4.2 是 Eclipse Mosquitto 在 2015 年 5 月发布的一个纯缺陷修复&…

2026/9/24 2:58:15 阅读更多 →
AI正在拆掉传统界面:从表单到对话,人机交互的范式转移

AI正在拆掉传统界面:从表单到对话,人机交互的范式转移

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 2:58:15 阅读更多 →
Segment Anything (SAM) 实战指南:在 AI-Research-SKILLs 中用点、框与掩码提示实现零样本图像分割

Segment Anything (SAM) 实战指南:在 AI-Research-SKILLs 中用点、框与掩码提示实现零样本图像分割

AI 技能人工智能大模型深度学习 【免费下载链接】AI-Research-SKILLs Comprehensive open-source library of AI research and engineering skills for any AI model. Package the skills and your claude code/codex/gemini agent will be an AI research agent with full hor…

2026/9/24 2:58:15 阅读更多 →
嵌入式软件静态测试(十二)——ISO 26262 ASIL等级对静态测试的要求:工具置信度与证据链构建

嵌入式软件静态测试(十二)——ISO 26262 ASIL等级对静态测试的要求:工具置信度与证据链构建

❄️ 我的个人专栏: 《智能软件工程AI4SE》 《嵌入式面试总结》 《嵌入式处理器架构解析》 《嵌入式与虚拟化》 《嵌入式软件测试》 🌟 Simplicity is the ultimate sophistication摘要:本文围绕 ISO 26262 标准对嵌入式软件静态测试的要求&…

2026/9/24 2:58:15 阅读更多 →
2025 geo搜索优化入门教程:助您轻松提升本地搜索排名【新手必看】

2025 geo搜索优化入门教程:助您轻松提升本地搜索排名【新手必看】

2025 geo搜索优化入门教程:助您轻松提升本地搜索排名【新手必看】您是否在为如何在激烈的市场竞争中脱颖而出而烦恼?在数字时代,geo搜索优化已成为企业,尤其是本地企业吸引目标客户的关键。本文将为您提供一份详尽的geo搜索优化入…

2026/9/24 2:57:14 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/23 4:49:06 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →