当前位置: 代码网 > it编程>数据库>Redis > Redis大Key导致生产事故排查与解决方法

Redis大Key导致生产事故排查与解决方法

2026年09月22日 Redis 我要评论
一、事故回顾:30 分钟的"幽灵故障"某天,生产prod 突然大面积不可用——白屏报错、无法登录、接口超时。运维同学看到的现象是:接口api 层 6 个 p

一、事故回顾:30 分钟的"幽灵故障"

某天,生产prod 突然大面积不可用——白屏报错、无法登录、接口超时。运维同学看到的现象是:

  • 接口api 层 6 个 pod 在 3 分钟内反复重启(k8s 存活探针失败 → pod 被重建 → 再失败 → 再重建……)
  • 认证服务 持续刷 unavailablesecuritymanagerexception(shiro securitymanager 不可用)和 oauthaccesstoken.find.fail(token 查不到)
  • 生产 接口api服务 所有 pod 持续报 oauthaccesstoken.find.fail,用户带 token 来校验,认证服务查不到
  • 认证服务本身是活着的——登录接口的日志显示 token 正常签发

这一切看起来像是认证服务出了问题。重启认证服务?重启了,新 pod 启动后同样的异常立即复现

排查陷入僵局。直到有人看了当天上午的部署记录,才发现真正的"凶手"不是认证服务,而是 生产环境 当天部署的一个"性能优化"版本——一次性新增了 5 个 redis 缓存服务,其中一个在 redis 里写入了一个 约 600 kib 的大 key,每次用户进首页都去读它,最终拖垮了共享 redis,连锁反应波及认证服务,导致 app 全线不可用。

持续了接近 30 分钟,直到回滚代码。

二、代码变更:一次"善意"的缓存批量优化

2.1 变更全貌

事故版本相对于上一个稳定版本(以下称"基线版本"),一共变更了 54 个文件、新增约 2600 行代码。其中最关键的变更是一次性新增了 5 个 redis 缓存服务,全部使用 stringredistemplate 读写共享 redis:

缓存服务新增行数缓存内容key 设计大 key 风险
posenhanceservice267 行首页增强逻辑(调用方)
posenhancecacheservice140 行在售商品树biz:onsale:detail固定单 key极高
bankcacheservice133 行品牌列表biz:bank:brand-list(固定单 key)
vconfigcacheservice199 行商品系列配置biz:v:s-list:all(code 为空时)
xxxxcacheservice74 行商品列表mall:mallitem:[code](按code分片)

核心问题出在 posenhancecacheservice——它的 listgooddetail() 方法用了一个固定的单 key,value 是全量产品系列的 json 序列化,约 600kb。

2.2 基线版本 vs 事故版本对比

基线版本:首页页的 enhance() 方法直接 feign 调下游服务获取数据,完全不碰 redis

// 基线版本:直接 feign 调用,独立线程池
private map<long, list<versionvo>> loadonsalemap() {
    response<list<xyyyvo>> response =
        assemblerreadapi.listdetail(terminal, is_new);  // feign 调用
    // ... 处理响应
}

事故版本:新增 posenhancecacheservice,改为读写 redis:

// 事故版本:新增缓存服务,改为读写 redis
public list<xyyyvo> listsdetail() {
    string cachekey = "biz:onsale:detail";          // 固定单 key,无分片
    string cached = redis.get(cachekey);             // 每次请求都读这个大 key
    if (cached != null) {
        return json.parsearray(cached, xyyyvo.class);  // 反序列化 ~600kb
    }
    // 缓存未命中 → 回源
    response<list<xyyyvo>> response = assemblerreadapi.listdetail(1, 1);
    list<xyyyvo> data = response.getdata();
    redis.set(cachekey, json.tojsonstring(data), 1, timeunit.days);  // 写入 ~600kb 大 key
    return data;
}

同时,poscontroller 也被修改,在返回响应前新增了 enhance() 调用:

// 基线版本:直接返回,无增强
response<list<posdto>> response = posservice.getrediskeysvalue(pagecode, ...);
return response;

// 事故版本:返回前先做 enhance(会触发大 key 读取)
response<list<posdto>> response = posservice.getrediskeysvalue(pagecode, ...);
if (response.success()) {
    posenhanceservice.enhance(response.getdata(), pagecode);  // ← 每次进页都读大 key
}
return response;

