Redis 反模式与排查宝典

4 阅读32分钟

概述

本系列从 Redis 特性全景出发,逐层深入到线程模型、内部编码、持久化、高可用、缓存设计、分布式锁、Stream 消息队列、安全与 ACL、Redis Stack 扩展、运维监控。然而,知晓原理不等于能在生产环境正确运用——反模式往往藏于一个默认的 maxmemory-policy noeviction、一个未被注意的 KEYS * 命令、一个因网络分区而发生的脑裂。本文作为系列反模式排查的核心篇章,将前面的所有核心技术点投射到真实的故障场景中,通过“反模式→排查→修复”的闭环,帮助读者将分散的知识点内化为系统化的排障直觉。

“Redis 内存突然飙满”“缓存命中率从 99% 跌到 50%”“主从断开后全量同步拖垮主库”“一条 KEYS * 让 Redis CPU 飙到 100%”——这些 Redis 相关的线上故障,根因往往不是某个技术本身有问题,而是使用方式违背了它的设计本意。本文将 Redis 生态中最常见的反模式归纳为五大领域,以 22 个以上的真实案例为载体,严格遵循“错误示例→现象描述→排查思路→根因分析→修正方案→最佳实践”的六步诊断法,并整合全系列诊断工具与标准化排查决策树,为读者提供一套可复用的、逻辑严密的排障体系。

核心要点:

  • 五大反模式领域(设计+运行时):数据结构、缓存、持久化、高可用、运维安全,共 22+ 案例。
  • 六步诊断法:从错误代码到修复的标准化流程,每个案例根因显式关联前文原理。
  • 诊断工具集与决策树:覆盖 Redis 端全工具链与四大典型故障的精确决策路径。

文章组织架构图

flowchart LR
    A["Redis 反模式与排查宝典"] --> B1["1. 数据结构反模式"]
    A --> B2["2. 缓存反模式"]
    A --> B3["3. 持久化反模式"]
    A --> B4["4. 高可用反模式"]
    A --> B5["5. 运维与安全反模式"]
    B1 --> B1a["设计反模式: 大Key String 阻塞/集合编码升级/ZSet span CPU"]
    B1 --> B1b["运行时反模式: KEYS */SMEMBERS/SORT/SINTER SUNION"]
    B2 --> B2a["设计反模式: noeviction 拒绝写入/缓存预热缺失"]
    B2 --> B2b["运行时反模式: 缓存穿透/击穿/雪崩/@Cacheable unless 空值"]
    B3 --> B3a["设计反模式: 零持久化/always fsync/AOF 重写风暴"]
    B3 --> B3b["运行时反模式: fork 阻塞主线程/AOF 损坏/混合持久化未开启"]
    B4 --> B4a["设计反模式: 哨兵 quorum 不当/Cluster timeout 过小/full-coverage yes 阻塞"]
    B4 --> B4b["运行时反模式: repl-backlog-size 过小/脑裂双主/CLUSTER FAILOVER MOVED 风暴"]
    B5 --> B5a["设计反模式: 无 ACL 空密码/公网暴露 SSH 写入"]
    B5 --> B5b["运行时反模式: maxclients 耗尽/MONITOR 生产运行/CLUSTER MEET 误合并/FLUSHDB 误操作"]
    A --> C["6. 诊断工具集与工具→现象映射表"]
    A --> D["7. 标准化排查决策树"]
    A --> E["8. 面试高频故障排查专题"]

    classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a

架构图说明:

  • 总览说明:全文 8 个模块以前 5 个反模式领域的案例分析为主体,每个案例采用设计/运行时双视角剖析,后接诊断工具集和决策树,最后以面试故障排查专题收束。
  • 逐模块说明:模块 1-5 覆盖 Redis 全生命周期中的典型错误,每个案例根因直接引用前文原理;模块 6 提供可打印的速查工具箱;模块 7 绘制故障决策路径图;模块 8 面试巩固。
  • 关键结论:Redis 反模式的根因往往可追溯到前 11 篇的核心原理。掌握六步诊断法、设计/运行时双视角分析范式、标准化决策树,是将理论转化为排障能力的关键。

1. 数据结构反模式

数据结构是 Redis 性能的基石,错误的使用方式会迅速引发 CPU 瓶颈或 OOM。这里从设计和运行时两个维度剖析五个典型案例。

1.1 设计反模式案例 1:大 Key(String 存储 10MB JSON)未拆分,DEL 阻塞主线程

1.1.1 错误示例

// 将用户的完整画像、行为序列、设置偏好等全部打包成一个巨大的 JSON 字符串
String userProfile = buildLargeProfile(userId); // 序列化后约 10MB
jedis.set("user:profile:" + userId, userProfile);

// 当用户注销时,直接删除
jedis.del("user:profile:" + userId);

1.1.2 现象描述

在业务高峰期,当有用户注销或管理员清理数据时,突然大量客户端出现超时。监控显示 Redis 的 CPU 使用率瞬间从 20% 飙至 100%,持续 1-2 秒。redis-cli --latency 显示瞬时延迟超过 300ms。

执行 SLOWLOG GET 5 得到如下典型输出:

127.0.0.1:6379> SLOWLOG GET 5
1) 1) (integer) 245
   2) (integer) 1715932800
   3) (integer) 405000   # 405ms 执行时间
   4) 1) "DEL"
      2) "user:profile:10082"
   5) "192.168.1.15:48726"
   6) ""

同时 INFO statstotal_commands_processed 的瞬时速率下降,因为主线程被阻塞无法处理新的命令。

1.1.3 排查思路

  1. 初步定位:当出现超时时,第一时间登录 Redis 节点,执行 redis-cli --latency-history 观察延迟峰值,或者 redis-cli --stat 观察实时 QPS 与延迟变化,可以看到周期性或偶发的巨大延迟尖峰。
  2. 慢日志分析:执行 SLOWLOG GET 20,按照耗时降序排列,发现最慢的命令是对特定 user:profile:*DEL 操作,耗时动辄几百毫秒。
  3. Key 大小确认:使用 redis-cli --bigkeys 扫描,可以发现类似 [00.00%] Biggest string found 'user:profile:10082' has 10485760 bytes 的输出。用 MEMORY USAGE user:profile:10082 可以精确计算出该 Key 实际占用的内存(可能略大于 10MB,因为有 SDS 头和分配器开销)。
  4. 关联业务:排查这些大 Key 的访问模式,发现删除操作都是在写请求之后直接发生的,没有做异步化处理。

1.1.4 根因分析

根据本系列第 3 篇对 SDS 和 jemalloc 内存分配器的原理分析:Redis 在删除一个 10MB 的 String Key 时,需要释放其对应的 redisObject、SDS 及底层的 10MB 缓冲区。在 jemalloc 中,释放大内存块(尤其是跨越多个 extent 的大块)需要复杂的合并与归还操作,这个过程可能在几百微秒到几毫秒,但对于 10MB 甚至更大的块,free() 调用可能会触发大量的脏页回写或复杂的伙伴算法重组,导致耗时达到数百毫秒。由于 Redis 主线程是单线程处理所有命令(第 2 篇线程模型),DEL 执行期间将完全阻塞任何后续命令,造成服务整体卡顿。从主从复制角度看,DEL 命令同样会被同步到从库执行,如果从库性能较差,还会引起主从延迟增加。

1.1.5 修正方案

  1. 拆分存储:将大 JSON 打散,按照业务维度存入 Hash 或不同的 String Key。
    // 拆分为多个小 Key
    jedis.hset("user:" + userId + ":basic", "name", name);    // < 1KB
    jedis.hset("user:" + userId + ":orders", "list", orders); // 按时间分片
    
  2. 异步删除:Redis 4.0 引入的 UNLINK 命令可以在后台线程中逐步回收内存。在 7.x 中,UNLINK 非常成熟,它仅从键空间解除 Key,然后由 lazyfree 线程池异步释放内存,主线程几乎无阻塞。
    UNLINK user:profile:10082
    
    也可以全局开启惰性删除 lazyfree-lazy-server-del yes,使得 DEL 在某些情况下自动转为异步。
  3. 应用层改造:如果无法避免大 Key,则必须在代码中将删除操作替换为 UNLINK,并设置更短的超时重试时间。

1.1.6 最佳实践

  • 在架构设计阶段,明文规定单个 Redis Key 的大小上限,例如 String 不超过 10KB,集合成员不超过 5000。
  • redis-cli --bigkeys 加入定期巡检(如每日低峰期),配合 --memkeys 分析内存占用。
  • 监控 mem_fragmentation_ratioused_memory_rss,大 Key 的频繁创建和删除极易产生内存碎片。
  • 对历史大 Key 的清理一律使用 UNLINK,并避开高峰期。

1.2 设计反模式案例 2:Set/Hash/ZSet 未预判数据规模,编码升级触发内存翻倍

1.2.1 错误示例

一个社交应用,使用 Set 存储用户关注列表,初期每个用户只关注几十人,直接使用 SADD 添加。

SADD user:123:follows 1001 1002 ... # 初期只有 30 个关注

当业务发展后,部分大 V 的关注数增加到 5 万以上。

1.2.2 现象描述

某天运维发现 Redis 内存使用率突然从 40% 飙升到 80%,而线上写入流量并没有明显增加。使用 MEMORY USAGE user:123:follows 发现该 Key 占用了 3.2MB,而之前该用户只有 2000 关注时内存占用还很低。执行 OBJECT ENCODING user:123:follows 返回 hashtable 而非 listpack。继续观察其他大 V,发现内存占用远大于预期,整体内存水位暴涨。

1.2.3 排查思路

  1. 初步发现:通过 Prometheus 告警 used_memory_dataset 升高。
  2. 定位问题 Keyredis-cli --memkeysredis-cli --bigkeys 列出内存占用 Top 的 Key,发现大量 user:*:follows 集合。
  3. 检查编码:执行 OBJECT ENCODING 查看这些集合的内部编码,发现均为 hashtable。查看配置 CONFIG GET set-max-listpack-entries,默认值为 128。当集合大小超过 128 时,Redis 自动将内部编码从紧凑的 listpack(或旧版 intset)转换为 hashtable(即 dict)。
  4. 内存计算MEMORY STATS 显示 overhead.hashtable 开销显著增加。

1.2.4 根因分析

