项目缓存实战:热点 Key 击穿、缓存穿透与多级缓存架构
1. 引言文章目录1. 引言2. 缓存优化2.1 缓存击穿逻辑过期方案2.2 缓存穿透缓存空值方案2.3 布隆过滤器缓存穿透的进阶防线2.3.1 布隆过滤器的原理2.3.2 为什么存在误判如何优化2.3.3 为什么无法删除数据如何解决3. 数据一致性3.1 更新数据库后删除缓存3.2 删除失败消息队列补偿重试3.3 TTL 兜底4. 多级缓存Caffeine Redis4.1 为什么需要多级缓存4.2 二级缓存架构4.3 代码实现4.4 多级缓存注意事项5. 总结在高并发场景下缓存是提升系统性能、降低数据库压力的核心手段。然而缓存的使用并非“一放了之”如果设计不当反而会引入缓存击穿、缓存穿透、数据不一致等一系列问题。本文结合生产实践围绕以下三个主题展开缓存优化使用逻辑过期方案防止 Redis 热点 Key 的缓存击穿问题使用缓存空值方案解决 Redis Key 的缓存穿透问题。数据一致性更新数据库后删除缓存若删除失败则通过消息队列补偿重试并以 TTL 兜底共同保证数据一致性。多级缓存使用 Caffeine 本地缓存和 Redis 缓存搭建二级缓存架构提高热点数据访问速度降低 Redis 压力。2. 缓存优化2.1 缓存击穿逻辑过期方案缓存击穿指的是某个热点 Key 在缓存过期的瞬间大量并发请求同时打到数据库导致数据库压力骤增甚至宕机。常见的解决方案有互斥锁和逻辑过期。这里重点介绍逻辑过期方案。逻辑过期的核心思想是不设置真正的 TTL而是将过期时间作为业务字段写入缓存值中。读取时判断逻辑时间是否过期若过期则尝试获取互斥锁由单个线程重建缓存其他线程先返回旧数据。publicclassRedisCacheService{privatestaticfinalStringLOCK_PREFIXlock:;/** * 写入缓存逻辑过期 */publicvoidsetWithLogicalExpire(Stringkey,Objectvalue,longexpireSeconds){// 封装逻辑过期时间RedisDataredisDatanewRedisData();redisData.setData(value);redisData.setExpireTime(System.currentTimeMillis()expireSeconds*1000);stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));}/** * 读取缓存逻辑过期 互斥锁重建 */publicObjectgetWithLogicalExpire(Stringkey,Class?type,IDomainServiceservice){StringjsonstringRedisTemplate.opsForValue().get(key);if(StrUtil.isBlank(json)){returnnull;}RedisDataredisDataJSON.parseObject(json,RedisData.class);ObjectvalueredisData.getData();longexpireTimeredisData.getExpireTime();// 1. 未过期直接返回if(expireTimeSystem.currentTimeMillis()){returnvalue;}// 2. 已过期尝试获取互斥锁StringlockKeyLOCK_PREFIXkey;booleanlockedtryLock(lockKey);if(locked){try{// 重建缓存ObjectnewValueservice.queryFromDatabase();setWithLogicalExpire(key,newValue,30L);returnnewValue;}finally{unlock(lockKey);}}// 3. 未获取到锁返回旧数据returnvalue;}}优点在缓存重建期间其他线程仍能读到旧数据不会直接打到数据库也不会出现“缓存雪崩”式的空窗期。2.2 缓存穿透缓存空值方案缓存穿透指的是请求查询一个根本不存在的数据缓存和数据库中都没有导致每次请求都穿透到数据库。解决方案之一是缓存空值当数据库查询结果为空时仍然将该 Key 写入缓存但 Value 为空值并设置较短的 TTL如 2~5 分钟防止恶意请求反复穿透。publicObjectqueryWithCacheThrough(Stringkey,Class?type,IDomainServiceservice){// 1. 查询缓存StringjsonstringRedisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(json)){returnJSON.parseObject(json,type);}// 2. 缓存未命中查询数据库Objectvalueservice.queryFromDatabase();// 3. 数据库为空缓存空值if(valuenull){stringRedisTemplate.opsForValue().set(key,,5L,TimeUnit.MINUTES);returnnull;}// 4. 数据库有值写入缓存stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(value),30L,TimeUnit.MINUTES);returnvalue;}注意缓存空值只能缓解穿透无法完全杜绝。对于极端恶意攻击还应配合布隆过滤器在缓存之前做一层拦截。2.3 布隆过滤器缓存穿透的进阶防线缓存空值只能缓解穿透对于极端恶意攻击更推荐在缓存之前加一层布隆过滤器Bloom Filter把大量不存在的 Key 直接拦截在缓存层之外。2.3.1 布隆过滤器的原理布隆过滤器是一种空间效率极高的概率型数据结构主要用于快速判断某个元素是否属于某个集合。它的底层由两个极简组件构成二进制位数组Bit Array一个长度为 m 的位数组初始化时所有位置均置为 0。k 个相互独立的哈希函数能将任意输入元素均匀地映射到位数组的不同下标位置。工作流程如下添加元素将元素依次输入 k 个哈希函数得到 k 个数组下标并将位数组中这些位置的 Bit 值从 0 置为 1。查询元素同样使用这 k 个哈希函数计算目标元素的 k 个位置。只要这 k 个位置中有任何一个为 0就100% 确定该元素不在集合中如果这 k 个位置全部为 1则判定该元素“可能存在”于集合中。publicclassBloomFilterDemo{privatefinalBitSetbitsnewBitSet(120);// 位数组privatefinalintk3;// 哈希函数个数// 模拟 k 个哈希函数privateinthash(Stringvalue,intseed){inthvalue.hashCode()^(seed*0x9e3779b9);returnMath.abs(h)%bits.size();}// 添加元素publicvoidadd(Stringvalue){for(inti0;ik;i){bits.set(hash(value,i));}}// 查询元素任一位置为 0 则一定不存在publicbooleanmightContain(Stringvalue){for(inti0;ik;i){if(!bits.get(hash(value,i))){returnfalse;}}returntrue;}}2.3.2 为什么存在误判如何优化误判原因假阳性误判的根源在于哈希碰撞。由于位数组长度有限不同元素经过哈希计算后可能映射到相同的 Bit 位置导致一个从未录入的元素其对应的 k 个位置碰巧都被其他元素置为 1从而被误判为存在。优化/解决方案增加位数组长度m增大空间是最直接有效的方法能显著降低哈希碰撞概率。优化哈希函数数量k哈希函数并非越多越好存在理论最优值k ≈ 0.693 × (m / n)n 为元素数量此时误判率最低。使用高质量哈希函数采用分布更均匀的哈希算法如 MurmurHash3、xxHash避免位聚集。业务层二次校验回源查询布隆过滤器仅保证“一定不存在”的确定性。当它返回“可能存在”时再回源查询数据库或精确数据结构进行最终确认。动态扩展Scalable Bloom Filter当元素数量接近设计容量时创建新的一层布隆过滤器进行扩容以维持设定的误判率。2.3.3 为什么无法删除数据如何解决无法删除原因布隆过滤器的底层是位映射结构位数组中的每一个 Bit 并不是独占的而是被多个元素共享。如果尝试删除某个元素需要将其对应的 k 个 Bit 位清零但这会不可避免地清除掉其他共享这些位置的元素的存在痕迹导致后续查询其他元素时出现“假阴性”把存在的元素误判为不存在。优化/解决方案计数布隆过滤器Counting Bloom Filter将原本的 1 个 Bit 扩展为计数器通常使用 4 bits。插入时对应位置计数加 1删除时计数减 1查询时检查计数器是否均大于 0。代价是内存消耗会增加 3~4 倍。布谷鸟过滤器Cuckoo Filter基于布谷鸟哈希算法使用“指纹存储”和“两个候选桶”机制。它不仅空间利用率更高而且天然支持高效的删除操作。定期重建在业务低谷期如每天凌晨基于最新的有效数据重新构建一个全新的布隆过滤器以清除已删除元素带来的影响。结合其他数据结构将布隆过滤器作为前置的“快速否定器”配合哈希表等精确数据结构使用。布隆过滤器负责过滤大量不存在的元素精确结构负责处理实际的删除和最终判定。3. 数据一致性3.1 更新数据库后删除缓存为了保证数据库与缓存的一致性业界常用Cache Aside旁路缓存模式先更新数据库再删除缓存。这样即使删除缓存失败也只会短暂出现脏数据而不会长期不一致。3.2 删除失败消息队列补偿重试如果删除缓存失败直接忽略会导致缓存长期为脏数据。因此引入消息队列补偿重试机制更新数据库成功后发送一条“删除缓存”的消息到 MQ消费者收到消息后尝试删除缓存若删除失败则进行重试可设置最大重试次数重试仍失败则记录日志并告警人工介入。publicvoidupdateData(Longid,ObjectnewData){// 1. 更新数据库databaseMapper.update(id,newData);// 2. 发送删除缓存消息补偿机制MQMessagemessagenewMQMessage();message.setKey(cache:user:id);mqProducer.send(cache.delete.topic,message);}为啥会删除失败失败类型典型原因结果网络/调用失败应用调用 Redis 时网络抖动、超时、连接池耗尽DEL没执行或响应丢失Redis 服务异常Redis OOM、主从切换、集群故障、持久化阻塞删除指令失败或未被正确传播应用侧异常更新 DB 后进程崩溃、Pod 重启、try-catch 吞掉异常删除代码根本没执行删除成功但数据仍残留主从切换后删除未同步并发读请求把旧值重新写回缓存看起来删了但缓存还是旧数据Key 算错/漏删同一数据有多个缓存 Key只删了主键缓存没删列表、聚合缓存部分缓存仍是旧值为啥先更新数据库数据库是事实源必须先落库缓存只是副本。先保证 MySQL 提交成功再让缓存失效下一次读请求会按统一读路径重新从数据库加载。比“先删缓存再更新 DB”安全得多如果先删缓存在 DB 还没更新完时读请求会命中空缓存从数据库读到旧值并回填。结果就是DB 已更新缓存却长期保留旧值直到 TTL 过期。删除比更新缓存更不容易出错更新缓存需要重新计算读模型并发写时容易出现“谁后写谁赢”的覆盖问题。删除缓存只是让旧副本失效幂等、简单、失败后重试也安全。删除失败更容易兜底先更新 DB 后删缓存如果删除失败缓存里是旧值但可以通过 MQ 重试、Canal 监听 Binlog、TTL 兜底恢复。3.3 TTL 兜底即便消息队列补偿也失败缓存 Key 仍带有 TTL到期后自动失效最终会重新从数据库加载从而保证最终一致性。因此TTL 是数据一致性的最后一道兜底防线。4. 多级缓存Caffeine Redis4.1 为什么需要多级缓存Redis 虽然快但每次访问仍有一次网络开销。对于超高并发的热点数据可以让请求优先命中本地缓存Caffeine未命中再访问 Redis最后才回源数据库。这样既能大幅降低 Redis 压力又能显著提升响应速度。4.2 二级缓存架构未命中未命中回填回填命中命中客户端请求Caffeine 本地缓存Redis 缓存数据库返回响应4.3 代码实现ComponentpublicclassMultiLevelCacheService{AutowiredprivateStringRedisTemplatestringRedisTemplate;privatefinalCacheString,ObjectcaffeineCacheCaffeine.newBuilder().maximumSize(10_000).expireAfterWrite(5,TimeUnit.MINUTES).build();publicObjectquery(Stringkey,IDomainServiceservice){// 1. 查本地缓存ObjectlocalcaffeineCache.getIfPresent(key);if(local!null){returnlocal;}// 2. 查 RedisStringjsonstringRedisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(json)){ObjectvalueJSON.parseObject(json,Object.class);caffeineCache.put(key,value);// 回填本地缓存returnvalue;}// 3. 查数据库Objectvalueservice.queryFromDatabase();if(value!null){stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(value),30L,TimeUnit.MINUTES);caffeineCache.put(key,value);}returnvalue;}}4.4 多级缓存注意事项本地缓存一致性Caffeine 是 JVM 级缓存多实例部署时各节点数据可能不一致需配合消息通知或短 TTL 控制。内存占用本地缓存不宜过大建议设置最大容量和过期时间。更新策略更新数据时需要同时删除本地缓存和 Redis 缓存避免脏读。5. 总结本文围绕项目缓存的核心痛点给出了三套可落地的实践方案问题方案关键点缓存击穿逻辑过期 互斥锁过期期间返回旧数据单线程重建缓存穿透缓存空值空值短 TTL配合布隆过滤器更佳数据一致性删缓存 MQ 补偿 TTL 兜底最终一致性高并发热点Caffeine Redis 二级缓存降低 Redis 压力提升响应速度缓存设计没有银弹需要结合业务场景、并发量、一致性要求综合权衡。希望本文的实践能为你提供参考。

相关新闻

tfsec 检查规则解析:aws-ssm-secret-use-customer-key 强制 Secrets Manager 使用客户托管密钥(CMK)

tfsec 检查规则解析:aws-ssm-secret-use-customer-key 强制 Secrets Manager 使用客户托管密钥(CMK)

应用安全SASTIaC 【免费下载链接】tfsec Tfsec is now part of Trivy 项目地址: https://gitcode.com/gh_mirrors/tf/tfsec 点击查看 免费下载 导读 本文深入解析 tfsec 内置检查规则 aws-ssm-secret-use-customer-key(旧 ID AWS095)&#…

2026/10/15 3:06:36 阅读更多 →
深度学习入门:从核心概念到PyTorch实战训练全流程

深度学习入门:从核心概念到PyTorch实战训练全流程

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

2026/10/15 3:06:36 阅读更多 →
067、RDMA多播通信:UD模式下的多播组管理

067、RDMA多播通信:UD模式下的多播组管理

一、一个让我熬夜到凌晨三点的多播问题 去年做分布式存储集群的节点间心跳同步,我选了RDMA UD多播来广播状态信息。测试环境跑得飞起,一上生产就出幺蛾子——部分节点收不到心跳包,而且不是固定的节点,是随机丢。更诡异的是,用ibv_devinfo看端口状态全绿,用ibv_rc_pingp…

2026/10/15 3:06:36 阅读更多 →

最新新闻

动态规划:线性DP——多状态DP与子数组专题笔记

动态规划:线性DP——多状态DP与子数组专题笔记

动态规划算法——简单多状态dp问题统一解题模板:线性DP五步法1. 状态表示:以i位置结尾,拆分多种状态(选/不选、持有/不持有、不同颜色)2. 状态转移方程:根据上一步所有合法状态推导,取最值3. 初…

2026/10/15 3:56:45 阅读更多 →
Redis 从 0 到 1(九):主库宕机 30 秒自动切主——1 主 2 从 3 哨兵 + Go 客户端

Redis 从 0 到 1(九):主库宕机 30 秒自动切主——1 主 2 从 3 哨兵 + Go 客户端

我不会起名字322 后端 / 算法 / 数据库 📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL 文章目录Redis 从 0 到 1(九)&#xf…

2026/10/15 3:56:45 阅读更多 →
第 9 篇 · 数值计算与图像处理:NumPy、SciPy、Pillow

第 9 篇 · 数值计算与图像处理:NumPy、SciPy、Pillow

数值计算与图像处理:NumPy、SciPy、Pillow 数据分析链路中,除表格处理外还涉及两类底层能力:数值计算(数组运算、线性代数、插值与优化、信号处理)与图像处理。本节以 NumPy、SciPy、Pillow 三个库为例,说…

2026/10/15 3:56:45 阅读更多 →
【学习记录】DAP调试器电路原理图完全详解:从STM32最小系统到SWD调试接口

【学习记录】DAP调试器电路原理图完全详解:从STM32最小系统到SWD调试接口

【学习记录】DAP调试器电路原理图完全详解:从STM32最小系统到SWD调试接口 DAP调试器(CMSIS-DAP)是嵌入式开发中最常用的调试工具之一。它本质上是一个小型的STM32最小系统 USB接口 目标调试接口。本文从原理图角度,逐模块拆解DA…

2026/10/15 3:56:45 阅读更多 →
Python做微信机器人:个人微信API最小收发闭环实战

Python做微信机器人:个人微信API最小收发闭环实战

用 Python 做微信机器人,最常见的起步方式是:找个示例、复制一段发送代码、在测试好友上点一点。 能发出去当然高兴,但一接到真实业务——自动回、CRM、AI——就会发现:收不稳、回双份、偶发找不到人。 个人微信 API 实战&#x…

2026/10/15 3:56:45 阅读更多 →
IDEA 的命令补全又进化了

IDEA 的命令补全又进化了

我们先来还原一个极其真实的场景 你正在写一个 Java 类,突然想重构一个方法。你明明知道 IDEA 能做到,甚至昨天刚用过。但此刻,你的大脑一片空白——快捷键是什么来着? CtrlAltShiftT?还是 CtrlAltM? 你开始…

2026/10/15 3:55:44 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/14 7:53:34 阅读更多 →