排查缓存问题我一般就靠慢查询、热 Key 分析和命中率曲线这三板斧,基本能把事故现场还原出来

缓存的本质是把钱花在刀刃上——用空间换时间。但真出了事故,靠瞎猜和重启服务混过去,迟早会在同一个坑里摔第二次。我处理线上缓存故障时,不看什么监控大盘,那些花花绿绿的图表在定位根因时帮不上忙。真正管用的就三招:慢查询日志、热 Key 分布分析和命中率波动曲线。这三板斧轮一遍,95% 的缓存事故都能还原出完整的事故现场。

慢查询日志:别只看平均值,要看拖后腿的尾巴

Redis 的单线程模型决定了,一个慢查询能拖垮整个实例。我查过最离谱的一个案例,某电商大促期间 Redis 平均延迟才 15ms,看上去毫无问题,但 p999 飙到了 800ms。原因是一条 KEYS order:2023* 在生产环境跑了 3 秒,把后面排队的所有请求都堵死了。

排查这类问题,第一步永远是打开慢查询日志。Redis 从 2.2.13 版本就支持了 SLOWLOG 命令,默认记录执行时间超过 10ms 的命令。但在高并发场景下,10ms 这个阈值太宽松了——我通常改成 5ms,能抓到更多有问题的操作。

# 设置慢查询阈值为 5000 微秒(5ms)
CONFIG SET slowlog-log-slower-than 5000

# 保留最近 1024 条记录
CONFIG SET slowlog-max-len 1024

# 查看最近的慢查询
SLOWLOG GET 20

看慢查询日志不是简单地扫一眼命令就完事。我习惯按三个维度分析:命令类型Key 的命名模式时间分布KEYSFLUSHALLFLUSHDB 这种 O(N) 级别的命令一旦出现,直接定性为代码缺陷,没有例外。如果某段时间集中出现 DEL large_hash_key 这类操作,大概率是有人在批量清理数据,同时把主线程卡住了。

还有个容易被忽略的点:SLOWLOG 记录的是命令执行时间,不包括网络 I/O 和排队时间。如果你看到慢查询记录里的耗时都在 50ms 以内,但客户端报的超时却有 200ms,那问题不在命令本身,而在 CPU 争抢或者网络层面。这时候得配合 redis-cli --latency 和系统层面的 perf top 一起看。

热 Key 分析:不是所有热点都能靠加内存解决

热 Key 问题是缓存架构设计的大忌。我见过一个社区团购系统,每天 20 点准时卡死,查了半天才发现是秒杀商品的库存缓存 Key 被 12 个服务实例同时读取,单个 Key 的 QPS 达到了 18 万次/秒。Redis 单线程再快也扛不住这个量级,CPU 直接打满。

排查热 Key 最直接的工具是 Redis 4.0 引入的 --hotkeys 选项。用法很简单:

redis-cli --hotkeys

这个命令会扫描整个实例的 Key 空间,统计每个 Key 的访问频率。但它有个致命缺陷:扫描过程本身会消耗大量 CPU,生产环境慎用。我的做法是先通过 monitor 或者客户端埋点缩小怀疑范围,再用 --hotkeys 做确认。

更安全的方案是在业务代码里埋点。用 Guava 的 AtomicLong 计数器或者直接打日志,按 Key 聚合访问次数:

// 简易版热 Key 统计埋点
private ConcurrentHashMap<String, AtomicLong> hotKeyCounter = new ConcurrentHashMap<>();

public Object getFromCache(String key) {
    hotKeyCounter.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
    // 每隔 10 秒输出一次 Top 20
    return redisTemplate.opsForValue().get(key);
}

发现热 Key 之后,处理策略不能一刀切。读多写少的配置类热 Key,拆成多个副本分散到不同 Redis 节点就行,比如 config:shard0config:shard1,客户端随机选一个读。但如果是频繁更新的库存类热 Key,拆副本会导致数据不一致,这时候得改用本地缓存 + 消息队列异步更新的方案。

最容易被误判的是"伪热 Key"——某个 Key 的 QPS 看着高,但实际上是因为上游服务在死循环重试。2021 年某支付系统故障,运维团队盯着一堆热 Key 分析了好几个小时,最后发现根因是网关层的超时配置从 200ms 被改成了 20ms,导致大量请求还没等到 Redis 返回就超时重试,自己把自己打成了热点。这种情况下,热 Key 是症状而不是病因。

命中率曲线:单点数值没意义,要看突变和趋势