本系列第 3 篇深入分析了 Redis 集合的内部编码与转换阈值。以 Set 为例,当满足以下任一条件时,编码会从 listpack 转换为 hashtable

  • 元素数量超过 set-max-listpack-entries(默认 128)
  • 单个元素长度超过 set-max-listpack-value(默认 64 字节)

listpack 是一种紧凑的顺序存储结构,几乎无额外开销;而 hashtable 是标准的 dict 结构,包含哈希表桶、指针数组等,每个元素平均需要额外的 20~30 字节开销。对于 5 万成员的集合,内存占用可能从大约 1MB(listpack)急剧膨胀到 3MB 以上(hashtable),这就是内存翻倍的根因。这种隐式的升级在设计时没有被预估,随着数据增长,整体内存消耗远超规划,可能触发 maxmemory 淘汰甚至 OOM。

1.2.5 修正方案

  1. 调整阈值:若业务明确需要大集合,可以适当调高阈值,避免自动转换。

    set-max-listpack-entries 100000
    set-max-listpack-value 128
    

    需权衡:listpack 在大型集合上的读写操作都是 O(N),性能会下降,适合数据量在阈值以下且无频繁更新。对于 5 万成员且频繁 SADD/SREMlistpack 可能引发性能问题。

  2. 分片拆解:更稳妥的方案是进行应用层分片。将一个大 Set 拆成多个小 Set,使每个分片的元素数保持在阈值以下,利用 listpack 节省内存。

    int shardId = userId % 10;
    jedis.sadd("user:follows:" + shardId, userId);
    

    查询时并行查询所有分片。

1.2.6 最佳实践

  • 在系统设计阶段,预估集合的最大可能规模,并据此调整 *-max-listpack-entries 参数,或采用分片设计。
  • OBJECT ENCODING 检查纳入日常巡检,重点监控编码变更为 hashtable 的 Key 数量。
  • 对于确实很大的集合,直接接受 hashtable 编码并预留足够内存,或使用 ZSet 等支持更高效数据结构。

1.3 设计反模式案例 3:Sorted Set 频繁 ZRANK/ZREVRANGE,skiplist span 重计算 CPU 飙高

1.3.1 错误示例

在一个实时排行榜场景中,每次玩家分数变化都会执行:

redis.zadd("leaderboard", score, playerId);           // 更新分数
long rank = redis.zrank("leaderboard", playerId);     // 查询排名
Set<String> top10 = redis.zrevrange("leaderboard", 0, 9); // 刷新前10

在数千并发写入下,以上操作频繁执行。

1.3.2 现象描述

监控显示 Redis 的 CPU 使用率持续维持在 90% 以上,并且 instantaneous_ops_per_sec 虽然在 1 万左右,但主要是 zadd, zrank, zrevrange 类命令。通过 SLOWLOG GET 20 发现大量的 ZADDZRANK 耗时在 5ms~10ms。执行 LATENCY DOCTOR 输出类似:

I have a few advices for you:
- Your instance suffers from high CPU load. The tracking of latencies shows some high spikes...
- Check your slowlog for slow commands: ZADD, ZRANK

1.3.3 排查思路

  1. CPU 确认INFO cpu 显示 used_cpu_sysused_cpu_user 很高,并且与命令处理相关。
  2. 慢命令分析SLOWLOG GET 50 过滤出 ZADDZRANK,并观察其执行耗时与请求量的关系。
  3. 热点 Key 识别redis-cli --hotkeys 或通过 OBJECT IDLETIME 辅助发现 leaderboard 是绝对热点。
  4. 原理映射:根据第 3 篇对 skiplist 跳表的实现剖析,我们知道每个节点维护 span 字段用于快速计算排名。每次插入或更新分数时,Redis 需要重新计算插入路径上所有节点的 span,这是一个局部 O(logN) 但消耗 CPU 的操作。高频率的更新导致跳表的 span 不断被重算,同时 ZRANK 也需要通过累加 span 来获取排名,二者叠加造成 CPU 瓶颈。

1.3.4 根因分析

详细根因见第 3 篇对 zset 内部编码的源码分析。Redis 中 zsetskiplistdict 共同实现。跳表节点结构如下(简化):

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;  // 跨度
    } level[];
} zskiplistNode;

span 表示从当前节点到下一个节点跨越的元素数量。ZADD 在插入新节点或更新分数(实际是删除再插入)时,必须更新从插入路径最顶层到最底层所有经过节点的 span,以保持排名信息的正确。ZRANK 通过累加底层前向指针的 span 获取排名,读操作虽然不修改,但频繁的写会使 CPU 缓存频繁失效。高并发写入同一个跳表时,CPU 几乎都在处理 span 的更新和查找。这是典型的热点 Key 高写场景下的性能瓶颈。

1.3.5 修正方案

  1. 异步聚合写入:将分数变化先缓存到本地队列,每 100ms 批量合并后再更新 Redis,减少直接写入频次。
  2. 本地缓存排行榜:在应用实例内存中维护一份 Top N 榜单,每 1 秒从 Redis 异步刷新一次,读请求直接走本地缓存,不再频繁调用 ZREVRANGE
  3. 拆分排行榜:将大榜单按赛区、时间段等因素拆分为多个小榜单,减少单 Key 的写并发。
  4. 使用其他结构:如果业务只是需要近似排名,可考虑 Redis Stack 的 BloomTop-K 功能,或者使用 Stream 结合离线计算。

1.3.6 最佳实践

  • 高并发排行榜必须进行写聚合和读缓存,绝不能让 Redis 直接面对每次的单个写入与实时查询。
  • 利用 LATENCYSLOWLOG 持续监控 zset 相关命令的耗时。
  • 提前评估业务的写入 QPS,单 Key 的 ZADD 极限通常不超过 5000/s(视机器性能),否则应考虑拆分。

1.4 运行时反模式案例 1:KEYS * 全量扫描导致 CPU 飙高与请求阻塞

1.4.1 错误示例

运维团队为了方便,在清理过期会话时使用了脚本:

#!/bin/bash
# 错误用法:直接 KEYS 扫描然后删除
redis-cli KEYS "session:*" | xargs redis-cli DEL

1.4.2 现象描述

当该脚本在线上 Redis 执行时,Redis 的 CPU 立即飙升至 100%,所有客户端请求超时。SLOWLOG 记录如下:

1) 1) (integer) 2
   2) (integer) 1715934000
   3) (integer) 4500000  # 4.5秒
   4) 1) "KEYS"
      2) "session:*"

redis-cli --stat 观察到此时 QPS 骤降为 0,直至 KEYS 执行完成。

1.4.3 排查思路

  1. 快速定位:通过监控告警发现 CPU 飙升,立即登录服务器执行 redis-cli SLOWLOG GET 1 捕获到 KEYS 命令。
  2. 确认影响INFO commandstats 显示 cmdstat_keys:calls=1,usec=4500000,usec_per_call=4500000
  3. 原理回溯:根据第 2 篇 Redis 线程模型,单线程顺序执行命令。KEYS 通过遍历整个键空间 dict 来匹配模式,复杂度 O(N)。在百万级 Key 的数据库中,遍历耗时可达到数秒,期间主线程完全阻塞,无法响应任何其他请求。

1.4.4 根因分析

KEYS 的实现在 src/db.c 中,它会获取整个 server.db->dict 的迭代器,遍历所有 Key 并逐一进行模式匹配(stringmatchlen)。这个过程不进行分片,在主线程中一次性完成。因此数据库 Key 数量越多,阻塞时间线性增长。这严重违背了 Redis 高性能单线程的设计原则。

1.4.5 修正方案

使用 SCAN 命令迭代,每次只返回一小批 Key,不会长时间阻塞主线程。

redis-cli --scan --pattern "session:*" | while read line
do
  redis-cli UNLINK "$line"  # 进一步使用 UNLINK 防止删除阻塞
done

另外,可以在配置文件中直接禁用 KEYS 命令:

rename-command KEYS ""

1.4.6 最佳实践

  • 在所有的运维脚本中禁止 KEYS,统一使用 SCAN 族命令。
  • 对所有 Redis 实例设置 rename-command KEYS "",从根源上杜绝误用。
  • 培训开发与运维,强调 KEYS 的危害。

1.5 运行时反模式案例 2:SMEMBERS 遍历大集合导致 OOM 与网络风暴

1.5.1 错误示例

// 获取当前在线用户列表,集合成员200万
Set<String> onlineUsers = jedis.smembers("online:users");

1.5.2 现象描述

执行该代码的应用服务器内存瞬间暴涨,并可能触发 Full GC 甚至 OOM。Redis 出口网络带宽打满(假设 200 万用户 ID 每个 20 字节,约 40MB 数据),传输期间 Redis 主线程也忙于发送数据,其他客户端响应变慢。SLOWLOG 中记录 SMEMBERS 耗时可能达到数秒。

1.5.3 排查思路

  1. 应用监控:发现某台机器堆内存陡增,dump 堆文件分析,发现大量反序列化的字符串。
  2. Redis 慢日志SLOWLOG GET 10 捕获到 SMEMBERS online:users
  3. Key 大小检查MEMORY USAGE online:users 确认该 Set 占用数百 MB。
  4. 原因SMEMBERS 一次性返回集合所有元素,没有采用分页机制。

1.5.4 根因分析

SMEMBERS 命令内部会遍历 Set 并将所有成员序列化返回给客户端。对于大集合,这会导致大量的内存分配和网络 IO。同时,由于 Redis 单线程处理网络输出,大包的发送会阻塞其他命令的响应(除非使用了 I/O 多线程,但命令执行仍单线程)。客户端一次性接收大量数据,易导致应用内存溢出。

1.5.5 修正方案

使用 SSCAN 进行分页获取:

String cursor = "0";
ScanParams params = new ScanParams().count(1000);
do {
    ScanResult<String> result = jedis.sscan("online:users", cursor, params);
    process(result.getResult());
    cursor = result.getCursor();
} while (!"0".equals(cursor));

1.5.6 最佳实践

  • 对集合类 Key 的遍历一律使用 SCAN 家族命令,并对客户端内存进行压力测试。
  • 如果只是需要判断某个元素是否存在,使用 SISMEMBER;需要随机获取使用 SRANDMEMBER
  • 大型集合应通过分片存储,避免单 Key 过大。

1.6 运行时反模式案例 3:SINTER/SUNION 对大集合求交集/并集耗时过长

