适用场景

业务使用 Redis 承载登录态、商品详情、计数器或排行榜。平时接口稳定,但在促销、定时任务或某些请求集中到来时,应用开始出现 Redis 超时;监控中 instantaneous_ops_per_sec 并不一定很高,CPU 也可能没有持续满载。

本文以单实例或主从架构为例,说明如何区分 Hot Key(极少数 Key 被高频访问)和 Big Key(单个 Key 的元素或字节数过大),并在不直接阻塞线上实例的前提下完成定位和治理。

现象与常见误判

典型表现包括:

  • 应用日志偶发 redis.exceptions.TimeoutError,持续数秒后自行恢复;
  • Redis 的慢日志出现 HGETALLSMEMBERSLRANGE 0 -1、大范围 ZRANGE
  • P99 延迟抖动明显,平均延迟却看起来正常;
  • connected_clients 和 QPS 未异常,单线程 CPU 却短时间打满;
  • 某个业务接口的流量增长与 Redis 延迟尖峰高度重合。

不要仅凭 used_memory 判断问题。Big Key 的风险不仅是占内存,还在于命令遍历、网络返回、过期删除和主从复制都会在 Redis 单线程上形成长尾。Hot Key 则可能很小,却能把请求集中到一个分片或一个主节点。

先确认是不是 Redis 侧排队

先在应用侧记录命令名、Key 的脱敏前缀、耗时和连接池等待时间,避免把 DNS、网络或连接池耗尽误归为 Redis 性能问题。随后在 Redis 节点执行:

redis-cli --latency-history -h 127.0.0.1 -p 6379
redis-cli INFO stats | egrep 'instantaneous_ops_per_sec|keyspace_hits|keyspace_misses'
redis-cli INFO clients | egrep 'connected_clients|blocked_clients'
redis-cli SLOWLOG GET 20

--latency-history 用于观察秒级尖峰;blocked_clients 非零常提示 BLPOP、Lua 或模块命令造成等待。慢日志的时间单位是微秒,但它只记录执行时间,不包含命令在 TCP 输入队列或 Redis 事件循环中的排队时间,因此“慢日志很少”不能排除 Hot Key。

若 Redis 7 已开启 latency monitor,可进一步查看事件类型:

redis-cli CONFIG GET latency-monitor-threshold
redis-cli LATENCY LATEST
redis-cli LATENCY DOCTOR

生产环境不要为了临时排查把阈值设得过低。通常从 100 毫秒开始观察更稳妥,排查完成后应恢复原有配置或纳入配置管理。

安全定位 Big Key

redis-cli --bigkeys 会扫描整个库,在极大数据集上耗时较长;更重要的是,它只给出每种数据类型的最大 Key,不代表所有风险 Key。优先在从库或低峰窗口运行:

redis-cli --scan --pattern 'cache:*' | while read key; do
  redis-cli MEMORY USAGE "$key"
done | sort -nr | head -n 20

上例以业务前缀缩小范围。MEMORY USAGE 返回近似字节数,适合筛选候选项;它不是逻辑元素数量。找到候选 Key 后,不要使用 GETHGETALLSMEMBERS 将全部内容拉回终端,应先确认类型和规模:

redis-cli TYPE 'cache:product:detail:8842'
redis-cli MEMORY USAGE 'cache:product:detail:8842'
redis-cli HLEN 'cache:product:detail:8842'
redis-cli TTL 'cache:product:detail:8842'

对于 List、Set、ZSet 分别使用 LLENSCARDZCARD。若一个 Hash 有数十万 field,HGETALL 即使“能成功”也会放大序列化、网络传输和客户端 GC,应该改用 HSCAN 分批读取。

定位 Hot Key:从采样到业务关联

Redis 本身不会在 INFO 中直接列出访问最频繁的 Key。推荐用两个低风险信号交叉判断:

  1. 应用指标按 Key 前缀或路由聚合,找出延迟尖峰期间请求数突增的接口;
  2. 在从库短时启用 MONITOR,并在采样窗口内做聚合。MONITOR 输出量很大,禁止在繁忙主库长时间运行。