很多团队的监控大盘上画着缓存命中率,但只看一个 98.5% 的平均值,跟没看一样。命中率得拉长时间轴看曲线,重点盯两个信号:断崖式下跌缓慢下降

断崖式下跌,比如从 98% 瞬间掉到 40%,只有三种可能:

  1. 缓存服务器被重启或清空,所有 Key 同时过期;
  2. 缓存容量触顶,新的写入触发了大规模的 LRU/LFU 淘汰;
  3. 某个大 Key 设置了相同的过期时间,导致缓存雪崩。

第三种情况我碰到过两次,都是因为产品经理要求"每日零点刷新数据",开发团队就简单粗暴地把所有缓存 Key 的过期时间设成了当天的 23:59:59。零点一到,几万个 Key 同时过期,数据库瞬间被打穿。这种问题查命中率曲线一眼就能定位——曲线在零点准时出现垂直断崖。

缓慢下降更隐蔽。通常是内存碎片化导致有效容量缩水,或者业务缓慢增长,缓存空间不够用了。Redis 的 INFO memory 命令可以看内存碎片率:

redis-cli INFO memory | grep mem_fragmentation_ratio

碎片率大于 1.5 说明碎片化严重,重启或者启用 Redis 4.0 的 activedefrag 能缓解。但如果碎片率正常,命中率还是持续走低,那就是单纯的容量不够,该扩容了。

分析命中率时还要区分整体命中率核心业务命中率。全局命中率 99%,但支付接口的命中率只有 70%,这说明缓存策略对不同业务线差别太大。我一般要求核心链路的缓存命中率维持在 99% 以上,非核心接口 95% 就行,一条曲线合并看会掩盖很多问题。

事故现场还原的标准流程

真出了事故,我按固定顺序走:

第一步,拉慢查询日志,看有没有 O(N) 命令或者异常的耗时操作。这一步能排除掉 40% 的"程序写得有问题"类型故障。

第二步,如果没有明显的慢查询,立刻看命中率曲线。找断崖点的时间戳,跟发布记录、定时任务和流量峰值做比对。能对上,基本就定位了。

第三步,如果命中率也没异常,但客户端还是超时,问题多半在热 Key 或者网络。用 --hotkeys 或者业务埋点找出热点 Key,同时用 redis-cli --latency 检查客户端到 Redis 的网络延迟。

这三步走完,事故现场的还原程度能到 95%。剩下 5% 的诡异问题,比如内核参数 vm.overcommit_memory 被改导致 fork 失败、AOF 重写期间 I/O 打满磁盘,这些属于操作系统层面的坑,需要配合系统监控工具排查。

常见问题

慢查询日志里都是 GET 命令,耗时也不高,但客户端就是超时,怎么回事?

这种情况问题不在 Redis 内部,而是网络或者客户端连接池。用 redis-cli --latency 从应用服务器所在机器测试延迟,如果延迟超过 50ms,检查网络带宽是否被打满,或者是否有交换机丢包。另外,Jedis 连接池的 maxTotal 设得太小也会导致线程等待连接超时,但这个等待时间不会体现在 Redis 的 slowlog 里。

热 Key 分析出来的是一个 List,QPS 也不高,为什么 CPU 还是 100%?

不是 QPS 高才会热,数据量大也算热。一个 List 里存了 10 万条记录,每次 LRANGE key 0 -1 会把整个列表序列化返回,这个操作 O(N) 且消耗大量 CPU 和网络带宽。用 MEMORY USAGE key 看看实际占用内存,超过 1MB 的 Key 都应该考虑拆分或者改用其他数据结构。

命中率曲线没有断崖,但一直在 95% 到 98% 之间抖动,需要关注吗?

如果是周期性抖动,比如每 5 分钟掉一次,大概率是定时任务批量更新缓存导致的短暂失效。问题不大,但可以优化为懒加载或者异步刷新。如果是随机抖动且幅度超过 5%,说明缓存容量紧张,某个 Key 被淘汰后很快又被加载回来,形成"缓存颠簸"。这时候看 INFO stats 里的 evicted_keys 指标,如果一直在增长,就该扩容了。

Redis 6.0 之后的 CLIENT TRACKING 能替代热 Key 分析吗?

不能。Client Side Caching 解决的是减少网络交互、降低延迟的问题,它跟踪的是"哪些 Key 被修改了",用来主动失效客户端本地缓存。热 Key 分析关注的是"哪些 Key 访问量太大",两者维度不同。不过可以结合使用:先用热 Key 分析找出高访问 Key,再针对这些 Key 启用 Client Side Caching 降低 Redis 压力。