1.6.1 错误示例

SINTER set_a set_b  # set_a 和 set_b 各有500万成员

1.6.2 现象描述

该命令执行耗时超过 10 秒,CPU 单核 100%,其他命令被严重阻塞。

1.6.3 排查思路

SLOWLOG 捕获命令,INFO commandstatssinter 的调用次数与总耗时异常。MEMORY USAGE 查看两个集合的大小。

1.6.4 根因分析

SINTER 的算法复杂度为 O(N*M),其中 N 是最小集合的元素个数,M 是集合数量。Redis 会遍历最小集合的所有元素,然后依次在其他集合中执行 SISMEMBER 检查。对于百万级集合,这会产生海量的 CPU 计算。该过程在主线程执行,导致长时间阻塞。

1.6.5 修正方案

  1. 离线计算:将交/并集计算转移到离线分析系统(如 Spark),Redis 仅作为存储。
  2. 应用层优化:利用 SCARD 获取集合大小,选择最小的集合进行过滤,甚至可以在客户端实现分页交集。
  3. 数据结构变更:如果交集查询频繁,可预先计算并存储结果。

1.6.6 最佳实践

  • 严禁在生产环境对大集合直接执行 SINTER/SUNION/SDIFF,除非集合元素数极少且可控。
  • 监控 SLOWLOG,设置阈值如 10ms,及时告警。

内存飙升排查流程图

flowchart TD
    Start[内存飙升告警] --> A[INFO memory]
    A --> B{mem_fragmentation_ratio > 1.5?}
    B -->|是| C[内存碎片严重]
    B -->|否| D[数据集增长]
    C --> E[MEMORY STATS 分析碎片与分配器]
    D --> F[redis-cli --bigkeys 扫描大Key]
    F --> G[SLOWLOG 查看内存相关慢命令]
    E --> H[MEMORY DOCTOR 获取建议]
    G --> H
    H --> I{根因判断}
    I --> J[大Key拆分/UNLINK/调整分配器]
    I --> K[调整maxmemory与策略]

图表说明:

  • 总览说明:从内存飙升现象出发,分步诊断碎片率和大 Key,最终由 MEMORY DOCTOR 给出建议。
  • 流程分解:先通过 INFO memory 获取宏观指标,再根据碎片率分支,利用 MEMORY STATS--bigkeys 深入。
  • 与前文原理映射:内存碎片问题可关联第 3 篇 jemalloc 分配机制;大 Key 拆分关联第 1 篇数据结构设计。
  • 运维要点:生产环境应常备 redis-cli --bigkeys 定时任务,并配置 activedefrag 自动内存碎片整理。

2. 缓存反模式

缓存的使用不当会直接导致后端 DB 崩溃。以下从设计和运行时维度剖析五个反模式。

2.1 设计反模式案例 1:maxmemory-policy noeviction 导致写入失败

2.1.1 错误示例

maxmemory 4gb
maxmemory-policy noeviction  # 默认策略

2.1.2 现象描述

当 Redis 使用内存达到 4GB 上限后,所有写请求返回 (error) OOM command not allowed when used memory > 'maxmemory'。应用开始抛出写异常,缓存功能失效。

2.1.3 排查思路

  1. INFO memory 查看 used_memory 接近 maxmemory
  2. INFO statsevicted_keys 为 0,同时 total_error_replies 增加。
  3. CONFIG GET maxmemory-policy 确认为 noeviction

2.1.4 根因分析

noeviction 策略在所有内存淘汰策略中优先级最高,即永不淘汰任何 Key,内存满时直接拒绝写操作。对于纯缓存场景,这种设置会导致缓存不可用。

2.1.5 修正方案

根据业务场景选择淘汰策略:

  • 如果所有 Key 都是缓存,可选用 allkeys-lruallkeys-lfu
  • 如果混合持久化数据和缓存,应设置 volatile-lru 等策略,并为持久化 Key 不设过期时间。
maxmemory-policy allkeys-lru

2.1.6 最佳实践

结合第 6 篇缓存设计原则,严格区分缓存与存储,为不同用途的 Key 设置合理的 maxmemory-policy。同时,预留 20% 的内存作为 buffer,以防内存波动触发淘汰。

2.2 设计反模式案例 2:缓存预热缺失导致启动瞬间 DB 压力过大

2.2.1 错误示例

新版本上线后,直接清空 Redis 缓存,服务启动后依赖懒加载逐步填充缓存。

2.2.2 现象描述

服务重启后,数据库连接池瞬间耗尽,大量慢查询,Redis 的 keyspace_misses 远大于 keyspace_hits,命中率从 99% 跌至 10%。

2.2.3 排查思路

  • 监控数据库 QPS 突然飙升。
  • INFO stats 观察 keyspace_hitskeyspace_misses,计算命中率。
  • 检查启动日志,发现缓存无预热步骤。

2.2.4 根因分析

冷启动时缓存为空,所有请求直接穿透到数据库,形成瞬时高峰。高并发下这种冲击可能直接拖垮数据库。

2.2.5 修正方案

设计缓存预热脚本:

  • 在服务启动后,通过离线批处理将热点数据加载到 Redis。
  • 使用 MSETPipeline 批量写入。
  • 预热完成前,将服务的流量入口进行灰度或限流。

2.2.6 最佳实践

参见第 6 篇 Spring Cache 整合,结合 @PostConstructApplicationRunner 执行预热,并监控缓存命中率。

2.3 运行时反模式案例 1:缓存穿透——布隆过滤器未配置

2.3.1 错误示例

String cache = jedis.get("product:" + productId);
if (cache == null) {
    // 查询数据库,即使是恶意请求的不存在ID
    Product prod = db.query(productId); 
    if (prod == null) {
        jedis.setex("product:" + productId, 60, "NULL"); // 缓存空值,但防不住大量不同ID
    }
}

2.3.2 现象描述

有人恶意遍历大量不存在的 productId,缓存每次都缓存空值,但不断更换 ID,导致数据库持续承受大量无效查询,CPU 飙高。

2.3.3 排查思路

  • INFO stats 显示 keyspace_misses 异常高,并且 Key 增长迅速(大量空值 Key)。
  • 抓取部分请求日志,发现大量随机 ID。
  • 缓存空值虽然能防止同 ID 重复穿透,但无法防止海量不同 ID 的攻击。

2.3.4 根因分析

根本原因是没有在缓存层前面加一道过滤层,拦截掉绝大多数不存在 Key 的请求。布隆过滤器可以用极小的内存判断一个 Key 是否可能存在,从而拦截掉非法请求。

2.3.5 修正方案

使用 Redis Stack 提供的 BF.RESERVEBF.EXISTS

BF.RESERVE product_filter 0.01 10000000
# 预热所有合法 productId
BF.ADD product_filter "product:1"

应用层代码:

if (!jedis.bfExists("product_filter", "product:" + id)) {
    return null; // 直接拒绝
}
// 正常缓存逻辑

2.3.6 最佳实践

结合第 10 篇 Redis Stack 模块化扩展,布隆过滤器是防御穿透的首选工具。需要设置合理的误判率和容量,并定期评估 Key 数量增长。

2.4 运行时反模式案例 2:缓存击穿——互斥锁超时不当

2.4.1 错误示例

String lockKey = "lock:hot_item";
if (jedis.setnx(lockKey, "1") == 1) {
    jedis.expire(lockKey, 3);  // 3秒超时,可能构建缓存需要5秒
    // 查询DB重建缓存...
    jedis.setex("hot_item", 600, data);
    jedis.del(lockKey);
}

2.4.2 现象描述

热点新闻缓存过期瞬间,大量请求涌入,多个线程同时 SETNX 成功,因为锁的超时时间太短,重建还没完成锁就自动过期,其他线程又获得了锁,导致多个线程同时查询数据库,数据库压力飙升。

2.4.3 排查思路

  • Redis MONITOR(极短时间)观察到同一 lockKey 的多次 SETNX 成功和 DEL
  • 数据库连接数突增,查询相同的热点数据。
  • 应用日志有大量获取锁失败的重试记录。

2.4.4 根因分析

SETNX + EXPIRE 不是原子操作(Redis 2.6.12 后可用 SET key value NX EX seconds 解决原子性),并且锁没有续期机制。如果业务重建时间大于锁超时,锁就会提前释放,导致击穿。根本解决方案需要使用成熟的分布式锁框架,如 Redisson,其提供了 Watchdog 自动续期机制。

2.4.5 修正方案

使用 Redisson 的 RLock

RLock lock = redisson.getLock("lock:hot_item");
try {
    // lock() 不设 leaseTime,Watchdog 每 10 秒续期一次,保证锁在任务完成前不会释放
    lock.lock();
    // 重建缓存
} finally {
    lock.unlock();
}

详见第 7 篇 Redisson 分布式锁的 Watchdog 原理。

2.4.6 最佳实践

  • 不要手动实现复杂锁逻辑,使用成熟的 Redisson 客户端。
  • 对于特别热点的 Key,可以采用“逻辑过期”方案,在缓存值中存储一个逻辑过期时间,当发现过期时不加锁,直接返回旧值,并异步启动一个线程去更新缓存。

2.5 运行时反模式案例 3:缓存雪崩——EXPIRE 未加随机抖动

2.5.1 错误示例

// 大量配置项在同一时间点过期
jedis.setex("config:level", 3600, value1);
jedis.setex("config:multiplier", 3600, value2);
// ... 数百个 Key 都在 1 小时后同时过期

2.5.2 现象描述

整点时刻,Redis 的 expired_keys 瞬时飙升,同时大量缓存 Key 过期,请求穿透到 DB,DB QPS 瞬时翻倍,响应变慢,甚至触发限流。

2.5.3 排查思路

  • INFO stats 中观察 expired_keys 的增量曲线,在某一时刻出现峰值。
  • keyspace_hits 下降,keyspace_misses 上升。
  • 查看应用日志,发现大量超时集中在特定时间点。

2.5.4 根因分析

所有 Key 的过期时间完全一致,导致在同一个时间窗口大量 Key 同时失效,缓存层瞬间无法提供服务,后端压力骤增,形成雪崩。

2.5.5 修正方案

在设置过期时间时,增加一个随机值:

int baseTtl = 3600;
int randomTtl = new Random().nextInt(600); // 0~600秒的随机抖动
jedis.setex(key, baseTtl + randomTtl, value);

