新闻详情

新闻详情

首页 / 资讯中心 / 详情

后端缓存实战:Redis穿透、击穿、雪崩与一致性方案

发布时间:2026/10/1 10:45:20来源:尧图网络
后端缓存实战:Redis穿透、击穿、雪崩与一致性方案
1. 缓存到底帮后端扛住了什么做后端这些年缓存数据应该是我最常打交道的技术之一了。不管你是刚入门准备面试还是已经维护了几个老项目只要系统一有性能问题第一反应基本都是“加缓存”。这个思路本身没错但缓存不是万能的用不好反而会引入一堆新麻烦。这篇笔记我会从后端工程师日常干活的角度把缓存的原理、选型、经典故障、数据一致性、实战写法以及踩过的坑一次性说清楚。内容偏向 Java 后端 Redis 这条技术线但核心思路对所有后端语言都通用无论你是正在学后端的初学者还是被线上问题折磨的老手都能在这里找到点有用的东西。先解释一个容易混淆的点这里的“后端”指的是服务端软件开发不是芯片设计领域那个数字后端。有些热词里提到的“数字后端”是 IC 设计流程里的概念跟本文的缓存数据不是一回事大家别串台了。1.1 没有缓存的系统每天都在重复劳动我见过不少团队系统上线初期数据量小用户少所有请求都直接查数据库一切岁月静好。等到用户量上来数据库连接开始告警CPU 飙升接口响应从几十毫秒变成几秒钟这时候大家才意识到系统里大部分查询其实都在做重复劳动。举个例子一个电商系统的首页商品推荐一万个用户请求过来每个请求都执行同样的 SQL从数据库里读出同样的数据这其实就是浪费。数据库做一次查询可能要 5 到 20 毫秒听起来不算慢但并发一高数据库的连接池被占满新的请求只能排队接口自然就慢了。缓存的核心价值就是用内存换取数据库的解脱。内存的读取速度是纳秒级别通常比数据库查询快一到两个数量级把高频读取的数据放在内存里让绝大多数请求根本不触达数据库数据库的压力就能大幅下降。1.2 缓存的标准读取流程缓存的读取策略其实非常朴素就三步请求进来先去缓存查数据。缓存命中了直接把数据返回给调用方。缓存没命中去数据库查然后把结果写回缓存再返回。这个流程看起来简单但里面隐藏了一个关键点缓存未命中的那一刻其实是风险最高的窗口期。如果突然有一大堆请求同时未命中数据库会被瞬间打爆这就是后面要说的击穿和雪崩问题。用生活场景类比的话缓存就像是厨房里的备菜柜。客人点菜的时候厨师先看备菜柜里有没有切好的菜有就直接下锅没有才去冰箱拿原材料现场处理。备菜柜的存在让出菜速度快了很多但备菜柜里的菜会变质需要定期更换这就是缓存的过期时间。2. 方案选型本地缓存还是分布式缓存很多后端新人第一次接触缓存就直接上 Redis但 Redis 不是唯一的答案。不同场景下选型逻辑完全不一样。2.1 本地缓存为什么不够用本地缓存就是跑在应用进程内部的缓存Java 生态里常见的有 Caffeine、Guava Cache、Ehcache。本地缓存的优势是极致的快因为数据就在当前进程里连网络 IO 都省了读取速度是纯内存操作。但本地缓存的短板也很明显多实例部署时数据不一致。两台服务器各自缓存一份数据其中一台更新了另一台还是旧数据。内存浪费。每个实例都存一份同样的热点数据内存整体利用率低。无法跨实例共享。一个用户请求被负载均衡分到了 A 实例后续操作被分到了 B 实例B 实例里没有缓存又得重新查一次数据库。当然本地缓存也不是一无是处。我见过不少项目把本地缓存用在一些几乎不变的系统配置上比如字典数据、静态规则每一台机器只需要在启动时加载一次之后本地读取就行。这种场景用本地缓存非常合适。2.2 分布式缓存为什么主流选 Redis一旦系统需要多实例共享缓存就得引入分布式缓存。市面上可选的有 Redis、Memcached、Tair 等但目前主流就是 Redis。Redis 能成为标配主要有几个原因数据结构丰富。String、Hash、List、Set、ZSet 都能存业务适配性很强。自带过期机制。TTL 设置非常方便这是很多缓存场景的刚需。持久化选项灵活。虽然缓存不是数据库但 Redis 提供 RDB 和 AOF 两种持久化方式可以在性能和可靠性之间取舍。高可用方案成熟。主从复制、哨兵模式、集群模式都有成熟的实践。我做了一个简单的对比表方便新手理解本地缓存和 Redis 的定位对比维度本地缓存Caffeine分布式缓存Redis读取速度极快无网络开销快但有网络传输成本数据一致性多实例不一致全局基本一致容量上限受单机内存限制可横向扩展故障影响进程挂了缓存没了需要高可用方案防止单点适用场景单机配置、极小热点数据共享热点数据、锁、计数、会话2.3 我常用的组合方式实际项目中我经常采用两级缓存的思路一级本地缓存挡住超高频率的小范围热点二级 Redis 缓存共享全局限的热点数据。例如用户维度的基础信息同一个用户在一段时间内会频繁访问但单个用户的访问频率又不足以让本地缓存产生内存压力。这时候本地缓存放最近访问的少量用户数据Redis 放全量热点同数据读取时先查本地再查 Redis最后查数据库。两级缓存的复杂度在于更新时要同时失效两层缓存一旦没处理好可能出现本地缓存还是旧数据、Redis 已是新数据的怪现象。所以我一般只在数据实时性要求不高的场景使用两级缓存比如一周不变化的配置项、标签数据等。实时性要求高的业务直接用 Redis 单级缓存就够了。3. 三个经典故障穿透、击穿、雪崩这几乎是后端缓存面试必问的题也是线上系统最容易出事故的场景。很多公司都经历过某一刻数据库突然被打爆追查下来基本都是这三类问题的变种。3.1 缓存穿透查了个不存在的数据缓存穿透指的是请求的数据在缓存和数据库中都不存在导致每次请求都直接打到了数据库。举个例子一个商城系统用户查询订单详情。如果攻击者伪造了一批不存在的订单号比如负数订单号、超长订单号系统先去缓存查缓存没有再去数据库查数据库也没有。正常情况下这是无害的但如果攻击者用脚本高频发起请求每一次都会穿透缓存直达数据库数据库很快就会被拖垮。应对穿透的主流方案有三个缓存空值。数据库查询结果为 null 时也在缓存里存一份空值并设置一个较短的过期时间比如 5 分钟。这样后续同样的请求会在缓存里直接拿到空值不再打到数据库。布隆过滤器。把可能存在的数据 ID 提前写入布隆过滤器请求进来先判断 ID 是否在集合内不在就直接返回根本不查数据库。布隆过滤器的缺点是存在误判率而且标准版不支持删除。参数校验。在接口入口校验参数的合法性比如 ID 必须为正整数不合法直接拒绝。我自己的习惯是缓存空值为主、参数校验为辅。布隆过滤器虽然性能好但维护成本偏高数据量不大时没必要上。缓存空值要注意一个问题空值也要设置过期时间否则会堆积大量空 key 占用内存。3.2 缓存击穿一个热点 key 过期缓存击穿和穿透很容易混淆。击穿指的是一个热点 key 在过期的瞬间大量请求同时找不到缓存全部打到数据库。新闻热点场景最典型。一条爆炸性新闻同时被千万用户访问缓存里存了这条新闻的数据过期时间到了刚好缓存被清掉下一瞬间所有用户都来请求缓存全部未命中数据库直接被压垮。解决击穿的核心思路就两个方向互斥锁。缓存未命中时先获取分布式锁只有拿到锁的请求才允许查数据库其他请求等待一段时间后重新查缓存。这种方式能保证同一时刻只有一个请求在查库但实现上要小心死锁和等待超时。逻辑过期。缓存中存的值不依赖 Redis 的 TTL而是在 value 里额外存一个过期时间戳。读取时如果发现逻辑时间已过期就尝试获取互斥锁拿到锁的请求去后台重建缓存并返回旧值拿不到锁的请求直接返回旧值或短暂等待后重试。这种方式在秒杀场景下很常用因为用户几乎无感知。互斥锁和逻辑过期的对比其实很明显互斥锁实现简单但可能会阻塞请求逻辑过期吞吐量高但实现复杂而且数据短暂不一致。小项目我建议直接用互斥锁大并发场景再考虑逻辑过期方案。3.3 缓存雪崩大量 key 同时失效雪崩和击穿的区别在于范围。击穿是单个热点 key雪崩是一大批 key 在同一时刻集体失效或者 Redis 本身挂了。想象一下业务上线时开发图省事把所有商品的缓存过期时间都设置成了同一个值比如 24 小时后一整批商品数据全部过期。如果系统刚好在那时迎来访问高峰每条商品数据都未命中缓存所有请求一起涌入数据库数据库瞬间就崩了。应对雪崩我常用的手段有过期时间加随机值。比如基础过期时间 1 小时再加 0 到 10 分钟的随机数让每个 key 的过期时间分散开避免集体失效。Redis 高可用。哨兵或集群模式保证 Redis 不轻易宕机。服务降级和熔断。数据库扛不住时直接返回降级数据或错误提示保证系统不整体瘫痪。这三种手段可以同时用而且在大型系统里几乎都是标配。不要只做其中一种因为现实中故障往往是组合出现的。4. 数据一致性到底该写缓存还是删缓存缓存和数据库双写的一致性是后端工程里讨论最多的问题之一。不少团队在产品初期都想过“先更新数据库再把新数据写进缓存”这种方案但实际上我强烈不推荐。4.1 Cache Aside 模式业界最经典的模式是 Cache Aside规则就是两句话读时先读缓存缓存没有读数据库再回填缓存写时先更新数据库然后删除缓存。为什么不是更新缓存而是删除缓存原因在于更新缓存存在两个隐患并发写后读可能读到旧值。写请求 A 更新数据库为新值写请求 B 也更新数据库为另一个新值如果 A 后写缓存、B 先写缓存缓存里可能留下旧值。一些写后的值根本不会被读。如果某个业务字段频繁更新但很少读取每次更新都写缓存纯属浪费。删除缓存的意义在于让下一次读取时发现未命中再从数据库拉最新值回填。这样设计虽然简单但能保证数据最终一致。4.2 延迟双删Cache Aside 在低并发下很好用但在并发读写下有一个经典问题读请求 A 查数据库拿到旧值还没写回缓存时写请求 B 更新数据库并删除缓存然后读请求 A 把旧值写回缓存导致缓存里长期存着旧数据。解决这个问题的常用手段叫延迟双删。流程是更新数据库。删除缓存。休眠一小段时间比如 500 毫秒。再删一次缓存。第一次删除是为了清掉缓存中的旧数据第二次删除是为了清掉“旧读请求回填的脏数据”。中间休眠的这段时间就是为了让并发读请求完成回填操作。延迟双删有个隐患如果缓存里根本还没数据休眠时间就白等了白白增加接口耗时。所以有些实现会改成异步延迟双删通过 MQ 或其他异步机制触发第二次删除不阻塞主线程。4.3 最终一致与 binlog 订阅如果要追求更强的最终一致性可以在数据库层面做文章。用 Canal 这类工具订阅 MySQL 的 binlog数据库数据变更后binlog 里有记录订阅方根据变更内容主动删除对应缓存。这种方案的优点是代码侵入性小业务代码里不需要写一串删除逻辑。缺点是需要额外部署 Canal 和 MQ运维成本上去了。中小项目我不建议一上来就上这套Cache Aside 加延迟双删已经能应付绝大多数场景。说句实在话后端工程师追求的数据一致性永远是“最终一致”而不是“实时一致”。在缓存和数据库之间想做到任何时刻完全一致代价会大到不现实。很多团队最后都接受了“短暂不一致可以容忍但不能长时间不一致”的原则。5. 实战Spring Boot Redis 做热点数据缓存理论知识聊完了讲讲实际怎么落地。这里我以 Java 后端 Spring Boot Redis 为例这是目前中小企业后端项目最常见的组合之一。5.1 环境准备与配置在 Spring Boot 项目里引入 Redis 非常简单pom 里加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependencyapplication.yml 里的配置spring: redis: host: 127.0.0.1 port: 6379 password: timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0这里有个很常见的坑Spring Boot 默认的 RedisTemplate 使用的序列化器是 JDK 序列化直接存对象会导致 Redis 里存的数据是一堆二进制乱码既没法在 Redis 客户端里直观查看跨语言读取也会出问题。我的做法是自定义一个 StringRedisTemplate 专门存 String 结构的数据或者自定义 RedisTemplate 并指定 Jackson 序列化器让 key 用 String 序列化、value 用 JSON 序列化。这样存进去的是可读的 JSON排查问题方便得多。5.2 缓存注解还是手动缓存Spring 提供了一套声明式缓存注解接口上加 Cacheable 就能自动缓存返回值。用起来很方便但坑也不少。一个典型的例子Cacheable(value product, key #id, unless #result null) public Product getProductById(Long id) { return productMapper.selectById(id); }这注解有几个地方新手容易踩坑key 的 SpEL 表达式写错导致缓存 key 不符合预期。默认没有全局过期时间缓存会永久存下去。方法内部调用时注解不生效因为 Spring 的动态代理不会拦截同类内部调用。返回值必须能序列化否则会报错。我在实际项目中更偏向手动操作 RedisTemplate因为思路更显式。比如这样public Product getProductById(Long id) { String cacheKey product:detail: id; Object cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return JSON.parseObject(cached.toString(), Product.class); } Product product productMapper.selectById(id); if (product ! null) { redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES); } return product; }手动缓存的好处在于每一步都在你的掌控里尤其是缓存过期时间、空值缓存、异常处理这些用注解反而容易藏问题。当然如果团队规范做得好注解确实能让代码更简洁这是个取舍问题。5.3 在若依这类框架里看到的缓存用法如果你用过若依框架也就是 RuoYi会发现它封装了一个 RedisCache 工具类里面就是各种 getCacheObject、setCacheObject、deleteObject。这个工具类本质上是对 RedisTemplate 做了一层薄封装把序列化和过期时间统一处理了。这个思路值得借鉴。实际项目中我也喜欢把缓存的 key 前缀、过期时间等集中管理而不是散落在业务代码里。比如统一建一个 CacheConstants 常量类public class CacheConstants { public static final String LOGIN_TOKEN_KEY login_tokens:; public static final String PRODUCT_KEY product:detail:; public static final long DEFAULT_EXPIRE 30L; }这样一来改前缀或者调过期时间都只动一个文件不会满项目找魔法值。另外前后端分离的项目里登录 token 存 Redis 是标配做法这在若依里体现得很明显。用户登录后服务端生成一个随机 token把用户信息和权限数据存进 Redis设置过期时间后续请求带上 token 就能在 Redis 里直接查到会话信息不用每次去数据库查用户表。这种做法既能实现会话管理也能顺便控制登录状态的有效期。再往深一步说前后端分离场景下按钮重复提交问题也常借助 Redis 解决。比如提交订单时前端把订单号带过来后端先用 SETNX 在 Redis 里加锁如果 key 已经存在说明正在处理中直接拒绝重复提交。这就是 Hessian 锁或者说分布式锁的一种应用底层依赖的同样是缓存数据。6. 缓存实战中的坑与排查心得缓存好不好用用一阵子就露馅了。下面这些坑是我自己在项目中真实踩过的每一条都对应过线上问题。6.1 Key 设计规范缓存 key 的设计是个看起来小、影响很大的事。我见过有人直接用 userId 当 key也见过直接把整个请求参数拼进去当 key结果就是 cache 命中率极低或者不同业务之间互相覆盖。我的习惯是业务模块名 主体标识 具体字段用冒号分隔。例如product:detail:1001 user:profile:9527 order:list:1001:page:1这样设计有几个好处可读性强Redis 客户端里一眼能看出属于什么业务方便批量删除比如要清掉某用户所有缓存可以直接用user:profile:9527*模式匹配删除。key 的长度也要控制。Redis 的 key 越短越好几百字节的 key 会浪费内存和网络带宽。6.2 大 Key 和热 KeyRedis 里有两个经典故障模式做后端的必须会识别。大 key 指的是某个 key 的 value 非常大比如一个 list 存了几百万个元素或者一个字符串有几 MB。大 key 的危害在于存取时网络传输时间长Redis 单线程模型下会阻塞其他命令的执行迁移或扩容时也可能导致卡顿。排查方法很简单Redis 自带的--bigkeys参数可以直接扫描大 keyredis-cli --bigkeys热 key 指的是某个 key 被超高频率访问Redis 单实例的 CPU 被打满影响同实例上其他 key 的访问。解决热 key 的手段有给 key 拼接随机后缀分散到多个实例、加本地缓存、限流控制。6.3 缓存过期时间别拍脑袋给缓存设置多长的过期时间看似随意其实有讲究。设太短缓存效果差数据库压力没减下去设太长数据不及时更新用户看到旧数据。我的经验是过期时间取决于业务能够接受多长的数据延迟。比如商品库存数量延迟 5 秒都可能导致超卖纠纷那就不适合加缓存或者只缓存极短时间。而商品介绍、类目名称这类信息延迟 30 分钟用户感知不到就可以放心设长一点。还有一个技巧热点大促期间可以把正常过期时间缩短动态调整为平时的三分之一避免大量 key 集中在高峰时段过期。配合上文的随机值方案能有效降低雪崩风险。6.4 敏感数据不要直接进缓存这条很多人容易忽略。缓存里存数据时一定要思考数据本身是否敏感。用户手机号、身份证号、密码摘要这类隐私数据哪怕 Redis 部署在内网也尽量别直接缓存放明文。我见过一个项目为了省事把用户完整信息对象整体存进了缓存包括手机号和邮箱。后来 Redis 被拖库泄露出来的就是一大批明文隐私数据。之后我养成了习惯缓存里只存必要字段像手机号这类的显示宁可查一次数据库也不放进缓存。这不仅是技术问题也是合规和道德问题。后端工程师做技术决策时数据安全这条底线必须守住。7. 最后分享一点个人体会做了这么多年后端我对缓存有一个越来越深的体会缓存是性能优化里的第一道防线但永远不是唯一的防线。一个系统如果只能靠缓存续命说明基础架构和数据访问层本身就有问题。我见过一些老项目内存里塞了几百个缓存 key从配置到业务数据什么都有过期时间也乱七八糟。后来排查问题时发现很多缓存根本没生效反而增加了代码复杂度和内存压力。缓存和收拾电脑磁盘上的临时文件有点像——长时间不清理系统只会越来越臃肿定期审查缓存里的 key 是否还需要保留、过期时间是否合理是非常必要的维护动作。对于刚开始学后端的同学我的建议是先把这个项目的缓存代码读一遍问自己三个问题缓存被谁写、何时过期、缓存失效后从哪里回填。把这三个问题理清楚你对这个系统的缓存设计就比多数人都明白了。写代码的时候也永远默认“缓存会丢、Redis 会挂、数据会不一致”在这个前提下做设计才不容易翻车。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AI智能体安全实战:提示词注入与自主入侵防御指南 2026/10/1 11:37:55

AI智能体安全实战:提示词注入与自主入侵防御指南

1. 这不是科幻片,是正在发生的攻防现场“AI智能体安全:提示词注入到自主入侵,企业如何设防?”——这句话里藏着的不是未来预警,而是过去三个月我帮六家客户做安全评估时,亲眼看到的真实攻击链。所谓“提示词…

阅读更多 →
STL分解+残差自回归:构建可解释的时间序列预测系统 2026/10/1 11:37:49

STL分解+残差自回归:构建可解释的时间序列预测系统

简介:一份基于季节性趋势分解(STL)与残差自回归的可解释时间序列预测完整项目实例,适合具有Python及Pandas、Statsmodels基础的数据分析、算法开发与业务系统人员。资源围绕零售、电力、交通、制造等场景的中短期预测需求&#xf…

阅读更多 →
放假7天不停更:运营人节前3小时排期法 2026/10/1 11:37:49

放假7天不停更:运营人节前3小时排期法

国庆假期7天,账号怎么办?停更,怕掉粉掉节奏;不停更,又不想把假期过成移动办公。这不是矫情,而是绝大多数运营人的真实处境。今天就和大家分享,我是如何在节前花3小时完成排期,让账号…

阅读更多 →
ResNet动物图像分类实战:从训练到ONNX部署 2026/10/1 11:37:42

ResNet动物图像分类实战:从训练到ONNX部署

简介:这是一份面向Python深度学习初学者与图像分类实践者的ResNet动物图像分类项目源码包,聚焦于使用PyTorch或TensorFlow框架实现端到端的模型训练与预测。资源完整覆盖数据预处理、ResNet18模型构建、训练调优、权重保存(含已训练的resnet1…

阅读更多 →
编译器命令选项优化:从-O0到-O3、LTO与PGO实战指南 2026/10/1 11:37:42

编译器命令选项优化:从-O0到-O3、LTO与PGO实战指南

很多人一提到“优化程序”,第一反应就是改算法、换数据结构,但有个东西往往被忽略——你手里的编译器命令选项。同一个 .c 文件,用 -O0 编译和用 -O2 编译,跑起来性能差出三五倍是常有的事,尤其是循环密集、计算量…

阅读更多 →
rocketMQ消息中间件docker部署 2026/10/1 11:37:42

rocketMQ消息中间件docker部署

前言: 11.0.564.39可以是你服务器的内网ip。 一、执行前检查 # 1. 确认本机 IP 确实是 11.0.564.39 ip addr | grep 11.0.564.39 # 2. 确认 Docker 和 docker-compose 可用 docker -v && docker-compose -v # 3. 确认关键端口没被占用 ss -tlnp | grep -E 9876|1091…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