2.3 调用链

每次用户进入首页都会触发以下调用链:

用户进入首页页(高频)
  → get /pos/detail
  → poscontroller.posdetails()
  → enhanceservice.enhance()
  → loadonsalemap()
  → cacheservice.listdetail()
  → redis.get("biz:onsale:detail")         ← 读 ~600kb 大 key
  → json.parsearray(cached, xyyvo.class)  ← 反序列化 ~600kb

而首页是 生产 的核心高频页面,用户每次进页都会触发。

2.4 key 设计问题对比

┌────────────────────────────────────────────────────────────────────┐
│  5 个缓存服务的 key 设计对比                                        │
├──────────────────────┬──────────┬──────────┬───────────────────────┤
│  缓存服务             │ key 粒度 │ 预估体积  │ 大 key 风险            │
├──────────────────────┼──────────┼──────────┼───────────────────────┤
│  onsale:detail       │ 固定单key │ ~600kb   │ ⛔ 极高 — 全量商品树  │
│  onsale:title:[code] │ 按code分片 │ < 1kb    │ ✅ 安全               │
│  all-carousel        │ 固定单key │ ~50kb    │ ⚠️ 中等              │
│  bank:brand-list     │ 固定单key │ ~10kb    │ ⚠️ 中等              │
│  x:y:all  │ code空时全量│ ~100kb  │ ⚠️ 中等              │
│  smallxy:[code] │ 按code分片 │ < 5kb    │ ✅ 安全              │
└──────────────────────┴──────────┴──────────┴───────────────────────┘

  → onsale:detail 是唯一的固定单key + 大体积组合,成为雪崩的导火索

三、雪崩过程:一个大 key 如何拖垮整条链路

3.1 redis 单线程的致命弱点

redis 是单线程架构——所有命令在主线程串行执行。一条命令耗时越长,排在后面的命令等待越久。

读取一个 600kb 的 value,redis 主线程需要:

  1. 从内存中找到对应 value(可能跨多页内存)
  2. 序列化为 resp 协议格式
  3. 通过网络 socket 发送给客户端

这个过程在低并发下可能只需几毫秒,但高并发下会严重积压。假设每次读取耗时 20ms,qps 500 的情况下,redis 主线程每秒要花 10 秒来处理这个 key 的读取——远超 1 秒的处理能力,后面的命令全部排队等待。

3.2 雪崩链路

用户进首页(高频)
    ↓
enhance() → redis.get("biz:onsale:detail")  ← 600kb 大 key
    ↓
redis 主线程被大 key 读取阻塞
    ↓
共享 redis 实例的认证服务 token 查询也被阻塞 → 超时
    ↓
生产  拿 token 校验 → 认证服务查不到 → authenticationexception
    ↓
prod 收到 401 → 请求超时 → broken pipe → 500 服务器异常
    ↓
prod 的 tomcat 线程被 redis 读取 + json 反序列化占满
    ↓
线程池耗尽 → 新请求排队 → 响应超时 → 存活探针失败
    ↓
k8s 杀 pod → 重建 → 立即又被打满 → 反复重启
    ↓
prod 大面积不可用(白屏、无法登录=报错)

3.3 认证服务为什么也报错?

这是排查时最容易踩的坑——认证服务的异常是连锁反应,不是根因

prod  大 key 拖住 redis
    ↓
认证服务的 token 存储、shiro session 也用同一个 redis
    ↓
token 查询超时 → oauthaccesstoken.find.fail
    ↓
shiro session 获取失败 → unavailablesecuritymanagerexception
    ↓
看起来像是认证服务的 shiro 配置问题

重启认证服务无效,因为 redis 仍被阻塞,新 pod 同样受影响。

四、排查过程:从误判到定位

第一阶段:误判为认证服务故障(约 10 分钟)

看到 unavailablesecuritymanagerexceptionoauthaccesstoken.find.fail,第一反应是认证服务的 shiro 配置有问题。尝试重启认证服务,故障立即复现——说明不是应用自身的问题,而是外部依赖被阻塞。

第二阶段:查看部署时间线(约 10 分钟)

搜索日志中的 tomcat started 关键字,还原当天的部署记录:

时间部署服务
2:11prod(最先部署)
2:14用户业务服务
2:17认证服务开始报错(prod 部署后 6 分钟)
2:28认证服务尝试重启(无效)

关键发现:prod 最先部署,认证服务的异常是在 prod 部署 6 分钟后才出现的

第三阶段:代码 diff 定位根因(约 15 分钟)

拿事故版本与上一个稳定版本做全量 diff,发现事故版本一次性新增了 5 个 redis 缓存服务(约 800 行新增代码)。其中 posenhancecacheservicelistdetail() 方法用了一个固定单 key,value 约 600kb。

关键代码:

  • biz:onsale:detail 是一个固定单 key,存储全量在售商品
  • value 是 json 明文,体积约 600kb(远超 redis 建议的 10kb 大 key 阈值)
  • 每次用户进页都会读取并反序列化这个大 key
  • 该 redis 实例被认证服务的 token 存储共享

验证

假设结果
删除缓存能解决?不能。 下一个请求会回源并重新写入同样的大 key
重启认证服务能解决?不能。 redis 仍被阻塞
回滚 prod缓存逻辑?可以。 去掉 redis 读写后恢复正常

五、为什么"删除缓存"不能解决问题

这是很多人会想到的第一个"快速止血"方案:

del biz:onsale:detail

删了之后会发生什么?

del biz:onsale:detail
    ↓
下一个请求 redis.get() 返回 null(缓存没了)
    ↓
回源 assemblerreadapi.listdetail()
    ↓
redis.set("biz:onsale:detail", json.tojsonstring(data), 1, days)
    ↓
600kb 大 key 重新写入 → 问题立即复现

更糟糕的是,高并发下删除缓存会导致缓存击穿:多个请求同时发现缓存不存在,同时回源下游服务,可能把下游也打挂。

正确做法:回滚代码 + 等待大 key 自然过期。

六、redis 大 key 的危害

6.1 什么是大 key

redis 单个 key 的 value 体积过大,通常建议:

value 大小风险等级
< 1 kb安全
1 ~ 10 kb正常
10 ~ 100 kb需关注
100 kb ~ 1 mb高危
> 1 mb危险

本案例的大 key 约 600kb,属于高危级别。

6.2 大 key 的四大危害

┌──────────────────────────────────────────────────────┐
│                                                      │
│  1. 阻塞主线程                                       │
│     redis 单线程,大 key 读写耗时长,阻塞所有命令     │
│                                                      │
│  2. 网络带宽占用                                      │
│     600kb × 500 qps = 300mb/s 带宽,可能打满网卡     │
│                                                      │
│  3. 内存分配抖动                                      │
│     大 key 频繁分配/释放,导致内存碎片和 gc 压力       │
│                                                      │
│  4. 删除时阻塞                                        │
│     del 一个大 key 会阻塞主线程数秒甚至更久           │
│                                                      │
└──────────────────────────────────────────────────────┘

6.3 为什么会影响认证服务

本案例中,prod的业务缓存和认证服务的 token 存储共用同一个 redis 实例。大 key 阻塞 redis 主线程后,认证服务的 token 查询也排队等待,导致超时 → oauthaccesstoken.find.fail

这就是共享基础设施的连带故障——一个服务的"优化"拖垮了另一个毫不相关的服务。

七、正确的缓存设计

7.1 大 key 拆分

如果要恢复缓存功能,将 600kb 大 key 按维度拆分:

// 改前:一个 600kb 大 key
string cachekey = "biz:onsale:detail";

// 改后:按code拆分,每个 key 几 kb
string cachekey = "biz:onsale:detail:" + code;

每个一个 key,value 几 kb,读取和写入都是毫秒级。

对比同版本的 smallitemcacheservice,它就做对了——按 code 分片,每个 key 只存一个code的商品列表,体积几 kb,风险极低。

7.2 本地缓存兜底

高频读 + 低频变更的数据应优先使用本地缓存:

请求 → l1 caffeine(本地, 亚毫秒) → miss → l2 redis(共享, ~1ms) → miss → 回源

本地缓存命中时不走 redis,彻底避免大 key 读取对 redis 主线程的影响。

7.3 value 压缩

大列表缓存不应存明文 json,应使用压缩:

方案压缩比读取性能适用场景
明文 json1x最快< 10kb
gzip + json~5-10x稍慢(需解压)10kb ~ 1mb
protobuf~3-5x快(二进制解析)> 100kb

7.4 redis 实例隔离

token 存储等核心链路不应与业务缓存共用 redis 实例。

改前(共用):
  redis 实例 a ← prod缓存 + 认证服务 token

改后(隔离):
  redis 实例 a ← 认证服务 token(核心链路,专用)
  redis 实例 b ← prod业务缓存(可降级)

7.5 缓存 key 设计自检清单

新增 redis 缓存前必须自检:

检查项通过标准
value 大小< 10kb ✅ / 10~100kb ⚠️ 需评估 / >100kb ❌ 必须拆分
key 粒度有分片维度(如 code/id)✅ / 固定单 key ❌
读取频率低频 ✅ / 高频需配合本地缓存 ⚠️
redis 实例独立实例 ✅ / 与核心链路共用 ❌
ttl短 ttl(分钟级)✅ / 长 ttl(天级)需评估
回源安全性单线程回源 / 并发回源需加锁

八、改进措施清单

层面改进项优先级
代码新增 redis 缓存必须评估 value 大小,超过 10kb 必须拆分p0
代码高频接口的缓存改动必须经过压测验证p0
代码新增缓存必须有 code review 检查 key 粒度和 value 体积p0
架构token 存储与业务缓存 redis 实例物理隔离p1
架构redis 大 key 监控告警(>100kb 报警)p1
架构feign 调用和 redis 操作使用独立线程池p2
流程涉及共享基础设施的变更需 code review 强制审查p0
流程部署分批灰度(先 1 pod 观察 10 分钟再全量)p1

九、经验教训

1. "加缓存"不等于优化

本事故的初衷是"优化性能"——将高频 feign 调用改为 redis 缓存。但缓存设计不当(大 key + 共享 redis)反而引入了更严重的问题。

缓存优化必须评估:value 大小、读取频率、redis 实例的共享情况。三者缺一不可。

2. redis 大 key 是隐性炸弹

redis 单线程架构决定了大 key 是"全民公敌"——一个 key 慢,所有 key 都要等。

600kb 的大 key 在高并发下相当于对 redis 做"慢查询 ddos"。

3. 连锁反应会误导排查方向

prod的大 key 拖垮 redis → 认证服务 token 查询超时 → 看起来像认证服务故障。排查初期花了大量时间在认证服务的 shiro 配置上,实际上认证服务是受害者。

共享基础设施故障时,应优先排查谁在"打"共享资源,而不是谁"挂了"。

4. 重启不是万能药

尝试重启认证服务后故障立即复现——说明根因不在应用本身,而在其依赖的共享资源(redis)。

重启无效时应立即转向外部依赖排查。

5. 批量新增缓存需统一评估

事故版本一次性新增了 5 个缓存服务,其中 3 个有不同程度的大 key 风险。如果逐个评估,可能只上线安全的 2 个(按 code 分片的),避免事故。

批量优化时每个缓存服务都应独立评估 key 设计,不能"一刀切"。

6. 删除缓存可能让问题更严重

在大 key 场景下,删除缓存会导致缓存击穿,不仅不能解决问题,还可能打挂下游。

正确做法是回滚代码 + 等待大 key 自然过期。

十、总结

一个"善意"的缓存批量优化
    → 一次性新增 5 个 redis 缓存服务
    → 其中一个写入 600kb 大 key(固定单 key + 全量商品)
    → 高频读取拖垮 redis 主线程
    → 共享 redis 的认证服务连锁故障
    → prod全线不可用 约半小时
    → 回滚代码恢复
项目内容
根因app 新版本新增 redis 缓存,其中 onsale:detail 为固定单 key,value 约 600kb
触发用户进入首页时高频读取大 key,阻塞共享 redis
影响app 全量用户不可用约 40 分钟
修复回滚缓存逻辑,恢复直接 feign 调用,等待大 key 自然过期
核心教训缓存设计必须评估 value 大小和 key 粒度;核心链路与业务缓存 redis 实例隔离;批量新增缓存需逐个评估;共享基础设施故障时优先排查"谁在打共享资源"

以上就是redis大key导致生产事故排查与解决方法的详细内容,更多关于redis大key导致生产事故的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com