2.5.6 最佳实践

  • 缓存设计时必须为过期时间添加随机因子,范围建议为基数的 10%~20%。
  • 结合熔断、限流等下游保护措施。

2.6 设计反模式案例补充:@Cacheable 的 unless 条件错误导致空值缓存与内存浪费

2.6.1 错误示例

@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
    return productRepo.findById(id).orElse(null);
}

2.6.2 现象描述

当数据库查询返回 null 时,由于 unless 条件 #result == nulltrue,该 null 结果不会被缓存。但是下次同样的请求仍会穿透到数据库,如果大量不存在 ID 请求进来,保护作用为零。

2.6.3 排查思路

查看缓存中的 Key 列表,发现没有对应空结果的 Key;keyspace_misses 高;数据库持续有对不存在 ID 的查询。

2.6.4 根因分析

unless 条件判断是在方法执行后,决定是否将结果放入缓存。#result == null 表示当结果为 null 时不缓存,这导致了空值无法缓存,与防穿透目的相悖。

2.6.5 修正方案

正确做法是允许缓存空结果,并设置一个较短的过期时间,同时配合布隆过滤器拦截。

@Cacheable(value = "products", key = "#id", unless = "#result == null", cacheManager = "cacheManager")
// 需要额外缓存空值需要修改逻辑,或者用 unless = "false" 并返回一个空对象占位

更好的方式是在方法内手动处理,返回一个特殊的空对象,并让 unless = "false"

2.6.6 最佳实践

  • 明确 @Cacheableunlesscondition 的作用时机。
  • 缓存空值时设置较短的 TTL,避免内存浪费。

缓存穿透/击穿/雪崩联合排查序列图

sequenceDiagram
    participant Client
    participant BloomFilter as 布隆过滤器
    participant Redis
    participant DB
    Client->>BloomFilter: BF.EXISTS key
    alt 不存在
        BloomFilter-->>Client: false
        Client->>Client: 直接返回空
    else 可能存在
        BloomFilter-->>Client: true
        Client->>Redis: GET key
        alt 缓存命中且未过期
            Redis-->>Client: 数据
        else 缓存过期或不命中
            Redis-->>Client: null
            Client->>Client: 获取分布式锁
            alt 获取锁成功
                Client->>DB: 查询数据
                DB-->>Client: 数据
                Client->>Redis: SETEX (带随机抖动TTL)
                Client->>Redis: 释放锁
            else 获取锁失败
                Client-->>Client: 休眠重试或返回旧逻辑
            end
        end
    end

图表说明:

  • 总览说明:联合使用布隆过滤器、分布式锁、随机 TTL 三大手段阻断缓存三大问题。
  • 流程分解:先过滤非法请求,再检查缓存,若过期则通过锁控制重建,最后写入随机 TTL。
  • 与前文原理映射:布隆过滤器对应第 10 篇 Redis Stack;互斥锁续期对应第 7 篇 Watchdog;随机 TTL 对应第 6 篇雪崩防护。
  • 运维要点:监控命中率、锁竞争次数、DB 查询量,三者联动告警。

3. 持久化反模式

持久化配置错误可能导致数据丢失或性能严重下降。本节选取四个代表性案例。

3.1 设计反模式案例 1:生产环境同时禁用 RDB 和 AOF

3.1.1 错误示例

save ""
appendonly no

3.1.2 现象描述

服务器意外断电,Redis 重启后所有数据丢失,系统需要从上游数据源全量恢复,耗时数小时。

3.1.3 排查思路

INFO persistence 输出:

rdb_last_save_time:0
aof_enabled:0

确认无任何持久化。

3.1.4 根因分析

Redis 默认没有开启持久化。未配置任何持久化时,数据仅存在于内存,重启或崩溃即丢失。

3.1.5 修正方案

根据业务数据安全要求,至少开启 RDB:

save 900 1
save 300 10
save 60 10000

或开启 AOF:

appendonly yes

3.1.6 最佳实践

推荐开启混合持久化 aof-use-rdb-preamble yes,兼具快速恢复与安全性。

3.2 设计反模式案例 2:appendfsync always 导致写入性能急剧下降

3.2.1 错误示例

appendonly yes
appendfsync always

3.2.2 现象描述

在写入 QPS 较高的场景下(如 5000/s),Redis 的实际写入性能下降到几百 QPS,LATENCY DOCTOR 报告中 aof-fsync 延迟在几百毫秒级别。磁盘 IO 利用率 100%。

3.2.3 排查思路

  • INFO persistenceaof_fsync_pending 堆积。
  • LATENCY DOCTORLATENCY GRAPH 显示高 fsync 延迟。
  • 系统 iostat 显示磁盘写延迟非常高。

3.2.4 根因分析

appendfsync always 策略让 Redis 在每次写入 AOF 后都调用 fsync(),等待操作系统将数据刷到磁盘。普通磁盘的 IOPS 有限(SATA 盘约 100-200 IOPS),因此写入 QPS 直接被磁盘性能卡住。

3.2.5 修正方案

将策略改为 everysec

appendfsync everysec

这样 Redis 每秒同步一次,平衡了性能和安全(最多丢失 1 秒数据)。

3.2.6 最佳实践

见第 4 篇持久化双雄,生产环境始终使用 everysec,并配合 no-appendfsync-on-rewrite yes

3.3 设计反模式案例 3:AOF 重写 auto-aof-rewrite-percentage 设置过低导致重写风暴

3.3.1 错误示例

auto-aof-rewrite-percentage 10
auto-aof-rewrite-min-size 64mb

3.3.2 现象描述

Redis 频繁触发 AOF 重写,INFO persistenceaof_rewrite_in_progress 经常为 1,latest_fork_usec 持续在几百毫秒。系统 CPU 和内存周期性波动,伴随客户端超时。

3.3.3 排查思路

  • 监控 aof_current_sizeaof_base_size 的比例,频繁超过 1.1。
  • LATENCY 显示 fork 延迟高峰。
  • 日志中出现大量 Background AOF rewrite started

3.3.4 根因分析

percentage 设为 10 表示当当前 AOF 文件大小比上次重写后的大小增长 10% 时就触发重写。在写入密集型场景,这会导致 AOF 文件几乎一直在重写。每次重写都需要 fork 子进程,频繁 fork 会消耗大量 CPU 和内存页表复制,严重影响性能。

3.3.5 修正方案

将百分比调整为更合理的值,例如 100%:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 1gb

3.3.6 最佳实践

根据写入速率设置参数,使重写间隔至少保持在数十分钟以上。监控 aof_base_sizeaof_current_size 比例。

3.4 运行时反模式案例 1:BGSAVE 在内存 >10GB 时 fork 耗时数百毫秒阻塞

3.4.1 现象描述

定时 RDB 持久化执行时,LATENCY DOCTOR 报告如下:

- A recent fork call took 450 milliseconds (high). This can cause latency spikes.

对应时间点客户端响应变慢。

3.4.2 排查思路

  • INFO statslatest_fork_usec:450000
  • INFO memoryused_memory_rss 在 12GB 左右。
  • 确认 RDB 持久化已开启。

3.4.3 根因分析

Linux 的 fork 系统调用会复制父进程的页表。内存占用越大,页表复制时间越长。在 Redis 这种内存巨大的进程中,fork 可能耗时上百毫秒,期间主线程阻塞。

3.4.4 修正方案

  • 关闭 RDB,完全依赖 AOF 和混合持久化。
  • 如果必须保留 RDB,将 Redis 实例拆分,每个实例内存控制在 4GB 以下。
  • 开启 lazyfree-lazy-server-del 等惰性删除,减少内存占用。

3.4.5 最佳实践

监控 latest_fork_usec,设置告警阈值 100ms。优先采用 aof-use-rdb-preamble

3.5 运行时反模式案例 2:AOF 文件损坏无法启动,修复丢失数据

3.5.1 现象描述

Redis 启动失败,日志提示:

Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>

3.5.2 排查思路

使用 redis-check-aof --fix appendonly.aof 执行修复。启动后发现部分数据丢失。

3.5.3 根因分析

磁盘写满、意外断电或系统崩溃可能导致 AOF 文件尾部写入不完整。redis-check-aof --fix 会截断到最后一个合法的命令,导致末尾数据永久丢失。

3.5.4 修正方案

  • 启动后从备份或其他数据源恢复丢失数据。
  • 启用 aof-use-rdb-preamble yes,由于 RDB 头是全量数据,即使 AOF 尾部损坏也只会丢失少量增量。
  • 配置主从复制,从另一个节点获取完整 AOF。

3.5.5 最佳实践

  • 对 AOF 文件定期备份。
  • 确保磁盘有足够空间,监控 appendonly.aof 大小。

4. 高可用反模式

高可用配置的细微偏差可能导致频繁切换或数据丢失。

4.1 设计反模式案例 1:哨兵 quorum 设置过小导致脑裂或误切换

4.1.1 错误示例

5 个哨兵节点,配置 quorum=2

4.1.2 现象描述

主库发生短暂网络分区,只与 3 个哨兵断开,但仍有 2 个哨兵可以连接。那 2 个哨兵主观下线后,因为 quorum=2 即满足,它们发起了故障转移,选举了一个新主库。原主库在分区内继续接收写入,形成双主。分区恢复后,数据冲突。

4.1.3 排查思路

  • 检查哨兵日志,发现有 +odownfailover_start 事件,但 failover_end 之后很快又发现原主库以从库身份连接。
  • INFO replication 在两个节点上都看到 role:master

4.1.4 根因分析

详见第 5 篇哨兵机制。quorum 是检测主观下线和执行故障转移所需的哨兵最小数量。过小的 quorum 无法代表多数派,网络分区时极易导致误判,发生脑裂。

4.1.5 修正方案

quorum 设为 N/2 + 1(5 个哨兵设 3)。同时必须配置:

min-replicas-to-write 1
min-replicas-max-lag 10

使得原主库在无法联系足够从库时拒绝写入,避免脑裂期间的数据写入。

4.1.6 最佳实践

  • quorum 必须是多数。
  • 始终配置 min-replicas-to-write,牺牲一定的可用性换来数据一致性。

4.2 设计反模式案例 2:Cluster cluster-node-timeout 过小导致网络抖动频繁选举

4.2.1 错误示例

cluster-node-timeout 500

4.2.2 现象描述

