案例 Redis 大 key 带 TTL:到期瞬间删除卡住主线程,slowlog 却查不到

2026-09-02 21:03:07

线上有一个很典型的 Redis 故障:实例没挂,主从都正常,但所有接口超时。慢查询日志(slowlog)里空无一物,因为整个阻塞发生在 Redis 主线程内部,不经过命令处理层。

定位后确认根因是大 key 过期。一个集合型 key(hash/list/set/zset)存了几十 MB 甚至上亿成员,平时只读不写,谁也没料到它会出事。但它设了 TTL,到期那一瞬间,Redis 主线程必须同步遍历整个集合去释放内存,这是一个 O(N) 操作。几百毫秒到秒级的时间窗口里,主线程卡在内存回收上,所有读写命令排队等待。主从同步也随之延迟——从库同样要执行这一次耗时释放。

这里有三层因素叠加:

  1. 过期删除在主线程执行
  2. 集合型大 key 的释放是 O(N),节点越多越慢
  3. 主库和从库各自做一遍同样的操作,故障面扩大

排查方向比较直接。先用 redis-cli --bigkeys 扫一轮,找出容量异常的 key。如果有具体怀疑对象,用 MEMORY USAGE key 确认内存占用,再根据 key 类型用 HLEN(hash)、SCARD(set)、ZCARD(zset)确认成员规模。绝对不要在生产环境对疑似大 key 执行 HGETALLKEYS *,这会把问题放大成事故。

确认故障后,处理方式取决于业务是否还需要这个 key。

如果 key 已经没用了,直接删。Redis 4.0 以上版本用 UNLINK key,主线程只做摘链操作立即返回,真正的内存释放丢给后台 bio 线程异步处理。

如果 key 还要保留,就只能在原地做渐进式瘦身。对大 hash 用 HSCAN,大 set 用 SSCAN,大 zset 用 ZSCAN,每次迭代取一批成员删除,配合 pipeline 减少 RTT。SCAN 是增量遍历,不保证一次返回 COUNT 个结果,也不是强一致快照,过程中如果有其他客户端写入,可能会扫到重复成员——删除脚本里要容忍这一点,用 SREM/HDEL 按 key 删不存在不会报错,这是安全的。每批 COUNT 控制在几百,不要照抄别人的数值,结合节点延迟和集合规模自己调,确保单次操作耗时在延迟预算内。

如果问题出在过期场景,Redis 4.0 起可以开启懒过期:lazyfree-lazy-expire yes,让过期 key 的释放也走后台线程。这个配置对主动删除(DEL 命令)无效,只针对过期淘汰路径,需要确认布的是哪个版本。

边界条件要讲清楚。UNLINK 之后 key 立即从主字典摘除,业务上表现为 key 不存在。如果业务还在读这个 key,就不能删,只能做拆分,比如把 cart:user:123 拆成 cart:user:123:0/1/2 多个分片 key,每个分片固定容量上限。

「大 key」不是一个绝对数字。同样的 key,低峰期删掉只让请求慢几十毫秒可能无感,延迟敏感链路里就是事故。阈值的判断标准是延迟预算,不是某个固定的 size 数字。排查手段和删除命令就这些,真正要在架构层面落地的是容量治理——别让这种 key 长出来。

再补充一个容易踩的细节:SCAN 在遍历过程中如果集合持续变小,它最终一定能遍历完,但返回的成员数可能比 COUNT 小很多,也可能在尾部才一次性返回大量结果。用 SCAN 写删除脚本时,需要通过游标是否为 0 判断循环是否结束,而不是依赖每次返回的数据量。

复制全文 生成海报 Redis 运维 故障排查 性能

推荐文章

程序员茄子在线接单