适用场景
业务使用 Redis 承载登录态、商品详情、计数器或排行榜。平时接口稳定,但在促销、定时任务或某些请求集中到来时,应用开始出现 Redis 超时;监控中 instantaneous_ops_per_sec 并不一定很高,CPU 也可能没有持续满载。
本文以单实例或主从架构为例,说明如何区分 Hot Key(极少数 Key 被高频访问)和 Big Key(单个 Key 的元素或字节数过大),并在不直接阻塞线上实例的前提下完成定位和治理。
现象与常见误判
典型表现包括:
- 应用日志偶发
redis.exceptions.TimeoutError,持续数秒后自行恢复; - Redis 的慢日志出现
HGETALL、SMEMBERS、LRANGE 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 后,不要使用 GET、HGETALL、SMEMBERS 将全部内容拉回终端,应先确认类型和规模:
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 分别使用 LLEN、SCARD、ZCARD。若一个 Hash 有数十万 field,HGETALL 即使“能成功”也会放大序列化、网络传输和客户端 GC,应该改用 HSCAN 分批读取。
定位 Hot Key:从采样到业务关联
Redis 本身不会在 INFO 中直接列出访问最频繁的 Key。推荐用两个低风险信号交叉判断:
- 应用指标按 Key 前缀或路由聚合,找出延迟尖峰期间请求数突增的接口;
- 在从库短时启用
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 *、HGETALL、SMEMBERS、全范围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 拉长了处理时间。先用延迟、慢日志和应用埋点确认排队,再在从库或低峰以 SCAN、MEMORY USAGE、短时采样定位候选 Key;最后通过有界读取、模型拆分、异步删除和访问分散完成治理,才能把偶发超时真正转化为可持续的容量与代码规范。
Discussion
评论