集群环境中,由于偶尔的网络延迟达到 200ms,多个节点频繁被标记为 PFAIL 然后 FAIL,导致集群不断进行故障转移,客户端收到大量 MOVED 重定向错误。

4.2.3 排查思路

  • CLUSTER INFO 显示 cluster_state:fail 时段频繁。
  • 节点日志大量 Node ... is now in PFAIL state
  • 客户端报错 Too many Cluster redirections

4.2.4 根因分析

cluster-node-timeout 是节点存活判断的超时时间,也是选举过程中必须等待的时间。设置过小,会因网络波动误判节点下线,触发不必要的选举,严重影响集群稳定性。

4.2.5 修正方案

根据网络环境调整到更大的值,一般建议至少 3000ms-5000ms:

cluster-node-timeout 5000

4.2.6 最佳实践

第 5 篇集群调优经验,timeout 应大于网络中最大合理延迟的 3 倍。

4.3 运行时反模式案例 1:repl-backlog-size 过小导致频繁全量同步

4.3.1 现象描述

从库由于短暂网络中断,重连后无法部分同步(PSYNC),总是触发全量同步。每次全量同步主库都需要 fork 产生 RDB,导致主库 latest_fork_usec 持续高,并消耗大量网络带宽。

4.3.2 排查思路

  • INFO replicationrepl_backlog_active:1,但 repl_backlog_size 仅为 1MB(默认)。master_repl_offset 与从库的 slave_repl_offset 差值很快超过 backlog 大小。
  • 日志中出现 Unable to partial resync with slave

4.3.3 根因分析

复制积压缓冲区(replication backlog)是一个环形缓冲区,用于记录最近的写命令。如果从库断线期间主库产生的写命令超过了 repl-backlog-size,那么部分同步所需的数据就会被覆盖,只能触发全量同步。

4.3.4 修正方案

增大 backlog 大小,至少能容纳断线期间的写入量:

repl-backlog-size 256mb

4.3.5 最佳实践

根据 master_repl_offset 的增长速率和可能的最大断线时间计算需求。一般设置 64MB~256MB。

4.4 运行时反模式案例 2:脑裂(网络分区 + 哨兵误判)导致双主写入

4.4.1 现象描述

如上文 quorum 案例,网络分区后发生双主写入。网络恢复后,原主库被降级为从库,触发全量同步,期间分区内写入的数据永久丢失(因为新主库的数据覆盖了它)。

4.4.2 排查思路

  • 哨兵日志显示 +odown, +switch-master
  • 对比两个节点的 keyspace,发现不一致。
  • 客户端有错误日志:写入旧主成功,但后来数据读不到。

4.4.3 根因分析

脑裂的根本原因是分区后的少数派仍能构成 quorum 并执行故障转移,而旧主库仍可写入。没有 min-replicas-to-write 的保护,旧主库会接收写入导致数据分歧。

4.4.4 修正方案

  1. 调整 quorum 为多数派。
  2. 配置 min-replicas-to-write 1min-replicas-max-lag 10
  3. 客户端在捕获到连接中断或 READONLY 等错误时,应停止写入并重试。

4.4.5 最佳实践

脑裂不可避免时,应接受数据可能丢失,但通过合理配置将丢失降到最低。

脑裂排查序列图

sequenceDiagram
    participant Master1 as 原主库
    participant Sentinel群
    participant Slave as 从库
    participant Master2 as 新主库(原从库)
    participant Client
    Note over Master1,Slave: 网络分区发生
    Sentinel群->>Master1: 心跳超时 SDOWN
    Sentinel群-->>Sentinel群: 投票 ODOWN, quorum达成
    Sentinel群->>Slave: 故障转移, 提升为新主库 Master2
    Client->>Master1: 分区内仍写入 (无 min-replicas 保护)
    Client->>Master2: 分区内部分客户端写入新主库
    Note over Master1,Master2: 网络恢复
    Master1->>Master2: 被配置为新主库的从库,触发全量同步
    Note over Master1: 分区期间的写入数据丢失

图表说明:

  • 总览说明:直观展示脑裂发生过程,双主写入及数据丢失路径。
  • 流程分解:从心跳超时、投票、切换,到分区内写入和最终同步。
  • 与前文原理映射:哨兵主观/客观下线对应第 5 篇;集群脑裂处理。
  • 运维要点:必须配置 min-replicas 限制写入,并建立脑裂检测告警。

5. 运维与安全反模式

运维与安全是 Redis 稳定运行的底线,疏忽将导致灾难性后果。

5.1 设计反模式案例 1:生产环境未设置 ACL 导致未授权访问

5.1.1 错误示例

# 无 requirepass,ACL 文件未配置,默认用户 default 无密码,拥有所有权限

5.1.2 现象描述

安全扫描发现内网 Redis 未授权访问,任何能够网络连通的应用都可以随意操作数据,甚至获取敏感信息。

5.1.3 排查思路

  • ACL LIST 返回 user default on nopass ~* +@all
  • CONFIG GET requirepass 返回空字符串。

5.1.4 根因分析

未按照第 9 篇的安全最佳实践设置密码和 ACL。这直接导致未授权访问风险。

5.1.5 修正方案

  • 启用 requirepass 并设置强密码,或使用 ACL 文件。
  • 示例 ACL 配置:
user default off
user app on >StrongPassword123 ~* +@all

5.1.6 最佳实践

生产环境必须对 Redis 进行身份认证,应用账号遵循最小权限原则。

5.2 设计反模式案例 2:protected-mode no + bind 0.0.0.0 导致公网暴露与 SSH 公钥注入

5.2.1 错误示例

protected-mode no
bind 0.0.0.0
port 6379
# 无密码

5.2.2 现象描述

Redis 被公网扫描发现,攻击者通过 CONFIG SET dir /root/.sshCONFIG SET dbfilename authorized_keys,再执行 BGSAVE,将自己的 SSH 公钥写入服务器 authorized_keys,最终获得服务器控制权。

5.2.3 排查思路

  • 安全团队发现 SSH 登录异常,检查 /root/.ssh/authorized_keys 多了不明公钥。
  • Redis 日志显示有 CONFIG SET 命令执行。
  • INFO commandstatsconfig|set 调用次数异常。

5.2.4 根因分析

Redis 在非保护模式下绑定公网地址且无密码,导致攻击者可以执行任何 Redis 命令,包括修改数据目录和文件名。结合 BGSAVE 持久化功能,攻击者可以写入任意文件。

5.2.5 修正方案

protected-mode yes
bind 127.0.0.1 10.0.0.5   # 仅内网
requirepass yourStrongPass
rename-command CONFIG ""   # 或重命名
rename-command BGSAVE ""

5.2.6 最佳实践

  • 使用 protected-mode yes,配置 bind 到可信地址。
  • 必须设置密码或 ACL。
  • 重命名或禁用 CONFIGDEBUGSAVEBGSAVE 等危险管理命令。

5.3 运行时反模式案例 1:MONITOR 命令在生产环境长时间运行导致 QPS 下降

5.3.1 错误示例

运维或开发为了调试,在生产环境执行:

redis-cli MONITOR > /tmp/debug.log

5.3.2 现象描述

执行后,Redis 总体 QPS 从 5 万降至 2.5 万,CPU 使用率上升,LATENCY DOCTOR 报告 CPU 和输出缓冲区延迟。

5.3.3 排查思路

  • CLIENT LIST 发现 cmd=monitor 的客户端。
  • INFO stats 显示 instantaneous_ops_per_sec 下降。
  • 内存可能因输出缓冲区积压而增加。

5.3.4 根因分析

MONITOR 会把 Redis 执行的每一条命令都发送给该客户端。在高 QPS 场景下,这会占用大量 CPU 用于序列化命令和网络发送,同时可能导致该客户端的输出缓冲区无限增大,引发内存问题。经验表明性能会下降 50% 以上。

5.3.5 修正方案

立即断开该客户端:

CLIENT KILL addr:ip:port

并永久禁用 MONITOR

rename-command MONITOR ""

5.3.6 最佳实践

生产环境禁用 MONITOR,调试使用 SLOWLOGLATENCY 或外部追踪工具。

5.4 运行时反模式案例 2:FLUSHDB 误操作但未设置 rename-command 禁用

5.4.1 错误示例

DBA 原本在测试环境执行 FLUSHDB 清理数据,但终端连接的是生产环境,直接执行了 FLUSHALL

5.4.2 现象描述

所有数据被清空,业务完全中断,只能从备份恢复。

5.4.3 排查思路

SLOWLOG GET 1 显示 FLUSHALL 命令,时间点与故障吻合。

5.4.4 根因分析

危险命令未重命名,误操作可直接执行。

5.4.5 修正方案

rename-command FLUSHALL ""
rename-command FLUSHDB ""

5.4.6 最佳实践

禁用或重命名所有危险命令(KEYS, FLUSHDB, FLUSHALL, CONFIG, SHUTDOWN 等)。操作前一定确认环境。


6. 诊断工具集与工具→现象映射表

6.1 Redis 端全工具链速查

  • redis-cli --bigkeys:扫描 Key 空间,输出各种类型最大的 Key。
  • redis-cli --memkeys:按内存占用排序输出 Key,需 redis-cli 较新版本支持。
  • SLOWLOG:记录执行时间超过 slowlog-log-slower-than 阈值的命令。
  • MONITOR:实时打印所有命令,性能影响极大,生产慎用。
  • INFO:提供 server、clients、memory、stats、replication、cpu、commandstats、cluster 等模块的详尽指标。
  • LATENCYLATENCY DOCTOR 提供延迟诊断报告;LATENCY GRAPH 可视化延迟事件;LATENCY HISTORY 查看历史延迟。
  • MEMORY 系列:MEMORY USAGE key 估算 Key 内存占用;MEMORY STATS 输出内部内存分布;MEMORY DOCTOR 给出内存优化建议。
  • redis-cli --stat:实时输出 OPS、内存、客户端数等滚动信息。
  • redis-cli --latency:测量到 Redis 节点的网络延迟分布。
  • Prometheus + Grafana:通过 redis_exporter 采集指标,建立长期监控与告警。

6.2 工具→现象映射表(≥12行)

