一次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 | used_memory:8388557312 # 约 8GB |
Redis 已用内存 7.81GB,最大内存限制 8GB,使用率 97.7%,几乎满了。
**关键发现:maxmemory_policy 为 noeviction**,意味着当内存满时,Redis 不会主动淘汰任何键,而是直接拒绝所有写操作(包括 SET、HSET、LPUSH 等),返回 OOM 错误。
第二步:分析内存占用
查看 Redis 内存使用详情:
1 | redis-cli -h redis-prod-01 -p 6379 -a <password> MEMORY DOCTOR |
输出提示内存碎片率正常,但总体内存占用过高。
使用 DEBUG JMAP 或 redis-rdb-tools 分析大 Key:
1 | # 扫描占用内存最多的 key(生产环境谨慎使用 KEYS,使用 SCAN 替代) |
发现几个异常的大 Key:
1 | Biggest string found so far '"product:detail:cache:all"' with 524288000 bytes # 500MB! |
product:detail:cache:all 这个 Key 存储了 500MB 的数据,这是一个”全量商品缓存”,是某开发同学为了提高商品详情接口性能,将全部商品数据序列化后存入一个 Redis Key 中。
activity:log:queue 是一个活动日志队列,因为消费者处理不及,积压了将近 300 万条记录。
第三步:追溯大 Key 的产生原因
通过查看应用代码和 Redis 写入监控,发现:
product:detail:cache:all:大促前夕,开发同学为了”预热缓存”,写了一个脚本把全量商品(约 5 万个商品,每个商品序列化后约 10KB)存入一个 String Key。这个 Key 在大促当天被反复覆盖写入(每次预热都覆盖一次),每次写入耗时 2-3 秒,本身就是一个隐患。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 | # Redis 4.0+ 支持 UNLINK 命令(异步删除) |
UNLINK 命令将 Key 的删除操作交给后台线程,主线程几乎无阻塞。
第三步:截断积压 List
1 | # 保留最新的 10000 条,其余丢弃(或者清空) |
第四步:修正商品缓存设计
与开发同学沟通,将”全量商品缓存”改为按商品 ID 分散存储:
1 | product:detail:{product_id} → 单个商品数据(约 10KB/key) |
并设置合理的 TTL(如 1 小时),避免永不过期。
第五步:修复消费者积压问题
扩容消费者实例数量(从 2 个扩到 8 个),加速消耗积压队列。
根因分析
根本原因:缓存设计不合理 + maxmemory 策略配置错误。
将大量数据存入单个 Redis Key,是典型的大 Key 反模式,不仅占用大量内存,还会导致网络带宽和序列化/反序列化的额外开销。
使用
noeviction策略对于缓存场景来说是错误选择——它适用于数据不能丢失的持久化场景(如队列),但对于普通缓存应改用allkeys-lru或volatile-lru。没有预估大促期间的内存需求,也没有设置合理的内存告警阈值(应在 80% 时告警)。
预防措施
1. Redis 内存监控告警
在 80% 使用率时告警,90% 时紧急告警,不等到 OOM 再处理。
2. 大 Key 定期扫描
通过 redis-cli --bigkeys 或 redis-rdb-tools 分析 RDB 文件,定期检查大 Key:
1 | # 分析 RDB 文件(离线分析,不影响线上) |
3. 禁止大 Key 设计规范
在代码规范中明确:单个 Redis Key 的 Value 大小不超过 1MB,List/Hash/Set 的元素数量不超过 5000。
4. 缓存淘汰策略合理化
纯缓存场景统一使用 allkeys-lru,结合 TTL 管理数据生命周期。
5. 生产变更前做容量评估
大促类活动前,评估 Redis 内存峰值需求,提前扩容。
总结
这次事故表明,Redis 的配置和使用规范同样需要纳入代码审查流程。一个大 Key、一个错误的淘汰策略,在平时可能不显眼,但到了业务高峰期就会变成定时炸弹。
对于 Redis 运维,除了关注可用性,还要重点关注大 Key、热 Key、内存使用率、慢查询这四个维度,建立起常态化的监控和审计机制。