概述
本系列从 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 stats 中 total_commands_processed 的瞬时速率下降,因为主线程被阻塞无法处理新的命令。
1.1.3 排查思路
- 初步定位:当出现超时时,第一时间登录 Redis 节点,执行
redis-cli --latency-history观察延迟峰值,或者redis-cli --stat观察实时 QPS 与延迟变化,可以看到周期性或偶发的巨大延迟尖峰。 - 慢日志分析:执行
SLOWLOG GET 20,按照耗时降序排列,发现最慢的命令是对特定user:profile:*的DEL操作,耗时动辄几百毫秒。 - Key 大小确认:使用
redis-cli --bigkeys扫描,可以发现类似[00.00%] Biggest string found 'user:profile:10082' has 10485760 bytes的输出。用MEMORY USAGE user:profile:10082可以精确计算出该 Key 实际占用的内存(可能略大于 10MB,因为有 SDS 头和分配器开销)。 - 关联业务:排查这些大 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 修正方案
- 拆分存储:将大 JSON 打散,按照业务维度存入 Hash 或不同的 String Key。
// 拆分为多个小 Key jedis.hset("user:" + userId + ":basic", "name", name); // < 1KB jedis.hset("user:" + userId + ":orders", "list", orders); // 按时间分片 - 异步删除:Redis 4.0 引入的
UNLINK命令可以在后台线程中逐步回收内存。在 7.x 中,UNLINK非常成熟,它仅从键空间解除 Key,然后由lazyfree线程池异步释放内存,主线程几乎无阻塞。
也可以全局开启惰性删除UNLINK user:profile:10082lazyfree-lazy-server-del yes,使得DEL在某些情况下自动转为异步。 - 应用层改造:如果无法避免大 Key,则必须在代码中将删除操作替换为
UNLINK,并设置更短的超时重试时间。
1.1.6 最佳实践
- 在架构设计阶段,明文规定单个 Redis Key 的大小上限,例如 String 不超过 10KB,集合成员不超过 5000。
- 将
redis-cli --bigkeys加入定期巡检(如每日低峰期),配合--memkeys分析内存占用。 - 监控
mem_fragmentation_ratio和used_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 排查思路
- 初步发现:通过 Prometheus 告警
used_memory_dataset升高。 - 定位问题 Key:
redis-cli --memkeys或redis-cli --bigkeys列出内存占用 Top 的 Key,发现大量user:*:follows集合。 - 检查编码:执行
OBJECT ENCODING查看这些集合的内部编码,发现均为hashtable。查看配置CONFIG GET set-max-listpack-entries,默认值为 128。当集合大小超过 128 时,Redis 自动将内部编码从紧凑的listpack(或旧版intset)转换为hashtable(即dict)。 - 内存计算:
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 修正方案
-
调整阈值:若业务明确需要大集合,可以适当调高阈值,避免自动转换。
set-max-listpack-entries 100000 set-max-listpack-value 128需权衡:
listpack在大型集合上的读写操作都是 O(N),性能会下降,适合数据量在阈值以下且无频繁更新。对于 5 万成员且频繁SADD/SREM,listpack可能引发性能问题。 -
分片拆解:更稳妥的方案是进行应用层分片。将一个大 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 发现大量的 ZADD 和 ZRANK 耗时在 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 排查思路
- CPU 确认:
INFO cpu显示used_cpu_sys和used_cpu_user很高,并且与命令处理相关。 - 慢命令分析:
SLOWLOG GET 50过滤出ZADD和ZRANK,并观察其执行耗时与请求量的关系。 - 热点 Key 识别:
redis-cli --hotkeys或通过OBJECT IDLETIME辅助发现leaderboard是绝对热点。 - 原理映射:根据第 3 篇对
skiplist跳表的实现剖析,我们知道每个节点维护span字段用于快速计算排名。每次插入或更新分数时,Redis 需要重新计算插入路径上所有节点的span,这是一个局部 O(logN) 但消耗 CPU 的操作。高频率的更新导致跳表的span不断被重算,同时ZRANK也需要通过累加span来获取排名,二者叠加造成 CPU 瓶颈。
1.3.4 根因分析
详细根因见第 3 篇对 zset 内部编码的源码分析。Redis 中 zset 由 skiplist 和 dict 共同实现。跳表节点结构如下(简化):
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 修正方案
- 异步聚合写入:将分数变化先缓存到本地队列,每 100ms 批量合并后再更新 Redis,减少直接写入频次。
- 本地缓存排行榜:在应用实例内存中维护一份 Top N 榜单,每 1 秒从 Redis 异步刷新一次,读请求直接走本地缓存,不再频繁调用
ZREVRANGE。 - 拆分排行榜:将大榜单按赛区、时间段等因素拆分为多个小榜单,减少单 Key 的写并发。
- 使用其他结构:如果业务只是需要近似排名,可考虑 Redis Stack 的
Bloom或Top-K功能,或者使用Stream结合离线计算。
1.3.6 最佳实践
- 高并发排行榜必须进行写聚合和读缓存,绝不能让 Redis 直接面对每次的单个写入与实时查询。
- 利用
LATENCY和SLOWLOG持续监控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 排查思路
- 快速定位:通过监控告警发现 CPU 飙升,立即登录服务器执行
redis-cli SLOWLOG GET 1捕获到KEYS命令。 - 确认影响:
INFO commandstats显示cmdstat_keys:calls=1,usec=4500000,usec_per_call=4500000。 - 原理回溯:根据第 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 排查思路
- 应用监控:发现某台机器堆内存陡增,dump 堆文件分析,发现大量反序列化的字符串。
- Redis 慢日志:
SLOWLOG GET 10捕获到SMEMBERS online:users。 - Key 大小检查:
MEMORY USAGE online:users确认该 Set 占用数百 MB。 - 原因:
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 commandstats 中 sinter 的调用次数与总耗时异常。MEMORY USAGE 查看两个集合的大小。
1.6.4 根因分析
SINTER 的算法复杂度为 O(N*M),其中 N 是最小集合的元素个数,M 是集合数量。Redis 会遍历最小集合的所有元素,然后依次在其他集合中执行 SISMEMBER 检查。对于百万级集合,这会产生海量的 CPU 计算。该过程在主线程执行,导致长时间阻塞。
1.6.5 修正方案
- 离线计算:将交/并集计算转移到离线分析系统(如 Spark),Redis 仅作为存储。
- 应用层优化:利用
SCARD获取集合大小,选择最小的集合进行过滤,甚至可以在客户端实现分页交集。 - 数据结构变更:如果交集查询频繁,可预先计算并存储结果。
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 排查思路
INFO memory查看used_memory接近maxmemory。INFO stats中evicted_keys为 0,同时total_error_replies增加。CONFIG GET maxmemory-policy确认为noeviction。
2.1.4 根因分析
noeviction 策略在所有内存淘汰策略中优先级最高,即永不淘汰任何 Key,内存满时直接拒绝写操作。对于纯缓存场景,这种设置会导致缓存不可用。
2.1.5 修正方案
根据业务场景选择淘汰策略:
- 如果所有 Key 都是缓存,可选用
allkeys-lru或allkeys-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_hits和keyspace_misses,计算命中率。- 检查启动日志,发现缓存无预热步骤。
2.2.4 根因分析
冷启动时缓存为空,所有请求直接穿透到数据库,形成瞬时高峰。高并发下这种冲击可能直接拖垮数据库。
2.2.5 修正方案
设计缓存预热脚本:
- 在服务启动后,通过离线批处理将热点数据加载到 Redis。
- 使用
MSET或Pipeline批量写入。 - 预热完成前,将服务的流量入口进行灰度或限流。
2.2.6 最佳实践
参见第 6 篇 Spring Cache 整合,结合 @PostConstruct 或 ApplicationRunner 执行预热,并监控缓存命中率。
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.RESERVE 和 BF.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 == null 为 true,该 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 最佳实践
- 明确
@Cacheable的unless和condition的作用时机。 - 缓存空值时设置较短的 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 persistence中aof_fsync_pending堆积。LATENCY DOCTOR或LATENCY 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 persistence 中 aof_rewrite_in_progress 经常为 1,latest_fork_usec 持续在几百毫秒。系统 CPU 和内存周期性波动,伴随客户端超时。
3.3.3 排查思路
- 监控
aof_current_size和aof_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_size 与 aof_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 stats中latest_fork_usec:450000。INFO memory中used_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 排查思路
- 检查哨兵日志,发现有
+odown和failover_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 replication中repl_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 修正方案
- 调整 quorum 为多数派。
- 配置
min-replicas-to-write 1和min-replicas-max-lag 10。 - 客户端在捕获到连接中断或
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/.ssh 和 CONFIG SET dbfilename authorized_keys,再执行 BGSAVE,将自己的 SSH 公钥写入服务器 authorized_keys,最终获得服务器控制权。
5.2.3 排查思路
- 安全团队发现 SSH 登录异常,检查
/root/.ssh/authorized_keys多了不明公钥。 - Redis 日志显示有
CONFIG SET命令执行。 INFO commandstats中config|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。
- 重命名或禁用
CONFIG、DEBUG、SAVE、BGSAVE等危险管理命令。
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,调试使用 SLOWLOG、LATENCY 或外部追踪工具。
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 等模块的详尽指标。
- LATENCY:
LATENCY 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, LATENCY | SLOWLOG GET 10; INFO commandstats 中高消耗命令 | KEYS *, SORT, 高频 ZRANK, SINTER |
| 内存飙升 | INFO memory, MEMORY STATS, --bigkeys | used_memory_rss; mem_fragmentation_ratio; 大Key扫描 | 大Key未拆分,集合编码升级,内存碎片 |
| 缓存命中率低 | INFO stats | keyspace_hits vs keyspace_misses;expired_keys 突增 | 雪崩(同时过期),穿透,预热缺失 |
| 主从全量同步频繁 | INFO replication, LATENCY | repl_backlog_size; master_repl_offset 差值 | backlog过小,网络不稳定 |
| 集群不可用/重定向 | CLUSTER INFO, CLUSTER NODES | cluster_state:fail; cluster-node-timeout; slot分布 | 超时过小,require-full-coverage |
| 写入被拒绝 | INFO stats, CONFIG GET maxmemory* | rejected_connections; evicted_keys; maxmemory-policy | maxclients耗尽,内存满且noeviction |
| 持久化性能问题 | INFO persistence, LATENCY DOCTOR | aof_fsync_pending; aof_rewrite_in_progress; latest_fork_usec | always fsync,重写风暴,fork阻塞 |
| 安全未授权 | ACL LIST, CONFIG GET requirepass | user default on nopass; protected-mode no | 无密码,公网暴露 |
| OOM / 慢查询 | redis log, MEMORY STATS, SLOWLOG | 进程被杀;SMEMBERS, SORT 等 | 大集合全量返回,未用SCAN |
| 客户端超时 | CLIENT LIST, LATENCY | omem(输出缓冲区)高;age时间大 | 慢命令阻塞,网络延迟 |
| 脑裂 | 哨兵日志, INFO replication | 双主 role:master;+odown事件 | quorum过小,min-replicas未配置 |
| AOF 损坏 | redis-check-aof | redis-check-aof --fix 截断 | 断电,磁盘满,写异常 |
| 数据丢失 | INFO persistence, 备份 | RDB/AOF 文件状态 | 无持久化,AOF截断,脑裂 |
| 监控干扰 | CLIENT LIST | cmd=monitor 客户端 | 生产环境执行 MONITOR |
7. 标准化排查决策树
面对线上故障,可以按照以下决策树快速定位问题。
“CPU 飙高”分支
- SLOWLOG:立即
SLOWLOG GET 20,找出耗时最长的命令。- 如果包含
KEYS→ 立刻找到执行者并优化为SCAN,配置重命名禁用。 - 如果包含
SORT→ 检查数据集大小,改为应用层排序或弃用。 - 如果包含
ZADD,ZRANK→ 检查是否热点 Key 过高频写入,需聚合/拆分。 - 如果包含
SINTER,SUNION→ 大集合运算,迁移至离线或拆分。
- 如果包含
- INFO commandstats:查看总调用量和总耗时,识别被高频调用的潜在慢命令。
- LATENCY DOCTOR:辅助判断延迟是否由 fork、I/O 等非命令引起。
“内存飙升”分支
- INFO memory:获取
used_memory_rss,used_memory_dataset,mem_fragmentation_ratio。- 碎片率 > 1.5 → 内存碎片问题,使用
MEMORY STATS和MEMORY DOCTOR分析,考虑activedefrag或重启。 - 碎片率正常 → 数据集确实增加。
- 碎片率 > 1.5 → 内存碎片问题,使用
- redis-cli --bigkeys 与
--memkeys:找出占用内存最多的 Key。- 如果是大 String → 拆分为多个小 Key 或 Hash。
- 如果是大集合 → 检查编码,考虑分片。
- SLOWLOG:检查是否有
SMEMBERS,KEYS等导致内存瞬时冲高的操作。 - MEMORY DOCTOR:执行并获取自动化建议。
“缓存命中率低”分支
- INFO stats:计算
keyspace_hits_ratio。 - 分析过期:
expired_keys瞬时增量 → 雪崩,检查 Key 的 TTL 是否集中,增加随机抖动。 - 分析穿透:如果
keyspace_misses很大且多为不存在 ID → 配置布隆过滤器,并缓存空值。 - 分析击穿:热点 Key 重建日志,数据库有高并发查询 → 加强分布式锁逻辑或使用逻辑过期。
- 检查预热:如果刚重启过,则为预热不足。
“集群不可用”分支
- CLUSTER INFO 和 CLUSTER NODES:查看集群状态。
cluster_state:fail且 slot 未完全分配 → 可能是cluster-require-full-coverage yes且正在迁移。- 节点状态为
fail→ 检查cluster-node-timeout是否过小,或节点确实宕机。
- 脑裂检查:如果采用哨兵,检查日志是否有双主。查看
INFO replication。 - 客户端侧:大量
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。
排查思路
- 快速止血:通过
CLIENT LIST找到发起KEYS的客户端 IP 和端口,确认是内部运维脚本。紧急执行CLIENT KILL addr:192.168.1.100:45678断开该连接,CPU 立刻回落。 - 溯源:检查该 IP 的定时任务,发现运维部署了一个清理过期 Session 的 Shell 脚本,里面使用了
redis-cli KEYS "session:*" | xargs redis-cli DEL。 - 验证:测试环境模拟 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家族。 - 在配置文件中禁用
KEYS、FLUSHDB等危险命令。 - 建立代码审查和上线前安全检查清单。
8.2 Redis 内存突然从 10GB 飙升至 30GB,但 Key 数量并未明显增加,如何定位根因?
场景描述
监控告警:Redis 内存使用量从 10GB 跳升至 30GB,但 DBSIZE 显示 Key 总数变化不大(从 50 万只增加到 52 万)。
排查思路
- 宏观检查:
INFO memory重点看:可见确实是真实数据增加,非内存碎片。used_memory_dataset:28000000000 # 数据集占用 28GB mem_fragmentation_ratio:1.02 # 碎片率正常 - 定位大 Key:
redis-cli --bigkeys输出:发现某个 Set 有 450 万成员,而之前设计容量仅为 5000 人。Biggest set found 'online:users:3' has 4500000 members - 检查内部编码:
OBJECT ENCODING online:users:3返回hashtable,而新建的小集合编码为listpack。已知集合元素数超过set-max-listpack-entries(默认 128)就会从listpack升级为hashtable。hashtable每个元素大约增加 20~30 字节开销,450 万成员的额外开销就高达近 100MB,再加上实际数据,总体内存爆炸。 - 验证:
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,大量慢查询导致连接池耗尽,订单创建超时。
排查思路
- 确认缓存指标:
INFO stats中:命中率约 50%,且keyspace_hits:1200000 keyspace_misses:1200000expired_keys在过去 1 分钟暴增了 10 万次。由此怀疑是大量 Key 集中过期导致的雪崩。 - 抽样检查 TTL:随机抽取缓存 Key 的 TTL,发现大量商品缓存 Key 的剩余时间都在 0~60 秒之间波动,且原本设置的过期时间都是 3600 秒整。正是由于没有随机抖动,这些 Key 都在同一时间点附近过期。
- 排除穿透与击穿:
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 使用率周期性飙高。
排查思路
- 检查复制状态:
INFO replication在主库看到:从库首次重连后,偏移量差距很大。master_repl_offset:123456789012repl_backlog_size显示为默认的 1MB。 - 计算写入量:通过监控估算主库每秒写入约 500KB,而从库断开一般持续 2-3 秒,产生的写入量约 1.5MB,已经超过 1MB 的 backlog 缓冲区。这导致部分同步失败,只能全量同步。
- 检查网络:网络监控显示从库和主库之间有微小丢包,但能很快恢复。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 机房旧主库被降级为从库,全量同步导致期间写入的数据丢失。
排查思路
- 哨兵日志:在 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 - Redis 日志:旧主库上,切换后依然有客户端写入,且
min-replicas-to-write为 0(未配置)。 - 数据对比:在新主库上发现,某些 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 以上。
排查思路
- 确认 fork 耗时:
INFO stats中latest_fork_usec:620000。 - 查看内存:
INFO memory显示used_memory_rss:15GB。当前实例为单节点,内存 15GB。 - 系统检查:
/proc/sys/vm/overcommit_memory为 0,也增大了 fork 失败风险。
根因分析
Linux fork() 调用会复制父进程的页表,内存越大页表复制越耗时。15GB 的 Redis 实例,其页表大小可能达到数百 MB,复制时间轻松达到数百毫秒,期间主线程完全阻塞。
修复方案
- 短期:关闭自动
save参数,改为在低峰期手动执行,或完全关闭 RDB,改用appendonly yes和aof-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>
排查思路
- 备份:立即备份损坏的
appendonly.aof。 - 修复:执行
redis-check-aof --fix appendonly.aof,日志显示Successfully truncated AOF,文件大小减小。 - 启动:成功启动 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。
排查思路
- 检查连接数:
INFO clients显示connected_clients:10000,而maxclients为 10000,rejected_connections为 125。连接数已满。 - 检查内存:
INFO memory显示used_memory:4.2GB,maxmemory:4GB,maxmemory-policy:noeviction。内存满了且策略为不淘汰,拒绝写入。 - 分析连接:
CLIENT LIST发现大量空闲连接未释放,可能是应用连接池泄漏。
根因分析 两个并发问题:连接数耗尽和内存满且 noeviction。应用未正确归还连接,导致新连接被拒。内存因不淘汰而无法写入。
修复方案
- 紧急:临时调高
maxclients并杀死空闲连接CLIENT KILL IDLE 300。切换maxmemory-policy为allkeys-lru(需重启或CONFIG SET热修改)。 - 长期:修复应用连接池泄漏,并设置合理的淘汰策略。
最佳实践
- 设置合适的
maxclients,并监控连接数趋势。 - 缓存场景务必使用
allkeys-lru或allkeys-lfu。
8.9 SORT 命令在 1000 万成员的 List 上执行导致 OOM,如何优化?
场景描述
运营人员执行一个数据导出脚本,调用了 SORT big_list LIMIT 0 100000,该列表包含 1000 万条记录。Redis 内存瞬间从 8GB 飙升至 32GB,触发系统 OOM Killer 杀死了进程。
排查思路
- 日志分析:Redis 日志显示
SORT command causing out of memory。 - 命令检查:
SLOWLOG看到正在运行的SORT,MEMORY 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,半小时后才发现系统报警。
排查思路
INFO stats看到instantaneous_ops_per_sec腰斩。CLIENT LIST找到cmd=monitor的客户端连接。- 执行
CLIENT KILL addr=10.0.0.5:56789后,QPS 瞬间恢复。
根因分析
MONITOR 将所有命令实时打印到该客户端,在高 QPS 下会产生巨大的 CPU 消耗(序列化)和输出缓冲区内存占用,通常导致性能下降 50% 以上。
修复方案
- 立即断开连接。
- 在配置文件中添加
rename-command MONITOR ""永久禁用。
最佳实践
- 生产环境严禁使用
MONITOR,用SLOWLOG和LATENCY代替。
8.11 Redis 被外部攻击者通过 CONFIG SET dir 写入 SSH 公钥,如何排查和加固?
场景描述
安全团队发现服务器被 root 登录,检查 /root/.ssh/authorized_keys 发现不明公钥,排查发现是通过 Redis 写入的。
排查思路
- Redis 日志:有
CONFIG SET dir /root/.ssh和CONFIG SET dbfilename authorized_keys记录。 - 配置检查:
protected-mode no,bind 0.0.0.0,requirepass为空。 - 攻击还原:攻击者连接到公网暴露的 6379 端口,
SET mykey "ssh-rsa AAA...",然后修改 dir 和 dbfilename,执行BGSAVE写入文件。
修复方案
- 删除恶意公钥。
- Redis 配置修改:
protected-mode yes,bind 127.0.0.1,requirepass设置强密码。 - 禁用危险命令:
rename-command CONFIG "",rename-command BGSAVE ""。
最佳实践
- Redis 绝对不能直接暴露在公网。
- 使用最小权限 ACL,重命名所有管理类命令。
8.12 CLUSTER FAILOVER 强制切换期间客户端大量抛出 MOVED 重定向错误,如何排查和优化?
场景描述
运维执行 CLUSTER FAILOVER 进行主从切换,切换过程中,应用日志突然刷出大量 redis.clients.jedis.exceptions.JedisMovedDataException: MOVED,部分请求失败。
排查思路
- 检查客户端:使用的是 Jedis,未配置自动重定向,只在连接初始化时获取一次槽位映射。
- 集群事件:
CLUSTER NODES显示 epoch 变更,slot 正在迁移。 - 原因:切换期间 slot 所有权变更,客户端缓存失效,请求发到旧节点导致 MOVED。
修复方案
- 客户端升级为 JedisCluster(内部处理 MOVED/ASK)或 Lettuce(内置自适应拓扑刷新)。
- 若无法升级,应用层需捕获
JedisMovedDataException,解析新地址并重试。
最佳实践
- 使用支持集群自动重定向的客户端,并配置合理的拓扑刷新间隔。
8.13 @Cacheable 的 unless 条件错误导致大量 NULL 值被缓存,如何排查和修复?
场景描述
Spring Boot 应用使用 @Cacheable 缓存商品信息,Redis 内存随时间异常增长,抽样发现许多 Key 的值为 null。
排查思路
redis-cli GET product:9999返回null,而数据库里 ID 9999 的商品确实不存在。- 查看代码:
原本想缓存空值防穿透,但@Cacheable(value = "product", key = "#id", unless = "#result == null")unless = "#result == null"的效果是:当结果为 null 时不缓存。可实际情况是,由于缓存未命中,方法执行返回 null,Spring 认为结果为 null 就不缓存,于是每次请求都穿透到 DB。但问题是为什么 Redis 里会有 null 值?可能应用中另有其他逻辑手动将 null 写入了缓存。 - 真正原因:另一个初始化方法错误地缓存了 null,或者开发在测试时手动 SET 了 null 字符串。
修复方案
- 修正
unless条件为unless = "false",或换成自定义CacheManager配置允许缓存 null 并设置较短的 TTL。 - 也可使用布隆过滤器防止穿透,不缓存 null。
最佳实践
- 清楚区分
condition和unless的作用时机。 - 对 null 值缓存必须配置独立 TTL,定期清理。
8.14 系统设计题:基于文中的决策树思想,为一套核心交易系统的 Redis 缓存层规划故障应急预案与监控体系,要求覆盖内存飙升、缓存雪崩、主从断开、集群脑裂四种典型故障的检测、告警与自愈策略。
背景需求 交易系统要求高可用、高一致性,缓存层采用 Redis Cluster(或哨兵模式)部署,日均 QPS 50 万。需要建立一套完整的可观测、可自愈的故障应对体系。
一、监控体系设计
使用 Prometheus + redis_exporter 采集指标,Grafana 可视化,Alertmanager 告警通知。核心监控面板划分:
- 黄金指标面板:QPS、命中率(
keyspace_hits_ratio)、P99 延迟(客户端直方图)、连接数。 - 内存与持久化面板:
used_memory_rss、mem_fragmentation_ratio、latest_fork_usec、AOF 重写状态。 - 复制与集群面板:
master_link_status、slave_repl_offset延迟、cluster_state、cluster_slots_fail。 - 安全与错误面板:认证失败次数、
rejected_connections、evicted_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 * 5且keyspace_hits_ratio < 0.5。
- 告警动作:严重告警,同时触发 DB 保护机制。
- 自愈策略:
- 在 API 网关层开启针对该业务的下游限流,将 QPS 压低到 DB 安全水位。
- 缓存代理层自动返回逻辑过期数据(若应用支持)。
- 通知配置中心,临时调大缓存 TTL 的随机范围(需应用支持动态调整)。
- 预热脚本自动运行,快速重建热点 Key。
3. 主从断开
- 检测规则:
redis_master_link_status == 0持续超过 30 秒。
- 告警动作:通知 DBA 和网络团队。
- 自愈策略:
- 首先检测网络,如果是节点宕机,自动用新实例替换并从主库全量同步(低峰期执行)。
- 若主库压力过大,暂时将读请求全部切到主库,直到从库恢复(修改负载均衡权重)。
- 预防:
repl-backlog-size预设为 256MB,减少全量同步概率。
4. 集群脑裂
- 检测规则:
- 同时存在两个节点
role:master且master_replid不同(需自定义脚本检测)。 - 哨兵日志检测到
+odown后紧随+switch-master,且旧主仍有写入流量。
- 同时存在两个节点
- 告警动作:立即 Critical 告警,并拉群应急。
- 自愈策略:
- 自动隔离旧主:脚本接到告警后,在旧主库上执行
CONFIG SET min-replicas-to-write 0(或CLIENT PAUSE命令暂停写入),然后CLIENT KILL断开所有客户端连接,使其停止写入。 - 新主库继续服务,待网络恢复后,旧主强制全量同步。
- 数据冲突处理:人工介入,从备份或日志修复。
- 自动隔离旧主:脚本接到告警后,在旧主库上执行
- 预防:配置
quorum=N/2+1,min-replicas-to-write=1,min-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 10 | CPU飙高, 延迟增加 |
| 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-aof | AOF 修复 | redis-check-aof --fix | AOF 损坏无法启动 |
延伸阅读:
- 《Redis 设计与实现》
- 《Redis 深度历险:核心原理与应用实践》
- Redis 官方文档 Troubleshooting 部分
- Martin Kleppmann 关于 Redlock 的分析文章
本宝典至此,通过体系化的反模式梳理与排查决策树,希望能为你的生产实践提供一套可靠的排障框架。将理论内化为排障直觉,方能在故障面前从容不迫。