典型现象推荐工具关键检查命令/指标常见根因
CPU 飙高SLOWLOG, INFO commandstats, LATENCYSLOWLOG GET 10; INFO commandstats 中高消耗命令KEYS *, SORT, 高频 ZRANK, SINTER
内存飙升INFO memory, MEMORY STATS, --bigkeysused_memory_rss; mem_fragmentation_ratio; 大Key扫描大Key未拆分,集合编码升级,内存碎片
缓存命中率低INFO statskeyspace_hits vs keyspace_missesexpired_keys 突增雪崩(同时过期),穿透,预热缺失
主从全量同步频繁INFO replication, LATENCYrepl_backlog_size; master_repl_offset 差值backlog过小,网络不稳定
集群不可用/重定向CLUSTER INFO, CLUSTER NODEScluster_state:fail; cluster-node-timeout; slot分布超时过小,require-full-coverage
写入被拒绝INFO stats, CONFIG GET maxmemory*rejected_connections; evicted_keys; maxmemory-policymaxclients耗尽,内存满且noeviction
持久化性能问题INFO persistence, LATENCY DOCTORaof_fsync_pending; aof_rewrite_in_progress; latest_fork_usecalways fsync,重写风暴,fork阻塞
安全未授权ACL LIST, CONFIG GET requirepassuser default on nopass; protected-mode no无密码,公网暴露
OOM / 慢查询redis log, MEMORY STATS, SLOWLOG进程被杀;SMEMBERS, SORT大集合全量返回,未用SCAN
客户端超时CLIENT LIST, LATENCYomem(输出缓冲区)高;age时间大慢命令阻塞,网络延迟
脑裂哨兵日志, INFO replication双主 role:master+odown事件quorum过小,min-replicas未配置
AOF 损坏redis-check-aofredis-check-aof --fix 截断断电,磁盘满,写异常
数据丢失INFO persistence, 备份RDB/AOF 文件状态无持久化,AOF截断,脑裂
监控干扰CLIENT LISTcmd=monitor 客户端生产环境执行 MONITOR

7. 标准化排查决策树

面对线上故障,可以按照以下决策树快速定位问题。

“CPU 飙高”分支

  1. SLOWLOG:立即 SLOWLOG GET 20,找出耗时最长的命令。
    • 如果包含 KEYS → 立刻找到执行者并优化为 SCAN,配置重命名禁用。
    • 如果包含 SORT → 检查数据集大小,改为应用层排序或弃用。
    • 如果包含 ZADD, ZRANK → 检查是否热点 Key 过高频写入,需聚合/拆分。
    • 如果包含 SINTER, SUNION → 大集合运算,迁移至离线或拆分。
  2. INFO commandstats:查看总调用量和总耗时,识别被高频调用的潜在慢命令。
  3. LATENCY DOCTOR:辅助判断延迟是否由 fork、I/O 等非命令引起。

“内存飙升”分支

  1. INFO memory:获取 used_memory_rss, used_memory_dataset, mem_fragmentation_ratio
    • 碎片率 > 1.5 → 内存碎片问题,使用 MEMORY STATSMEMORY DOCTOR 分析,考虑 activedefrag 或重启。
    • 碎片率正常 → 数据集确实增加。
  2. redis-cli --bigkeys--memkeys:找出占用内存最多的 Key。
    • 如果是大 String → 拆分为多个小 Key 或 Hash。
    • 如果是大集合 → 检查编码,考虑分片。
  3. SLOWLOG:检查是否有 SMEMBERS, KEYS 等导致内存瞬时冲高的操作。
  4. MEMORY DOCTOR:执行并获取自动化建议。

“缓存命中率低”分支

  1. INFO stats:计算 keyspace_hits_ratio
  2. 分析过期expired_keys 瞬时增量 → 雪崩,检查 Key 的 TTL 是否集中,增加随机抖动。
  3. 分析穿透:如果 keyspace_misses 很大且多为不存在 ID → 配置布隆过滤器,并缓存空值。
  4. 分析击穿:热点 Key 重建日志,数据库有高并发查询 → 加强分布式锁逻辑或使用逻辑过期。
  5. 检查预热:如果刚重启过,则为预热不足。

“集群不可用”分支

  1. CLUSTER INFOCLUSTER NODES:查看集群状态。
    • cluster_state:fail 且 slot 未完全分配 → 可能是 cluster-require-full-coverage yes 且正在迁移。
    • 节点状态为 fail → 检查 cluster-node-timeout 是否过小,或节点确实宕机。
  2. 脑裂检查:如果采用哨兵,检查日志是否有双主。查看 INFO replication
  3. 客户端侧:大量 MOVED 错误 → 确保客户端库支持自动重定向,并已正确初始化。

标准化排查决策树总图

flowchart TD 
    Start[故障现象] --> A{CPU飙高?}
    A -->|是| A1[SLOWLOG GET + INFO commandstats]
    A1 --> A2{命令类型?}
    A2 -->|KEYS *| A3[替换为 SCAN, 重命名禁用]
    A2 -->|SORT| A4[移除或应用层排序]
    A2 -->|ZADD/ZRANK高频| A5[聚合写入/本地缓存]
    A2 -->|集合运算| A6[拆分集合/离线计算]
    A -->|否| B{内存飙升?}
    B -->|是| B1[INFO memory + MEMORY STATS]
    B1 --> B2{碎片率 > 1.5?}
    B2 -->|是| B3[activedefrag / MEMORY DOCTOR]
    B2 -->|否| B4[redis-cli --bigkeys 定位大Key]
    B4 --> B5[拆分Key/UNLINK]
    B -->|否| C{缓存命中率低?}
    C -->|是| C1[INFO stats hits/misses]
    C1 --> C2{大量同时过期?} --> C3[加随机TTL防雪崩]
    C1 --> C4{大量不存在Key?} --> C5[布隆过滤器防穿透]
    C1 --> C6{热点Key重建竞争?} --> C7[分布式锁/逻辑过期]
    C -->|否| D{集群不可用?}
    D -->|是| D1[CLUSTER INFO / NODES]
    D1 --> D2{节点down?} --> D3[调整timeout/检查网络]
    D1 --> D4{slot未覆盖?} --> D5[检查 full-coverage 配置]
    D1 --> D6{哨兵脑裂?} --> D7[调整quorum / min-replicas-to-write]

图表说明:

  • 总览说明:将四大典型故障的排查路径整合到一个决策树中,实现从现象到根因的快速导航。
  • 流程分解:每个分支对应具体诊断命令和常见反模式,并在叶节点给出修复动作。
  • 与前文原理映射:CPU 分支关联线程模型和编码;内存分支关联编码升级和持久化 fork;缓存命中率关联缓存设计;集群关联高可用架构。
  • 运维要点:生产环境可将该决策树固化为 SOP 手册,结合自动化脚本实现初诊。

考虑到你要求面试部分更加详尽,我将对第 8 节“面试高频故障排查专题”进行大幅扩写,增强每道题的场景细节、排查路径、命令示例和最佳实践,使每一题都成为独立的小型故障排查案例。以下仅提供替换后的完整面试专题部分,你可以在原文章中直接替换。


8. 面试高频故障排查专题

8.1 线上 Redis CPU 飙到 100%,SLOWLOG 显示多条 KEYS * 命令,如何紧急处理和长期修复?

场景描述 晚上 8 点促销高峰,Redis 突然 CPU 100%,所有服务超时。运维登录服务器,执行 redis-cli SLOWLOG GET 10,发现多条如下记录:

1) 1) (integer) 5
   2) (integer) 1715934000
   3) (integer) 4200000  # 4.2 秒
   4) 1) "KEYS"
      2) "session:*"

同时,redis-cli --stat 显示 QPS 下降到 0,直至 KEYS 执行完毕。应用日志大量 JedisConnectionException: timeout

排查思路

  1. 快速止血:通过 CLIENT LIST 找到发起 KEYS 的客户端 IP 和端口,确认是内部运维脚本。紧急执行 CLIENT KILL addr:192.168.1.100:45678 断开该连接,CPU 立刻回落。
  2. 溯源:检查该 IP 的定时任务,发现运维部署了一个清理过期 Session 的 Shell 脚本,里面使用了 redis-cli KEYS "session:*" | xargs redis-cli DEL
  3. 验证:测试环境模拟 100 万 Key,执行 KEYS "session:*" 耗时 3.8 秒,与线上一致。

根因分析 KEYS * 命令在主线程中全量遍历整个键空间,复杂度 O(N),且执行期间阻塞所有其他命令。这在任何拥有百万级 Key 的 Redis 实例上都会导致数秒的服务中断,是典型的阻塞性命令误用。

修复方案

  • 立即将脚本中的 KEYS 替换为 SCAN
    redis-cli --scan --pattern "session:*" | while read line; do redis-cli UNLINK "$line"; done
    
  • 同时配置 rename-command KEYS "" 彻底禁用该命令,防止再次误用。
  • 对存量大 Key 的删除也统一改用 UNLINK,避免删除阻塞。

最佳实践

  • 所有运维脚本禁止使用 KEYS,统一使用 SCAN 家族。
  • 在配置文件中禁用 KEYSFLUSHDB 等危险命令。
  • 建立代码审查和上线前安全检查清单。

8.2 Redis 内存突然从 10GB 飙升至 30GB,但 Key 数量并未明显增加,如何定位根因?

场景描述 监控告警:Redis 内存使用量从 10GB 跳升至 30GB,但 DBSIZE 显示 Key 总数变化不大(从 50 万只增加到 52 万)。

排查思路

  1. 宏观检查INFO memory 重点看:
    used_memory_dataset:28000000000  # 数据集占用 28GB
    mem_fragmentation_ratio:1.02     # 碎片率正常
    
    可见确实是真实数据增加,非内存碎片。
  2. 定位大 Keyredis-cli --bigkeys 输出:
    Biggest set found 'online:users:3' has 4500000 members
    
    发现某个 Set 有 450 万成员,而之前设计容量仅为 5000 人。
  3. 检查内部编码OBJECT ENCODING online:users:3 返回 hashtable,而新建的小集合编码为 listpack。已知集合元素数超过 set-max-listpack-entries(默认 128)就会从 listpack 升级为 hashtablehashtable 每个元素大约增加 20~30 字节开销,450 万成员的额外开销就高达近 100MB,再加上实际数据,总体内存爆炸。
  4. 验证MEMORY USAGE online:users:3 计算出该 Key 占用约 1.2GB,而如果仍为 listpack 编码,同样数量成员可能只占用 200MB。

