
缓存这个话题几乎每个做后端的人都会被问到几次。面试也好线上故障也罢翻来覆去就是那几个经典问题缓存穿透、击穿、雪崩再加上缓存和数据库的一致性。很多人能背出概念但真到了线上环境一遇到缓存失效导致数据库被打爆的场景还是会手足无措。这篇内容我想抛开教科书式的定义从实际踩坑和解决问题的角度把这几类经典问题掰开揉碎聊一遍。这些问题看似基础但每个背后都藏着不少设计上的取舍适合刚接触缓存、或者已经在用 Redis 但被线上问题困扰过的开发者参考。1. 缓存穿透请求打到了不存在的数据上1.1 穿透的本质与故障表现缓存穿透说的是请求查询的数据在缓存和数据库里都不存在每次请求都绕过了缓存直接打到数据库上。正常流程下用户请求先查缓存缓存没命中再查数据库数据库有数据则回写缓存下次请求直接命中。但如果被查询的是一个不存在的 ID数据库查不到缓存也不会写于是每次请求都会重复经历“缓存未命中→数据库查询→返回空”这个完整链路。我见过最典型的一次事故是这样的某个对外接口接收用户传入的订单号某天有一批脚本刷量请求传入的全是随机拼接、根本不存在的订单号。正常情况下这个接口的缓存命中率在 95% 以上而这批请求全部穿透数据库瞬间扛不住接口响应时间从 30ms 飙到了 3 秒以上紧接着数据库连接池被打满整个服务开始连环超时。穿透的本质是请求绕过了缓存这层屏障让数据库承受了本该由缓存挡住的压力。更麻烦的是这种请求往往不是正常用户行为而是恶意遍历、脚本攻击或者上游系统传入的脏数据。所以解决穿透不只是加一层缓存那么简单得从入口流量、缓存策略和数据校验三个层面同时想办法。很多团队第一反应是“把不存在的 key 也缓存起来”这确实有效但要注意细节。对于查询结果为空的 key可以把它缓存成一个特殊值比如空字符串或者一个约定好的占位符并且设置一个相对较短的过期时间比如 5 到 10 分钟。这样做的好处是短时间内相同的无效请求会命中这个空值缓存不再穿透到数据库。坏处也很明显如果有大量不同的无效 key 进来缓存里会堆积海量的空值占位白白浪费内存。所以空值缓存通常会加一个限制条件只有当某个 key 在短时间内被重复查询多次才去缓存空值。比如用一个简单的计数结构记录每个 key 的查询次数超过阈值再写入空值缓存。这个方案比无脑缓存所有空 key 要聪明一些内存占用可控也能防住大多数穿透场景。1.2 布隆过滤器与前置拦截另一个经典解法是布隆过滤器。布隆过滤器的核心思想是用一个很小的位数组加上多个哈希函数判断一个元素是否在集合中。它的特点是“判断不存在一定准确判断存在可能有误判”。所以布隆过滤器特别适合用来挡穿透把所有可能存在的订单号、用户 ID 或商品 ID 预先放到布隆过滤器里请求进来先查布隆过滤器如果它说这个 key 不存在那就直接返回根本不用查缓存和数据库。我实际用过布隆过滤器之后感受比较深的一点是初始化数据是个麻烦事。如果是订单号这种有规律的 ID可以全量刷进过滤器但如果数据量大比如上亿条记录构建布隆过滤器要花不少时间和内存。还有一种场景是增量数据新产生的 ID 必须同步写入布隆过滤器否则实际存在的 key 会被过滤器误判为不存在导致正常请求被拦截。这个同步逻辑往往比过滤器本身更复杂。布隆过滤器的使用位置也有讲究。可以放在应用层由业务代码先查过滤器再查缓存也可以放在缓存前面的一层代理上比如某些网关或者缓存代理组件支持过滤器配置。放在应用层的好处是灵活可以根据不同业务定制不同的过滤器策略。放在代理层的好处是对业务透明不用改业务代码。我在实践里更倾向于应用层因为能拿到更细的业务语义比如不同接口可以用不同的过滤器集合。需要注意布隆过滤器的误判率跟位数组大小和哈希函数数量直接相关。位数组越小误判率越高但省内存位数组越大误判率越低但占用空间也随之增加。新手很容易忽视初始化参数的调整直接用了默认值结果误判率偏高。实际上误判带来的影响不是“放过了不存在的请求”而是“拦截了部分真实请求”这种问题在线上是很难排查的。所以参数一定要根据预估数据量提前算好。提示布隆过滤器不是万能的它只能解决“key 不存在”的穿透问题。如果请求背后的业务逻辑本来就是查询一个合法但不存在的资源比如未上架的商品那过滤器里就不会有对应 ID这些请求依然会走数据库。这种情况要靠空值缓存来兜底。说到穿透还有一个容易忽略的点入口层的参数校验。很多穿透攻击其实是通过不断变换 GET 参数实现的如果在网关或者 Controller 层就能校验参数格式、范围、长度大部分无效请求根本走不到缓存层。比如订单号有固定长度和格式那就先校验格式格式不对直接返回参数错误。这个成本几乎为零但能挡掉大量低质量攻击。排除上面的思路还有一个更立体的方案限流 降级。如果穿透请求量实在太大超出了一定阈值干脆对非核心接口做降级处理返回默认值或者提示“系统繁忙”优先保护数据库。这虽然不是根治方案但在大促或者被刷的高峰期是保命的手段。我现在的习惯是穿透治理从来不只靠一种手段空值缓存、布隆过滤器、参数校验、限流策略组合起来用才能把数据库的压力彻底降下来。2. 缓存击穿热点 key 过期的瞬间压力2.1 击穿与穿透的区别很多人把缓存击穿和缓存穿透混为一谈其实它们完全是两类问题。穿透针对的是不存在的 key而击穿针对的是一个非常热点、同时在缓存中过期的 key。这个热点 key 一旦过期瞬间会有大量请求同时发现缓存没有数据然后一起涌向数据库去查同一条数据。击穿的关键词是“热点 key”和“瞬时并发”。比如某个电商平台突然有一个爆款商品被推上首页这个商品的详情被缓存起来了缓存过期时间设的是 30 分钟。过了 30 分钟缓存刚好失效而此刻正有几十万用户在看这个商品这几十万个请求在同一秒里全部穿透缓存直接打到数据库。数据库根本扛不住这种同 key 高并发查询哪怕只是查一条简单的数据也会因为瞬间涌入的连接数把数据库打垮。我做过一次线上复盘场景跟上面说的几乎一模一样。当时为了缓解数据库压力我们不得不在缓存失效后的那几秒内对商品详情接口做了人工限流同时让老数据继续返回。这次事故之后我们彻底认识到热点 key 的过期时间设计不能跟普通 key 一样拍脑袋决定。从本质上讲击穿是“并发放大”的问题。一个 key 过期本来只有一次数据库查询需求但因为有大量并发请求同时到达变成了一次性 N 次数据库查询。这种问题不能光靠加缓存解决因为缓存解决的是“读多写少”的重复查询问题而击穿是“同一时刻集体绕过缓存”的问题。要解决它思路就是要么让同一时刻只有一个请求去查数据库要么让这些请求不直接打到数据库。2.2 互斥锁与逻辑过期方案拦截并发请求最直接的方式是互斥锁。当缓存没有命中时不是立刻去查数据库而是先尝试获取一个分布式锁比如 Redis 的 SETNX 命令。拿到锁的请求去查数据库并重建缓存其他请求则在锁外等待缓存重建完成后统一从缓存读取。这个方案的关键细节在于锁的获取方式。如果用 SETNX 实现要注意设置过期时间防止持有锁的线程崩溃导致死锁。我用的是SET key value NX EX 10这种原子命令一次性设置锁和过期时间。锁的粒度要尽可能小最好只用缓存 key 本身作为锁的标识这样不同数据之间的查询不会互相阻塞。互斥锁方案的缺点是在缓存失效的短暂窗口内会有大量请求阻塞等待锁释放导致接口响应时间变长。从用户体验来看假如热点 key 重建缓存需要 200ms那排在后面的请求可能要多等 200ms 甚至更久。对于一些对延迟极为敏感的接口这种等待是不可接受的。逻辑过期方案则是另一种思路。它不设置真正的 TTL而是在缓存 value 里额外存一个过期时间戳。请求读取缓存时发现逻辑过期时间到了不会立刻删除缓存而是先获取互斥锁在锁内去数据库查新数据并更新缓存其他请求在锁未释放时依然可以读取旧的缓存数据只是这次读取可能返回的不是最新数据。从一致性角度看逻辑过期方案牺牲了短暂的数据新鲜度换来了可用性。对于商品详情、公告内容这类对时效性要求没那么苛刻的数据这个权衡是非常有价值的。我实际用下来逻辑过期方案比互斥锁更平滑因为不会出现请求堆在锁上的现象但代价是实现复杂度高一些要为缓存 value 设计带有过期字段的结构还要处理旧数据和新数据结构兼容的问题。注意逻辑过期方案中的“旧数据”不等于脏数据。它只是延迟了数据更新的生效时间最终新数据还是会写入缓存。业务场景必须能容忍这种短暂不一致如果数据一秒都不能差这个方案就不适用。还有一种辅助手段是热点 key 的永不过期策略。对于一些极端热点的数据干脆不设置过期时间而是通过后台异步任务定时刷新缓存。刷新任务每隔一段时间从数据库拉取最新数据写进缓存如果数据库更新失败了缓存里还有旧数据不会出现缓存完全失效的情况。这相当于把“读时重建”变成了“主动刷新”规避了瞬时高并发打库的问题。永不过期方案适合那些数据量不大但读取频率超高的场景比如配置信息、热门榜单、推荐位数据。它的缺点是需要额外的定时任务或者消息驱动机制来维护缓存更新而且对于数据实时性要求高的场景异步刷新的延迟可能达不到预期。所以我一般会把“永不过期 异步刷新”跟“逻辑过期 互斥锁”搭配使用核心热点数据用前者普通热点数据用后者。3. 缓存雪崩大规模缓存同时失效3.1 雪崩的典型成因缓存雪崩跟击穿最大的区别在于范围。击穿是单个热点 key 失效雪崩是大批 key 同时失效或者整个缓存层不可用。大面积失效的直接后果就是几乎所有请求都绕过缓存打到数据库数据库瞬间被压垮整个系统进入雪崩状态。最常见的雪崩诱因有两个。第一个是大量 key 设置了相同的过期时间。比如系统里有一批热点数据都是每天凌晨 0 点统一过期一到 0 点成千上万的 key 同时失效数据库压力瞬间达到峰值。第二个是整个缓存节点宕机比如 Redis 主节点挂了如果没做好高可用所有请求在缓存恢复前都会直接打到数据库。我遇到过一种典型的雪崩场景运营活动上线时一次性往缓存里写了几千个商品 key过期时间全部设置为 30 分钟。活动上线半小时后这批 key 同时到期瞬间数据库查询量翻了十倍CPU 直接飙到 99%。复盘时发现其实只要在生成 key 时给过期时间加个随机值就能避免这种集体失效。从系统设计上看雪崩的破坏力不只是数据库压力大还会引发连锁反应。数据库慢查询变多导致应用服务线程池被打满应用层接口全部变慢前端不断重试进一步放大流量。这种正反馈一旦形成光靠恢复缓存是救不回来的必须要有熔断和降级机制兜底。3.2 过期分散化与多级缓存降级防止雪崩最实用的一招就是让 key 的过期时间分散开。具体做法是在基础过期时间上加上一个随机数。比如原本设置 30 分钟过期现在设置为 30 分钟加上 1 到 5 分钟的随机偏移量。这样每个 key 的实际过期时间都有差异不会集中在同一秒大面积过期。代码实现非常简单int baseTtl 1800; // 30分钟 int randomOffset random.nextInt(300); // 随机偏移 0~5 分钟 int actualTtl baseTtl randomOffset; redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(actualTtl));这个随机偏移量的上限怎么定我一般按基础 TTL 的 5% 到 10% 来设置。如果基础 TTL 是 30 分钟随机偏移 3 分钟左右就够用了。设置太大可能会导致部分 key 提前过期影响缓存命中率设置太小则分散效果不明显。第二招是多级缓存。在多级缓存的架构下应用本地缓存比如 Caffeine、Guava作为第一级Redis 作为第二级数据库作为第三级。查询时先查本地缓存没命中再查 Redis再没命中才查数据库。这样一来即使 Redis 里的大批 key 同时失效本地缓存依然能扛住很大一部分请求数据库压力并不会瞬间暴涨。多级缓存的难点在于数据一致性维护。Redis 里的数据更新了本地缓存可能还是旧的需要借助消息通知或者版本号机制让各节点感知变化。我之前在某个项目里用的是发布订阅模式数据变更时先更新数据库和 Redis再发送一条消息到本地节点的监听器让本地缓存主动失效。这种方式实现不算复杂但前提是业务能接受短暂的数据不一致。多级缓存适合读多写少、数据量适中、节点数可控的业务。如果节点特别多每个节点的本地缓存都存储同一份数据内存浪费会比较严重。第三招是依赖降级和熔断。即使做了过期分散缓存层也可能因为别的故障整体不可用这时系统必须有预案。我的习惯是给每个下游调用设置超时时间和失败降级逻辑查缓存超时了直接跳过缓存查数据库数据库也超时了返回一个兜底数据或者直接提示稍后重试而不是让请求无限阻塞。降级逻辑在代码里要写得尽量轻不要因为降级本身引入新的性能问题。比如有些团队在熔断时用线程池隔离但线程池的排队机制反而会加剧资源耗尽。我后来改用信号量隔离或者直接用轻量级限流组件效果比线程池好不少。提示雪崩治理的最终目标是让系统在极端情况下仍能保持核心可用而不是追求缓存永不失效。所以每次做缓存设计时我都会提前问自己如果缓存 10 秒内不可用系统会怎样如果数据库撑不住 5 倍流量哪些接口可以先降级把这些问题的答案写进应急预案比出事后再想方案可靠得多。4. 缓存与数据库的一致性最棘手的数据正确性问题4.1 更新顺序的权衡缓存与数据库一致性的核心矛盾在于缓存和数据库是两个独立的存储系统无法用单事务保证原子性任何顺序都会存在不一致的窗口。经典的做法是 Cache Aside 模式读的时候先读缓存读不到再读数据库然后回写缓存写的时候先更新数据库再删除缓存。为什么更新数据库后是删除缓存而不是更新缓存这个选择背后有实际原因。更新缓存需要知道数据库里的最新值还要考虑并发写覆盖的问题删除缓存则简单得多下次读请求自然会把最新数据重新加载到缓存。删除缓存的方式避免了并发写时旧数据覆盖新数据的风险代价是删除后的一小段时间里缓存是空的会有一次额外的数据库查询。但“先更新数据库再删除缓存”也有问题如果删缓存失败了缓存里就一直是旧数据除非等 TTL 到期否则用户会持续读到过期数据。这是我在线上遇到过最多的情况。删除失败的原因可能是 Redis 超时、网络抖动或者删除时 Redis 正好在故障切换。单纯执行一次删除操作而不考虑失败后的补偿一致性就无从保证。还有一种并发场景很隐蔽两个线程同时操作线程 A 先更新数据库为新值线程 B 后更新数据库为更旧的值然后线程 A 删除缓存线程 B 也删除缓存最终缓存是空的看起来没问题。但更复杂的时序里如果线程 A 先删了缓存线程 B 在数据库更新前又读了一次旧值并写回缓存那么线程 A 更新后的删除操作可能发生在缓存回写之后导致缓存留下旧值。这种场景在前几年被认为是个 bug后来逐渐被延迟双删方案覆盖。4.2 延迟双删与可靠补偿延迟双删的思想很简单更新数据库之后删除一次缓存然后等待几百毫秒再删除一次缓存。第二次删除是为了清除在第一次删除之后、数据库更新完成之前可能被并发读请求写回缓存的旧数据。延迟时间怎么定需要大于一次典型的读请求耗时。如果业务读请求平均耗时在 50ms延迟时间可以设置成 200ms 到 500ms。如果读请求耗时波动大或者涉及慢查询延迟时间要相应拉长。这个参数没有标准答案需要根据业务实际观察。延迟双删的实现可以放在业务代码里用一个延迟线程池去执行第二次删除也可以用消息队列异步发送删除任务。不过延迟双删并不能做到强一致它只是缩小了不一致的时间窗口。如果对一致性要求极高光靠延迟双删是不够的。更可靠的做法是引入可靠事件机制删除缓存失败后把删除任务投递到消息队列由消费端重试删除。重试逻辑里要设置最大重试次数和退避策略避免无限重试打爆 Redis。我的推荐是对一致性要求高的数据用“先更新数据库→删除缓存→删除失败则发消息重试”这套组合拳。配合对 Redis 操作的监控能发现删除失败的概率高不高。实际使用中这套方案的不一致窗口非常小对绝大多数业务完全够用。另外一种思路是监听数据库的 binlog 来驱动缓存更新。组件会监听数据库变更日志解析出变更的数据内容然后由消费者更新对应缓存。这个方案的好处是业务代码完全不需要手动维护缓存逻辑数据库更新即触发缓存更新不需要考虑业务代码里忘删缓存的问题。坏处是引入了额外的中间件架构复杂度上升而且 binlog 解析到缓存更新之间有延迟依然不是强一致。从实际选型的角度考虑我认为大部分业务场景不需要追求缓存强一致。缓存本来就是为了性能牺牲一点一致性而引入的中间层。只要不一致窗口控制在可接受范围比如几百毫秒用户基本感受不到。真正要注意的是不能出现长时间、大面积的不一致比如缓存误删失败导致数据老半天不更新。可靠的删除补偿机制才是一致性的底线。注意凡是涉及缓存更新我都不推荐在业务代码里直接“更新缓存值”。宁可多花一次缓存重建的数据库查询成本也不要冒着并发覆盖的风险去更新缓存。这个原则看似保守实际能规避大量复杂问题。5. 缓存淘汰与过期策略内存管理的实践细节5.1 LRU、LFU 与 Redis 的近似实现缓存容量总是有限的不可能把所有数据都放在内存里。当缓存空间不足时必须有一套规则决定淘汰哪些数据。最常见的淘汰算法是 LRULeast Recently Used最近最少使用和 LFULeast Frequently Used最不经常使用。LRU 的核心思路是如果一个数据最近被访问过那么未来被访问的概率也更高。所以淘汰时优先干掉最近没被访问过的数据。实现上常用双向链表配合哈希表每次访问一个 key就把它移动到链表头部淘汰时从链表尾部删除。LFU 的核心思路则是如果数据在过去的访问频率很低未来大概率也不会被访问。淘汰时优先干掉访问频率最低的数据。LFU 适合处理那种“周期性热点”的场景——数据可能不是最近访问但是长期被频繁访问LRU 很可能误杀高频数据。Redis 实际实现的是近似 LRU并非严格 LRU。因为严格 LRU 需要记录每个 key 的访问顺序内存开销太大。Redis 的做法是在内存达到上限时随机抽取若干个 key按最后访问时间排序淘汰其中最旧的一个。抽取样本数可以配置默认是 5 个样本数越多越接近严格 LRU但 CPU 消耗也越高。Redis 4.0 之后加入了 LFU 淘汰策略。LFU 的实现也用近似方式用一个概率衰减机制来模拟访问频率的衰减避免热数据永远不被淘汰。从实践看LFU 适合读多写少且存在长期热点数据的业务LRU 适合访问模式相对均匀的业务。很多团队直接默认 LRU遇到数据访问频率差距很大的场景时命中率其实并不理想。5.2 淘汰策略的选择与参数配置Redis 的淘汰策略通过maxmemory和maxmemory-policy两个参数控制。maxmemory设定了 Redis 能用的最大内存maxmemory-policy设定了内存达到上限后的淘汰行为。常见策略包括noeviction不淘汰写命令直接报错allkeys-lru在所有 key 中按 LRU 淘汰volatile-lru只在设置了过期时间的 key 中按 LRU 淘汰allkeys-lfu在所有 key 中按 LFU 淘汰volatile-lfu只在设置了过期时间的 key 中按 LFU 淘汰allkeys-random随机淘汰任意 keyvolatile-random在设置了过期时间的 key 中随机淘汰这里有一个新手比较容易踩的坑如果使用volatile-lru缓存里必须有一定比例的 key 设置了过期时间否则内存达到上限时找不到可淘汰的 keyRedis 就只能对写入报错。我之前就把一个业务实例的内存策略配成了volatile-ttl结果业务代码里大量 key 都没设置 TTL运行一段时间后 Redis 直接拒绝写入排查了很久才意识到是策略跟业务不匹配。从实践角度我给大部分缓存场景推荐allkeys-lru。理由很简单缓存层的数据本身就没有强一致要求任何 key 被淘汰都可以由下次查询重建。设置过期时间的主要目的是防止数据长期不更新而不是配合“只能淘汰过期 key”的策略。maxmemory的设定也有讲究。不能直接设成物理内存大小要给操作系统和 Redis 自身的其他开销留余量。比如机器内存 16GRedis 实例的maxmemory可以设在 12G 左右。设置太小浪费机器内存设置太大容易触发系统 swap性能会急剧下降。Redis 分配内存是高于实际数据量一点的所以预留 20% 到 30% 比较稳妥。另外还要关注过期的 lazzy 清理机制。Redis 默认是惰性删除 周期删除结合访问 key 时如果发现过期就删除同时后台周期性地抽样清理过期 key。如果大量 key 设置了同一个过期时间周期清理可能来不及处理导致过期 key 还占着内存等访问时才发现过期。这也是我建议过期时间加随机分散的原因之一不止是防雪崩还能缓解过期 key 清理的压力。提示内存监控不能只看used_memory还要关注maxmemory之下是否频繁触发驱逐。如果 Redis 日志里经常出现evicted_keys增长说明内存已经不健康了要么扩容要么优化 key 的大小。我之前见过一个团队把一个大对象序列化后直接存 Redis一个 key 就占了几 MB导致maxmemory很快打满排查时看到缓存命中率其实很高但内存全部被那几百个大 key 吃掉了。这里也顺带提一个容易被忽略的点key 本身要设计得短而规范。比如user:123456:detail:v3这种 key 虽然可读性好但比u:123456:d:3多占不少内存。当 key 数量达到千万级别时key 命名的长短对内存的影响就很明显了。我曾经把一个业务模块的 key 从长命名改成短命名Redis 内存占用降了 15% 左右效果相当可观。6. 缓存预热与监控别等上线后才拍脑袋6.1 缓存预热策略与启动加载缓存预热是指系统上线前或大促开始前把预计会被高频访问的数据提前写入缓存。预热做得好系统启动后就能直接命中缓存避免冷启动阶段大量请求打到数据库。预热的做法并不复杂重点是判断哪些数据需要预热。我的做法是结合业务特征和数据统计两步走。首先列出一批核心接口涉及的数据范围比如商品详情、用户信息、配置参数。其次根据历史访问日志统计出 Top N 热点数据把这些数据优先预热。预热任务一般放在应用启动后的异步线程里执行或者单独跑一个离线任务。如果是多节点部署要注意重复预热问题。每个节点启动都去拉全量热点数据写缓存会在短时间内产生大量重复的数据库查询和批量写入。我一般会加一个分布式锁保证同一时间只有一个节点在预热其他节点等预热完成后再提供服务。如果是大促场景预热通常在活动开始前 1 小时到半小时执行。预热的 key 要覆盖两类一类是已知的固定热点比如主推商品另一类是可能的潜在热点比如加购量突然飙升的商品。潜在热点的获取可以依赖实时热点分析系统把每个商品的访问量做滑动窗口统计达到阈值自动加入预热队列。这个方案实现起来成本略高但对大促这类极端场景几乎是必需的。预热之后还要考虑缓存数据的过期时间设计。如果过期的时点恰好是大促高峰期那预热的效果就大打折扣了。所以预热数据的过期时间要避开高峰或设置得足够长大促期间尽量不失效。6.2 命中率监控与调优闭环缓存用了不等于用好了必须通过监控数据不断调优。最重要的指标是缓存命中率。命中率高说明缓存发挥了作用命中率低说明缓存的数据跟实际访问的匹配度差需要调整缓存策略。Redis 本身提供了info命令查看命中率比如keyspace_hits和keyspace_misses。我习惯在监控系统里把这两个指标拉出来算出一个滑动窗口内的命中率按业务模块分别统计。如果发现某个模块命中率低于 80%我就会去分析几个原因key 设计是否合理、过期时间是否太短、是否存在大量无效查询、预热是否覆盖了真正的热点。围绕命中率调优常见手段包括延长过期时间、调整淘汰策略、增加二级缓存、优化数据访问模式。调优过程不能一次性改太多参数要把改动和线上效果对应起来用数据说话。我通常的做法是每次只改一个变量观察一天到两天的命中率变化再决定下一步。还有一个指标容易被忽略缓存 key 的数量与内存使用率。如果 key 数量长期增长、内存使用率接近maxmemory说明缓存中存在大量不被访问的“僵尸 key”。此时可以引入 key 的定期体检任务统计每个 key 的最后访问时间把长时间没被访问的 key 主动清理掉。Redis 扫描全部 key 用scan命令千万不要用keys *会阻塞 Redis。注意不管命中率多高缓存都是性能优化手段不是数据持久层。要定期演练缓存宕机的应急预案确认降级逻辑真的能兜住。有些团队线上 Redis 一直很健康结果内存不足引发一次节点迁移缓存短暂不可用数据库直接被流量打挂就是因为降级预案只写在文档里从没验证过。关于缓存很多经典问题从概念上看并不复杂但真正处理好需要结合业务场景做大量取舍。穿透、击穿、雪崩、一致性、淘汰策略、预热监控每一个都是一套组合拳没有任何单一方案能解决所有问题。我在实际项目里最基本的准则就一条每加一层缓存都要想清楚它万一失效了会怎样。想清楚这个再回头设计缓存方案很多坑都能提前避开。