例如,在隔离的只读副本上仅采样 30 秒:

timeout 30 redis-cli -h redis-replica MONITOR |
  grep -E '"(GET|HGET|ZRANGE)"' |
  sort | uniq -c | sort -nr | head -n 20

命令形式会因客户端和 Redis 版本而异,因此结果应结合应用访问日志确认。若不能使用副本,优先在客户端增加命令埋点,而不是在主库上执行 MONITOR

一个现场示例

某商品详情接口将完整商品对象以 JSON 存入 cache:product:detail:<id>,同时首页每次刷新都读取 cache:product:rank(一个 8 万成员的 ZSet)。活动开始后,首页流量增加,代码调用了:

# 不推荐:每次请求返回整个排行榜
rows = redis_client.zrevrange("cache:product:rank", 0, -1, withscores=True)

单次返回数据量大,Redis 需要遍历并编码全部成员;大量并发请求又让这个 Key 成为 Hot Key。修复后的读取范围明确,并让页面层再缓存短时间结果:

TOP_N = 100
rows = redis_client.zrevrange(
    "cache:product:rank", 0, TOP_N - 1, withscores=True
)

同时把“全量榜单导出”迁移到异步任务,使用 ZSCAN 分批处理,避免在线请求与离线任务争抢事件循环。

分阶段修复方案

1. 先止血

  • KEYS *HGETALLSMEMBERS、全范围 LRANGE/ZRANGE 从请求链路移除;
  • 给大对象读取增加分页或字段白名单,并限制单次返回大小;
  • 为热点页面增加本地缓存或 CDN,设置随机抖动 TTL,防止同一秒集体失效;
  • 对缓存未命中采用请求合并(singleflight)或互斥重建,避免缓存击穿。

2. 改造数据模型

  • 大 JSON 拆为摘要与详情两个 Key,列表页只读摘要;
  • 大 Hash 按业务维度或时间窗口拆分,例如 order:2026-08:<bucket>
  • 超大集合改为分页索引,保留热点窗口;
  • 热点读多写少的数据使用只读副本分担,但要明确允许的复制延迟边界。

删除 Big Key 也需要避免阻塞。Redis 4 及以上优先使用异步删除:

redis-cli UNLINK 'cache:obsolete:large-object'

UNLINK 会从主字典移除 Key,并在后台线程回收内存;它仍会产生复制和 AOF 传播,因此应先确认 Key 的业务归属和回滚方案,不能把它当成“无影响删除”。

3. 控制访问倾斜

对于极热的计数器,可在允许最终一致性的前提下做分片计数:

import zlib

def shard_key(counter: str, user_id: str, shards: int = 32) -> str:
    shard = zlib.crc32(user_id.encode()) % shards
    return f"{counter}:{shard}"

redis_client.incr(shard_key("counter:article:read", user_id))

读取时聚合分片,或通过定时任务汇总到展示值。切勿对需要强一致原子语义的库存直接套用该方案。

预防与监控

  • 监控 Redis 命令延迟分位数、慢日志长度、网络输出缓冲区、CPU、主从复制延迟和每秒过期数;
  • 对单 Key 的逻辑大小设开发期约束,例如 Hash field 数、JSON 序列化后字节数、排行榜最大成员数;
  • 在代码评审中禁止无界范围命令进入在线链路;
  • 每周在副本运行受限 SCAN 审计,产出 Top N 内存 Key 和异常 TTL;
  • 缓存 Key 设计加入版本、业务前缀和 TTL,便于灰度迁移与安全回收。

总结

Redis 延迟尖峰往往不是“QPS 太高”这么简单,而是单线程被某个昂贵命令或集中访问的 Key 拉长了处理时间。先用延迟、慢日志和应用埋点确认排队,再在从库或低峰以 SCANMEMORY USAGE、短时采样定位候选 Key;最后通过有界读取、模型拆分、异步删除和访问分散完成治理,才能把偶发超时真正转化为可持续的容量与代码规范。