根因分析 Redis 集合在元素数量超过阈值时,会自动从紧凑的 listpack(或旧版 intset)升级为 hashtable(即 dict)。hashtable 有哈希桶、指针等额外开销,内存占用远大于 listpack。这种隐式升级在设计时未被预估,导致线上内存翻倍。

修复方案

  • 短期:将大集合拆分为分片,如按用户 ID 哈希取模 online:users:0 ~ online:users:9,每个分片控制在阈值以下,使用 listpack 节省内存。
  • 长期:调整 set-max-listpack-entries 配置到适合业务的值,或者直接接受 hashtable 并提前规划内存。

最佳实践

  • 系统设计阶段预估集合最大规模,设定合理的内部编码阈值。
  • 监控 OBJECT ENCODING 的变更,当大量 Key 从内存高效编码转变为通用编码时产生告警。

8.3 缓存命中率从 99% 跌至 50%,应用日志显示大量 DB 查询超时,如何排查?

场景描述 交易系统的缓存命中率突然从 99% 下跌到 50%,数据库的 QPS 从 500 飙升至 5000,大量慢查询导致连接池耗尽,订单创建超时。

排查思路

  1. 确认缓存指标INFO stats 中:
    keyspace_hits:1200000
    keyspace_misses:1200000
    
    命中率约 50%,且 expired_keys 在过去 1 分钟暴增了 10 万次。由此怀疑是大量 Key 集中过期导致的雪崩。
  2. 抽样检查 TTL:随机抽取缓存 Key 的 TTL,发现大量商品缓存 Key 的剩余时间都在 0~60 秒之间波动,且原本设置的过期时间都是 3600 秒整。正是由于没有随机抖动,这些 Key 都在同一时间点附近过期。
  3. 排除穿透与击穿BF.EXISTS 检查商品布隆过滤器,未发现大量不存在 Key 的请求。日志中也未看到大量锁竞争,故非穿透/击穿。

根因分析 所有缓存 Key 设置的过期时间完全相同,在某个时刻同时过期,缓存雪崩发生。瞬间大量请求直达数据库,远超其处理能力。

修复方案

  • 临时:重启缓存预热脚本,快速加载热点数据。同时对数据库查询开启限流,保护 DB。
  • 永久:在缓存过期时间上增加随机值,如 int ttl = 3600 + ThreadLocalRandom.current().nextInt(600);
  • 引入多级缓存(本地 Caffeine + Redis),进一步减小 Redis 故障时对 DB 的冲击。

最佳实践

  • 为所有缓存过期时间添加至少 10% 的随机抖动。
  • 监控 expired_keys 的增长速率,出现异常尖峰时提前告警。

8.4 主从复制频繁断开重连,每次重连都触发全量同步导致主库 CPU 飙升,如何解决?

场景描述 主从复制状态不稳定,从库因为网络微闪断就自动重连,但每次重连后都开始全量同步,主库频繁 fork 生成 RDB,latest_fork_usec 高达 600ms,CPU 使用率周期性飙高。

排查思路

  1. 检查复制状态INFO replication 在主库看到:
    master_repl_offset:123456789012
    
    从库首次重连后,偏移量差距很大。repl_backlog_size 显示为默认的 1MB。
  2. 计算写入量:通过监控估算主库每秒写入约 500KB,而从库断开一般持续 2-3 秒,产生的写入量约 1.5MB,已经超过 1MB 的 backlog 缓冲区。这导致部分同步失败,只能全量同步。
  3. 检查网络:网络监控显示从库和主库之间有微小丢包,但能很快恢复。backlog 过小是主因。

根因分析 复制积压缓冲区 repl-backlog-size 过小,无法容纳从库断线期间主库产生的写命令,迫使从库每次都进行全量同步。每次全量同步都需要主库 fork 子进程生成 RDB,消耗大量 CPU 和内存。

修复方案repl-backlog-size 调整为 256MB,确保即使断开 10 秒也能部分同步:

repl-backlog-size 256mb

同时优化网络,减少闪断。

最佳实践 根据业务写入速率和最大容忍断线时间计算 backlog 大小,公式:backlog_size >= 写入速率 * 最大断线时间 * 2。一般生产环境建议至少 64MB。


8.5 哨兵自动切换后出现双主写入,网络恢复后数据出现冲突,如何排查和修复?

场景描述 双机房部署,主库在 A 机房,哨兵在 A、B 两机房共 5 个节点,quorum=2。某日 A-B 机房网络专线中断,B 机房的两个哨兵发现主库不可达,标记 SDOWN,随后达成 quorum(2 个哨兵)将 B 机房的从库提升为新主库。此时 A 机房的主库仍在接收本地客户端写入。网络恢复后,两个主库并存,数据冲突,最终 A 机房旧主库被降级为从库,全量同步导致期间写入的数据丢失。

排查思路

  1. 哨兵日志:在 B 机房的哨兵日志中发现:
    +sdown master mymaster 10.0.1.10 6379
    +odown master mymaster 10.0.1.10 6379 #quorum 2/2
    +switch-master mymaster 10.0.1.10 6379 10.0.2.10 6379
    
  2. Redis 日志:旧主库上,切换后依然有客户端写入,且 min-replicas-to-write 为 0(未配置)。
  3. 数据对比:在新主库上发现,某些 Key 的更新时间晚于旧主库,但旧主库有部分 Key 是在分区期间新增的,现已丢失。

根因分析 quorum=2 在 5 个哨兵中仅需两个即可发起故障转移,无法抵御网络分区。同时未设置 min-replicas-to-write 来限制旧主在无法复制时的写入,最终导致脑裂与数据丢失。

修复方案

  • 调整 quorum 为 3(5/2+1),确保需要多数派同意。
  • 配置主库的写入保护:
    min-replicas-to-write 1
    min-replicas-max-lag 10
    
  • 客户端在捕获到连接中断或重定向时,应暂停写入并重试。

最佳实践

  • 哨兵集群必须部署在奇数个不同网络区域,quorum 为多数。
  • 必须设置 min-replicas-to-write,牺牲部分可用性换取数据一致性。

8.6 BGSAVE 执行期间 Redis 响应变慢,latest_fork_usec 超过 500ms,如何优化?

场景描述 每 5 分钟执行一次 BGSAVE 进行持久化,每次执行时,LATENCY DOCTOR 报告:

- A recent fork call took 620 milliseconds (high).

对应时间点客户端请求的 P99 延迟从 5ms 飙升至 600ms 以上。

排查思路

  1. 确认 fork 耗时INFO statslatest_fork_usec:620000
  2. 查看内存INFO memory 显示 used_memory_rss:15GB。当前实例为单节点,内存 15GB。
  3. 系统检查/proc/sys/vm/overcommit_memory 为 0,也增大了 fork 失败风险。

根因分析 Linux fork() 调用会复制父进程的页表,内存越大页表复制越耗时。15GB 的 Redis 实例,其页表大小可能达到数百 MB,复制时间轻松达到数百毫秒,期间主线程完全阻塞。

修复方案

  • 短期:关闭自动 save 参数,改为在低峰期手动执行,或完全关闭 RDB,改用 appendonly yesaof-use-rdb-preamble yes
  • 长期:将 Redis 实例拆分,例如按业务拆分为多个小实例,单个实例内存控制在 4GB 以下,fork 耗时即可保持在 50ms 内。

最佳实践

  • 监控 latest_fork_usec,超过 100ms 立刻告警。
  • 高内存实例首选 AOF 混合持久化,避免频繁 fork。

8.7 AOF 文件损坏导致 Redis 无法启动,如何紧急恢复并保证数据完整性?

场景描述 机器异常断电,Redis 重启时打印错误:

Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>

排查思路

  1. 备份:立即备份损坏的 appendonly.aof
  2. 修复:执行 redis-check-aof --fix appendonly.aof,日志显示 Successfully truncated AOF,文件大小减小。
  3. 启动:成功启动 Redis,但检查发现最新几分钟的写入数据丢失。

根因分析 操作系统崩溃或磁盘满时,AOF 文件的最后几条命令可能未完整写入(尾部损坏)。redis-check-aof --fix 会自动截断到最后一个完整的命令,导致部分数据丢失。

修复方案

  • 从其他主/从节点复制一份完整的 AOF 文件,如果只有该节点,可尝试从备份或应用日志回补。
  • 如果是主从架构,可直接提升从库为主库,并从它复制数据。

最佳实践

  • 开启混合持久化 aof-use-rdb-preamble yes,RDB 头部可保证大部分数据完整,只丢少量增量。
  • 监控磁盘空间和 IO 健康,提前预警。

8.8 Redis 突然拒绝所有写入,INFO stats 显示 rejected_connections > 0,如何排查?

场景描述 应用报错:redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory',部分报 connection refused

排查思路

  1. 检查连接数INFO clients 显示 connected_clients:10000,而 maxclients 为 10000,rejected_connections 为 125。连接数已满。
  2. 检查内存INFO memory 显示 used_memory:4.2GBmaxmemory:4GBmaxmemory-policy:noeviction。内存满了且策略为不淘汰,拒绝写入。
  3. 分析连接CLIENT LIST 发现大量空闲连接未释放,可能是应用连接池泄漏。

根因分析 两个并发问题:连接数耗尽和内存满且 noeviction。应用未正确归还连接,导致新连接被拒。内存因不淘汰而无法写入。

修复方案

  • 紧急:临时调高 maxclients 并杀死空闲连接 CLIENT KILL IDLE 300。切换 maxmemory-policyallkeys-lru(需重启或 CONFIG SET 热修改)。
  • 长期:修复应用连接池泄漏,并设置合理的淘汰策略。

最佳实践

  • 设置合适的 maxclients,并监控连接数趋势。
  • 缓存场景务必使用 allkeys-lruallkeys-lfu

8.9 SORT 命令在 1000 万成员的 List 上执行导致 OOM,如何优化?

场景描述 运营人员执行一个数据导出脚本,调用了 SORT big_list LIMIT 0 100000,该列表包含 1000 万条记录。Redis 内存瞬间从 8GB 飙升至 32GB,触发系统 OOM Killer 杀死了进程。

排查思路

  1. 日志分析:Redis 日志显示 SORT command causing out of memory
  2. 命令检查SLOWLOG 看到正在运行的 SORTMEMORY USAGE big_list 确认列表巨大。

