一次Redis内存溢出导致缓存服务全部失效的排查实录

问题背景

某业务大促活动开始后约 2 小时,监控告警系统收到大量告警:

  • 数据库(MySQL)慢查询数量骤增,P99 响应时间从 50ms 升至 8000ms
  • 部分 API 接口 5xx 错误率达到 15%
  • Redis 实例的 rejected_connections 计数器快速增长

业务团队紧急联系运维排查,此时活动已进入高峰期,影响用户体验,需要快速定位并修复。


故障现象

  • Redis INFO 命令返回 used_memory 接近服务器总内存
  • Redis 日志中出现大量 OOM command not allowed when used memory > 'maxmemory' 错误
  • 应用日志中 Redis 写操作报错:COMMAND NOT ALLOWED When Used Memory > 'maxmemory'
  • MySQL 慢查询日志急剧增多,大量查询耗时 5-10 秒
  • 部分服务接口响应超时,用户请求报 504

排查过程

第一步:确认 Redis 状态

1
redis-cli -h redis-prod-01 -p 6379 -a <password> INFO memory

关键输出:

1
2
3
4
5
6
used_memory:8388557312          # 约 8GB
used_memory_human:7.81G
maxmemory:8589934592 # 8GB
maxmemory_human:8.00G
maxmemory_policy:noeviction # ⚠️ 不淘汰策略
mem_fragmentation_ratio:1.23

Redis 已用内存 7.81GB,最大内存限制 8GB,使用率 97.7%,几乎满了。

**关键发现:maxmemory_policynoeviction**,意味着当内存满时,Redis 不会主动淘汰任何键,而是直接拒绝所有写操作(包括 SET、HSET、LPUSH 等),返回 OOM 错误。

第二步:分析内存占用

查看 Redis 内存使用详情:

1
redis-cli -h redis-prod-01 -p 6379 -a <password> MEMORY DOCTOR

输出提示内存碎片率正常,但总体内存占用过高。

使用 DEBUG JMAPredis-rdb-tools 分析大 Key:

1
2
# 扫描占用内存最多的 key(生产环境谨慎使用 KEYS,使用 SCAN 替代)
redis-cli -h redis-prod-01 -p 6379 -a <password> --bigkeys

发现几个异常的大 Key:

1
2
3
Biggest string found so far '"product:detail:cache:all"' with 524288000 bytes  # 500MB!
Biggest hash found so far '"user:session:hash"' with 1258291 fields
Biggest list found so far '"activity:log:queue"' with 2847593 elements

product:detail:cache:all 这个 Key 存储了 500MB 的数据,这是一个”全量商品缓存”,是某开发同学为了提高商品详情接口性能,将全部商品数据序列化后存入一个 Redis Key 中。

activity:log:queue 是一个活动日志队列,因为消费者处理不及,积压了将近 300 万条记录。

第三步:追溯大 Key 的产生原因

通过查看应用代码和 Redis 写入监控,发现:

  1. product:detail:cache:all:大促前夕,开发同学为了”预热缓存”,写了一个脚本把全量商品(约 5 万个商品,每个商品序列化后约 10KB)存入一个 String Key。这个 Key 在大促当天被反复覆盖写入(每次预热都覆盖一次),每次写入耗时 2-3 秒,本身就是一个隐患。

  2. activity:log:queue:活动期间消费者处理速度远低于生产速度,积压数百万条未处理的日志条目,占用了大量内存。

第四步:评估立即处理方案

此时有几个选项:

方案 A:调大 maxmemory(临时方案)

  • 风险:服务器本身可用内存只有 12GB,Redis 已用约 8GB,系统还有其他进程,不建议调太高
  • 可临时调至 10GB,争取时间清理大 Key

方案 B:修改 maxmemory_policy 为 allkeys-lru(临时方案)

  • 让 Redis 自动淘汰最近最少使用的 Key
  • 风险:可能淘汰到重要的缓存数据,但好过直接拒绝写入

方案 C:立即删除大 Key(直接处理)

  • 删除大 String Key,释放 500MB 空间
  • 清空或截断积压 List

综合评估后,选择先临时调整 maxmemory_policy,再清理大 Key,最后重新评估内存上限


解决方案

第一步:临时修改淘汰策略(不重启)

1
redis-cli -h redis-prod-01 -p 6379 -a <password> CONFIG SET maxmemory-policy allkeys-lru

修改后,Redis 开始主动淘汰冷数据,写操作不再被拒绝,应用侧错误率立即下降。

第二步:删除大 Key(避免直接 DEL,防止阻塞)

直接 DEL 一个 500MB 的 Key 会阻塞 Redis 主线程数秒。使用异步删除:

1
2
# Redis 4.0+ 支持 UNLINK 命令(异步删除)
redis-cli -h redis-prod-01 -p 6379 -a <password> UNLINK "product:detail:cache:all"

UNLINK 命令将 Key 的删除操作交给后台线程,主线程几乎无阻塞。

第三步:截断积压 List

1
2
# 保留最新的 10000 条,其余丢弃(或者清空)
redis-cli -h redis-prod-01 -p 6379 -a <password> LTRIM "activity:log:queue" 0 9999

第四步:修正商品缓存设计

与开发同学沟通,将”全量商品缓存”改为按商品 ID 分散存储:

1
product:detail:{product_id}  →  单个商品数据(约 10KB/key)

并设置合理的 TTL(如 1 小时),避免永不过期。

第五步:修复消费者积压问题

扩容消费者实例数量(从 2 个扩到 8 个),加速消耗积压队列。


根因分析

根本原因:缓存设计不合理 + maxmemory 策略配置错误

  1. 将大量数据存入单个 Redis Key,是典型的大 Key 反模式,不仅占用大量内存,还会导致网络带宽和序列化/反序列化的额外开销。

  2. 使用 noeviction 策略对于缓存场景来说是错误选择——它适用于数据不能丢失的持久化场景(如队列),但对于普通缓存应改用 allkeys-lruvolatile-lru

  3. 没有预估大促期间的内存需求,也没有设置合理的内存告警阈值(应在 80% 时告警)。


预防措施

1. Redis 内存监控告警

在 80% 使用率时告警,90% 时紧急告警,不等到 OOM 再处理。

2. 大 Key 定期扫描

通过 redis-cli --bigkeysredis-rdb-tools 分析 RDB 文件,定期检查大 Key:

1
2
# 分析 RDB 文件(离线分析,不影响线上)
rdb --command memory dump.rdb | sort -rn -k3 | head -20

3. 禁止大 Key 设计规范

在代码规范中明确:单个 Redis Key 的 Value 大小不超过 1MB,List/Hash/Set 的元素数量不超过 5000。

4. 缓存淘汰策略合理化

纯缓存场景统一使用 allkeys-lru,结合 TTL 管理数据生命周期。

5. 生产变更前做容量评估

大促类活动前,评估 Redis 内存峰值需求,提前扩容。


总结

这次事故表明,Redis 的配置和使用规范同样需要纳入代码审查流程。一个大 Key、一个错误的淘汰策略,在平时可能不显眼,但到了业务高峰期就会变成定时炸弹。

对于 Redis 运维,除了关注可用性,还要重点关注大 Key、热 Key、内存使用率、慢查询这四个维度,建立起常态化的监控和审计机制。