
当命中率从 95% 骤降到 30%如何在混乱中建立排查节奏找到真正的根因。周五晚上九点监控群突然炸开。生产环境 Redis 命中率在五分钟之内从95% 跌到 30%下游数据库连接数暴涨CPU 飙到 90%服务响应时间从 20ms 爬到 2s。值班同学的第一反应是扩容但加完 Redis 节点后命中率纹丝不动。这不是我第一次遇到 Redis 大规模 miss但每一次都像是在迷雾里找出口。经过多年踩坑我总结出一套分层排查的方法论。这篇文章不会罗列命令手册而是分享一种在混乱中建立排查节奏的思路帮助你从现象出发逐层逼近根因。一、先建立排查框架别急着动手很多人看到命中率暴跌第一反应是redis-cli --bigkeys或者重启实例。这种散弹枪式的排查效率极低。更合理的方式是从数据流的全链路视角切入把问题定位拆成三个层次Redis 命中率暴跌 ├── 第一层应用层 │ ├── 连接池耗尽 │ ├── 超时配置不合理 │ ├── 代码逻辑变更 │ └── 缓存 key 设计缺陷 │ ├── 第二层Redis 服务端 │ ├── 内存逐出 │ ├── 大 key / 热 key │ ├── 持久化阻塞 │ └── 主从同步延迟 │ └── 第三层网络与基础设施 ├── 网络抖动/丢包 ├── 容器资源限制 ├── DNS 解析异常 └── 中间件代理故障这个框架的核心是自上而下、由近及远。应用层离你最近最容易验证应该优先排查只有当应用层被排除后才需要深入 Redis 服务端和基础设施。很多新手反其道而行一开始就抓 Redis 的慢日志结果折腾两小时发现是应用发布时把超时时间配错了。关键原则在拿到任何监控数据之前先问三个问题最近有没有发布有没有配置变更流量有没有突增这三个问题的答案能帮你排除 50% 以上的场景。二、第一层应用层排查应用层是 Redis 的调用方也是问题最高发的环节。这里我总结出四个高频根因。2.1 连接池耗尽与等待队列堆积当并发突增时如果连接池大小设置偏小新的请求会在连接池队列里等待。等待时间超过超时阈值后客户端会直接抛异常或降级走数据库表现为 “Redis miss”。但实际上 Redis 服务端根本没有收到这些请求。排查方式很简单。以 Lettuce 为例观察连接池监控# 通过 Micrometer 暴露的指标 redis_connection_pool_active{pooluserCache} 48 redis_connection_pool_idle{pooluserCache} 2 redis_connection_pool_pending{pooluserCache} 127如果pending持续大于 0说明连接池已经成为瓶颈。但注意扩连接池不是万能药。每个连接在 Redis 服务端都会占用内存和文件描述符盲目扩大会把压力转嫁到服务端。更合理的做法是先确认连接池配置是否合理通常公式是核心线程数 × 单节点连接数再考虑增加 Redis 分片。2.2 超时配置的隐性陷阱我见过最隐蔽的一次事故是某次发布把 Lettuce 的timeout从 3s 改成了 200ms。发布初期一切正常直到晚高峰流量上来网络延迟波动到 50ms 以上大量请求在 200ms 内拿不到响应就被判定为失败缓存逻辑直接跳过请求击穿到数据库。更危险的是某些客户端的默认行为。比如 Jedis 在连接超时后部分版本不会把异常抛给业务层而是静默返回null业务代码把这个null当成 “缓存不存在”于是每次都走数据库查询。// 错误的兜底逻辑try{valueredis.get(key);}catch(Exceptione){// 超时后也返回 null调用方无法区分valuenull;}if(valuenull){valuedb.query(key);// 缓存穿透}正确的做法是把异常和空值分开处理超时异常应该记录日志并触发告警而不是当成 miss 处理。2.3 缓存 key 的设计缺陷某次业务迭代后缓存 key 从user:12345变成了user:v2:12345。但清理逻辑里还沿用旧前缀匹配导致大量旧 key 堆积新 key 又没预热。结果是Redis 内存满了逐出策略开始工作新写入的 key 很快被踢掉命中率雪崩。这类问题的排查要关注两个指标keyspace_hits/keyspace_misses的比率变化evicted_keys的增长速率如果 evicted_keys 在命中率下降的同时明显上升说明内存已经吃紧需要检查 key 的生命周期管理是否合理。2.4 批量操作引发的连锁反应有些业务为了降低网络往返会用MGET一次性拉取上百个 key。这本身没问题但如果其中某个 key 对应的数据结构很大比如一个 Hash 里有几十万个 field整个 MGET 的响应时间会急剧拉长拖慢同批次里的其他 key导致这些 key 被客户端判定为超时 miss。定位这类问题需要把监控粒度拆到命令级别。Redis 的SLOWLOG和latency-monitor能帮你找到耗时异常的命令127.0.0.1:6379 SLOWLOG GET 10 1) 1) (integer) 12345 2) (integer) 1721123400 3) (integer) 5200000 4) 1) MGET 2) user:10001 3) user:10002 ...上面的5200000表示这个命令耗时 5.2 秒。当你看到 MGET 的耗时不正常地高就要怀疑里面混入了大 key。三、第二层Redis 服务端排查应用层排查无果后把视线转向 Redis 服务端。服务端的问题通常更隐蔽但对全链路的破坏力也更大。3.1 内存逐出最经典的雪崩起点Redis 的内存上限由maxmemory控制超过后根据maxmemory-policy决定如何释放空间。如果策略配置为allkeys-lru或allkeys-random当内存满了之后每次新写入都会触发逐出而逐出操作是同步阻塞的。在极端情况下逐出会演变成一场灾难。比如内存使用率长期维持在 99%此时突然有一批大 key 写入Redis 需要在短时间内逐出大量旧 key 来腾出空间。这个逐出过程会阻塞主线程导致正常的读写请求被延迟客户端超时后判定为 miss。排查内存问题的标准动作INFO memory确认used_memory和maxmemory的关系INFO stats观察evicted_keys是否在持续增长用redis-cli --bigkeys或MEMORY USAGE key定位内存大户注意很多人看到内存满了就直接扩容但如果根本原因是 key 没有设置过期时间扩容只是延缓问题。建议配合TTL扫描找出那些 “永久存活” 的 key让业务方确认是否可以清理。3.2 大 key 与热 key两个截然相反的问题大 key 和热 key 经常被混为一谈但它们的成因和解决方案完全不同。维度大 key热 key定义单个 key 的 value 体积过大单个 key 被高频访问危害阻塞主线程、占用带宽、序列化耗时单节点 CPU 飙高、连接打满发现方式--bigkeys、MEMORY USAGEhotkeys参数、访问频率统计解决思路拆分数据结构、压缩、本地缓存本地缓存、读写分离、key 拆分对于热 key一个实用的 trick 是在 key 后面加随机后缀做副本拆分。比如把config:global拆成config:global:0到config:global:9客户端按哈希取模分散读取。这样能把单点 QPS 降到原来的 1/10。3.3 持久化与主从同步的阻塞效应如果 Redis 开启了 AOF 的always策略或者正在执行BGSAVE/BGREWRITEAOF主线程会被 fork 出的子进程影响。在内存数据量较大的实例上fork 操作本身就可能耗时数百毫秒期间主线程无法处理新请求。排查这类问题需要关注INFO persistence里的几个关键指标rdb_last_bgsave_status上次持久化是否成功aof_last_rewrite_duration_secAOF 重写耗时master_last_io_seconds_ago主从同步延迟一个经验值是如果master_last_io_seconds_ago持续大于 10 秒说明主从同步已经出现明显滞后从节点的数据已经不新鲜读请求落到从节点上等同于 miss。四、第三层网络与基础设施当应用层和服务端都被排除后问题往往藏在网络层或基础设施里。这一层的问题最难定位因为指标分散在不同的监控系统中。4.1 网络抖动与丢包Redis 基于 TCP对网络延迟非常敏感。一次简单的 GET 请求客户端到服务端的 RTT 通常在 0.5ms 以内。但如果网络出现抖动RTT 突然跳到 50ms 以上那些超时阈值设置偏低的客户端就会大面积抛异常。排查网络问题不要只看应用层的超时日志要抓更低层的指标容器/虚机层面的tcp_retransmits重传率网卡层面的rx_errors、tx_dropped中间网络设备的流量突增告警如果 Redis 部署在 Kubernetes 里还要检查 CNI 插件的 iptables 规则是否过于复杂。某次事故中我们发现一个节点上的 conntrack 表满了导致新连接被随机丢弃表现就是 Redis 间歇性不可达。4.2 容器资源限制容器化环境中的资源限制cgroups是另一个隐形杀手。Redis 的 CPU 使用率看起来只有 50%但如果容器被限制了 2 核而 Redis 是单线程模型实际上它已经把一个核心跑满了。剩余的 “空闲” CPU 只是其他进程没有用完的配额。更隐蔽的是内存限制。如果容器设置了memory.limit_in_bytes而 Redis 的maxmemory配置得比容器限制还大Redis 以为还有空间但操作系统已经开始 OOM kill 或者 swap性能断崖式下跌。# 检查容器实际的 cgroup 限制 cat /sys/fs/cgroup/memory/memory.limit_in_bytes cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us cat /sys/fs/cgroup/cpu/cpu.cfs_period_us4.3 DNS 与中间件代理如果使用 Twemproxy、Codis 或者 Envoy 作为 Redis 代理代理层本身也可能成为瓶颈。代理的转发逻辑、连接池管理、健康检查机制任何一环出问题都会导致 “Redis 不可用” 的假象。一个典型的场景是代理的健康检查间隔为 5 秒但某台 Redis 实例因为持久化阻塞了 8 秒代理把它标记为不可用把所有流量切到了剩余节点上引发连锁反应。等 Redis 恢复后代理又需要一段时间才能重新把它加回池子。五、实战一个完整的定位时间线回到文章开头的那个周五晚上。以下是真实的排查时间线展示了如何结合上述框架逐步缩小范围。21:05 监控告警命中率 95% → 30% 21:07 确认无发布无配置变更流量正常 21:10 应用层检查连接池 pending 0超时配置正常 21:15 INFO memory → used_memory 7.8GB, maxmemory 8GB 21:18 INFO stats → evicted_keys 每分钟增长 12k 21:22 --bigkeys 发现 hash:user:tags 占用 1.2GB 21:25 业务确认该 key 为全量用户标签聚合本不该进缓存 21:30 紧急清理大 key命中率回升至 88% 21:45 补充本地缓存熔断命中率恢复到 95%这次事故的根本原因是某次需求上线时开发同学把 “全量用户标签” 这个巨大的 Hash 写入了 Redis而 key 没有设置过期时间。Redis 内存被撑到临界值后LRU 逐出开始高频工作新业务写入的 key 把老业务的 key 大量踢出引发命中率雪崩。这里的教训是监控 evicted_keys 比监控命中率更重要。命中率下降是结果evicted_keys 上升才是原因。如果告警里加了 evicted_keys 的阈值这个问题可能在 21:05 之前就被发现了。六、监控与预防体系事后救火不如事前防火。一个健壮的 Redis 监控体系应该覆盖以下维度。6.1 必加的告警项指标告警阈值意义命中率 80% 持续 2min缓存失效或穿透evicted_keys 增长 1000/min内存不足逐出频繁used_memory / maxmemory 85%内存即将耗尽连接数 maxclients × 80%连接泄漏或攻击慢查询数量 10/min大 key 或复杂命令主从延迟 10s同步阻塞或网络问题CPU 使用率 80% 持续 2min热 key 或复杂计算6.2 代码层面的防御除了监控代码里也要埋好防御性逻辑// 1. 缓存空值防止缓存穿透Stringvalueredis.get(key);if(valuenull){valuedb.query(key);if(valuenull){// 空值也缓存TTL 设短一些redis.setex(key,60,__NULL__);}else{redis.setex(key,3600,value);}}// 2. 加随机抖动防止缓存雪崩intttl3600ThreadLocalRandom.current().nextInt(300);redis.setex(key,ttl,value);// 3. 异步预热 熔断降级if(circuitBreaker.isOpen()){returnfallback.get(key);}6.3 架构层面的兜底本地缓存Caffeine/Guava作为 Redis 的 L1 缓存即使 Redis 短暂不可用也能扛住一部分流量。熔断降级Sentinel/Hystrix当 Redis 超时率超过阈值时自动切断对 Redis 的访问直接走降级逻辑防止把数据库也打挂。读写分离热 key 的读操作分散到多个从节点减轻主节点压力。七、总结Redis 命中率突降从来不是单一原因造成的。它可能是应用层的配置失误可能是服务端内存管理的漏洞也可能是网络层的一次短暂抖动。建立分层排查的框架比记住十个命令更有价值。最后分享三个我反复验证过的经验先问变更再问指标。70% 的故障与最近的发布或配置变更有关确认这一点能帮你省去大量无意义的排查。evicted_keys 是比命中率更前置的指标。命中率下降是结果内存逐出才是根因。把 evicted_keys 加入核心告警能让你提前发现问题。客户端超时不等于服务端慢。网络抖动、连接池耗尽、代理故障都会表现为 “Redis 慢”但根因可能在离 Redis 很远的地方。缓存是高性能系统的基石但也是最容易被忽视的灰色地带。希望这篇文章能帮你在下一次 Redis 告警响起时少一分慌乱多一分从容。