根因分析 SORT 命令会为列表所有元素创建临时数据结构进行排序,需要的内存数倍于原始数据。1000 万条记录足以耗尽所有内存。

修复方案

  • 禁止对大列表直接 SORT。改用 ZSet 维护排序,或在应用层分批加载并排序。
  • 如果必须排序,使用 SORT ... STORE 将结果存入新 Key,且务必加上 LIMIT 限制排序范围。

最佳实践

  • 在配置中禁用 SORT 命令,或限制其使用场景。
  • 大数据量排序任务交给离线处理系统。

8.10 MONITOR 命令在生产环境执行后 Redis QPS 从 5 万跌至 2 万,如何恢复?

场景描述 新入职同事为排查问题,在跳板机上执行了 redis-cli -h prod-redis MONITOR,半小时后才发现系统报警。

排查思路

  1. INFO stats 看到 instantaneous_ops_per_sec 腰斩。
  2. CLIENT LIST 找到 cmd=monitor 的客户端连接。
  3. 执行 CLIENT KILL addr=10.0.0.5:56789 后,QPS 瞬间恢复。

根因分析 MONITOR 将所有命令实时打印到该客户端,在高 QPS 下会产生巨大的 CPU 消耗(序列化)和输出缓冲区内存占用,通常导致性能下降 50% 以上。

修复方案

  • 立即断开连接。
  • 在配置文件中添加 rename-command MONITOR "" 永久禁用。

最佳实践

  • 生产环境严禁使用 MONITOR,用 SLOWLOGLATENCY 代替。

8.11 Redis 被外部攻击者通过 CONFIG SET dir 写入 SSH 公钥,如何排查和加固?

场景描述 安全团队发现服务器被 root 登录,检查 /root/.ssh/authorized_keys 发现不明公钥,排查发现是通过 Redis 写入的。

排查思路

  1. Redis 日志:有 CONFIG SET dir /root/.sshCONFIG SET dbfilename authorized_keys 记录。
  2. 配置检查protected-mode nobind 0.0.0.0requirepass 为空。
  3. 攻击还原:攻击者连接到公网暴露的 6379 端口,SET mykey "ssh-rsa AAA...",然后修改 dir 和 dbfilename,执行 BGSAVE 写入文件。

修复方案

  • 删除恶意公钥。
  • Redis 配置修改:protected-mode yesbind 127.0.0.1requirepass 设置强密码。
  • 禁用危险命令:rename-command CONFIG ""rename-command BGSAVE ""

最佳实践

  • Redis 绝对不能直接暴露在公网。
  • 使用最小权限 ACL,重命名所有管理类命令。

8.12 CLUSTER FAILOVER 强制切换期间客户端大量抛出 MOVED 重定向错误,如何排查和优化?

场景描述 运维执行 CLUSTER FAILOVER 进行主从切换,切换过程中,应用日志突然刷出大量 redis.clients.jedis.exceptions.JedisMovedDataException: MOVED,部分请求失败。

排查思路

  1. 检查客户端:使用的是 Jedis,未配置自动重定向,只在连接初始化时获取一次槽位映射。
  2. 集群事件CLUSTER NODES 显示 epoch 变更,slot 正在迁移。
  3. 原因:切换期间 slot 所有权变更,客户端缓存失效,请求发到旧节点导致 MOVED。

修复方案

  • 客户端升级为 JedisCluster(内部处理 MOVED/ASK)或 Lettuce(内置自适应拓扑刷新)。
  • 若无法升级,应用层需捕获 JedisMovedDataException,解析新地址并重试。

最佳实践

  • 使用支持集群自动重定向的客户端,并配置合理的拓扑刷新间隔。

8.13 @Cacheable 的 unless 条件错误导致大量 NULL 值被缓存,如何排查和修复?

场景描述 Spring Boot 应用使用 @Cacheable 缓存商品信息,Redis 内存随时间异常增长,抽样发现许多 Key 的值为 null

排查思路

  1. redis-cli GET product:9999 返回 null,而数据库里 ID 9999 的商品确实不存在。
  2. 查看代码:
    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    
    原本想缓存空值防穿透,但 unless = "#result == null" 的效果是:当结果为 null 时不缓存。可实际情况是,由于缓存未命中,方法执行返回 null,Spring 认为结果为 null 就不缓存,于是每次请求都穿透到 DB。但问题是为什么 Redis 里会有 null 值?可能应用中另有其他逻辑手动将 null 写入了缓存。
  3. 真正原因:另一个初始化方法错误地缓存了 null,或者开发在测试时手动 SET 了 null 字符串。

修复方案

  • 修正 unless 条件为 unless = "false",或换成自定义 CacheManager 配置允许缓存 null 并设置较短的 TTL。
  • 也可使用布隆过滤器防止穿透,不缓存 null。

最佳实践

  • 清楚区分 conditionunless 的作用时机。
  • 对 null 值缓存必须配置独立 TTL,定期清理。

8.14 系统设计题:基于文中的决策树思想,为一套核心交易系统的 Redis 缓存层规划故障应急预案与监控体系,要求覆盖内存飙升、缓存雪崩、主从断开、集群脑裂四种典型故障的检测、告警与自愈策略。

背景需求 交易系统要求高可用、高一致性,缓存层采用 Redis Cluster(或哨兵模式)部署,日均 QPS 50 万。需要建立一套完整的可观测、可自愈的故障应对体系。

一、监控体系设计

使用 Prometheus + redis_exporter 采集指标,Grafana 可视化,Alertmanager 告警通知。核心监控面板划分:

  1. 黄金指标面板:QPS、命中率(keyspace_hits_ratio)、P99 延迟(客户端直方图)、连接数。
  2. 内存与持久化面板used_memory_rssmem_fragmentation_ratiolatest_fork_usec、AOF 重写状态。
  3. 复制与集群面板master_link_statusslave_repl_offset 延迟、cluster_statecluster_slots_fail
  4. 安全与错误面板:认证失败次数、rejected_connectionsevicted_keys

二、四大故障的检测、告警与自愈策略

1. 内存飙升

  • 检测规则
    • redis_memory_used_bytes / redis_memory_max_bytes > 0.8(预警)
    • > 0.9(严重告警)
    • mem_fragmentation_ratio > 1.5(碎片告警)
  • 告警动作:企业微信通知 SRE,附带 --bigkeys 扫描结果(每日低峰自动执行并缓存报告)。
  • 自愈脚本
    • 若碎片率过高,低峰期自动开启 activedefrag yes
    • 若是数据集膨胀,自动分析 Top Key 白名单,对允许删除的大 Key 执行 UNLINK(需二次人工确认)。
    • 终极手段:触发集群扩容,增加分片以分担内存。

2. 缓存雪崩

  • 检测规则
    • rate(redis_expired_keys_total[1m]) > baseline * 5keyspace_hits_ratio < 0.5
  • 告警动作:严重告警,同时触发 DB 保护机制。
  • 自愈策略
    • 在 API 网关层开启针对该业务的下游限流,将 QPS 压低到 DB 安全水位。
    • 缓存代理层自动返回逻辑过期数据(若应用支持)。
    • 通知配置中心,临时调大缓存 TTL 的随机范围(需应用支持动态调整)。
    • 预热脚本自动运行,快速重建热点 Key。

3. 主从断开

  • 检测规则
    • redis_master_link_status == 0 持续超过 30 秒。
  • 告警动作:通知 DBA 和网络团队。
  • 自愈策略
    • 首先检测网络,如果是节点宕机,自动用新实例替换并从主库全量同步(低峰期执行)。
    • 若主库压力过大,暂时将读请求全部切到主库,直到从库恢复(修改负载均衡权重)。
  • 预防repl-backlog-size 预设为 256MB,减少全量同步概率。

4. 集群脑裂

  • 检测规则
    • 同时存在两个节点 role:mastermaster_replid 不同(需自定义脚本检测)。
    • 哨兵日志检测到 +odown 后紧随 +switch-master,且旧主仍有写入流量。
  • 告警动作:立即 Critical 告警,并拉群应急。
  • 自愈策略
    • 自动隔离旧主:脚本接到告警后,在旧主库上执行 CONFIG SET min-replicas-to-write 0(或 CLIENT PAUSE 命令暂停写入),然后 CLIENT KILL 断开所有客户端连接,使其停止写入。
    • 新主库继续服务,待网络恢复后,旧主强制全量同步。
    • 数据冲突处理:人工介入,从备份或日志修复。
  • 预防:配置 quorum=N/2+1min-replicas-to-write=1min-replicas-max-lag=10

三、应急预案演练

定期进行故障演练,验证监控告警和自愈脚本的准确性,确保团队熟悉决策树路径。最终目标是将平均故障恢复时间(MTTR)从 30 分钟缩短至 5 分钟以内。


文末 Redis 排障工具速查表

工具作用域关键命令典型现象映射
redis-cli --bigkeys大Key扫描redis-cli --bigkeys内存飙升, 慢查询
redis-cli --memkeys内存Key扫描redis-cli --memkeys内存占用 Top Key
SLOWLOG慢命令分析SLOWLOG GET 10CPU飙高, 延迟增加
MONITOR实时监控(生产慎用)MONITOR调试性能瓶颈 (风险)
INFO all全模块指标INFO memory, INFO stats几乎所有故障初诊
LATENCY DOCTOR延迟诊断LATENCY DOCTOR延迟毛刺, fork耗时长
MEMORY STATS内存分布MEMORY STATS碎片, 内存飙升
MEMORY USAGE key单个Key内存MEMORY USAGE key定位大Key具体大小
MEMORY DOCTOR内存诊断报告MEMORY DOCTOR内存问题综合建议
redis-cli --stat实时监控redis-cli --stat实时QPS/延迟观察
redis-cli --latency网络延迟redis-cli --latency网络问题排查
Prometheus + Grafana长期监控与告警redis_exporter metrics趋势分析, 自动告警
redis-check-aofAOF 修复redis-check-aof --fixAOF 损坏无法启动

延伸阅读

  • 《Redis 设计与实现》
  • 《Redis 深度历险:核心原理与应用实践》
  • Redis 官方文档 Troubleshooting 部分
  • Martin Kleppmann 关于 Redlock 的分析文章

本宝典至此,通过体系化的反模式梳理与排查决策树,希望能为你的生产实践提供一套可靠的排障框架。将理论内化为排障直觉,方能在故障面前从容不迫。