Redis 大 Key 与热 Key 排查治理实战:从发现、拆分到安全删除
前言
Redis 用久了,几乎每个团队都会踩到“大 Key”和“热 Key”的坑:某个实例内存突然告警、某个分片 CPU 打满而其他分片很闲、一次 DEL 让业务卡顿好几百毫秒。这类问题的共同点是平时看不出来,出事时影响很大。本文把我在华为云分布式缓存服务 DCS(Redis)和自建 Redis 上排查、治理大 Key 与热 Key 的步骤整理成一套流程:先定义标准,再发现、再定位来源、最后拆分和安全删除。每一步都附上可以直接执行的命令或代码。
一、先统一“多大算大、多热算热”
没有标准就没法治理。我们团队采用的阈值如下,大家可以按业务规模调整:
- String 类型 value 超过 10 KB;
- Hash、List、Set、ZSet 元素个数超过 5000,或总大小超过 10 MB;
- 单个 Key 的访问 QPS 超过实例总 QPS 的 10%,或单 Key 每秒访问超过 5000 次,视为热 Key。
阈值写进团队规范后,代码评审和告警都能对齐同一个口径,后面的工具输出也能直接按这个标准过滤。
二、发现大 Key:控制台分析与命令行扫描结合
DCS 控制台的“缓存分析”里提供了大 Key 分析和热 Key 分析,可以手动触发,也可以配置定时任务。分析基于 SCAN 实现,对线上影响较小,建议在业务低峰期每天跑一次,把结果作为日常巡检的一部分。
自建实例或者需要更灵活的筛选时,可以用 redis-cli 自带的扫描:
# 按类型统计最大的 Key,-i 0.01 表示每 100 次 SCAN 休眠 10ms,降低对线上的压力
redis-cli -h 127.0.0.1 -p 6379 -a '******' --bigkeys -i 0.01
# Redis 6.0 以上可以按内存占用统计
redis-cli -h 127.0.0.1 -p 6379 -a '******' --memkeys -i 0.01
--bigkeys 只给出每种类型最大的一个 Key,信息不够时,我会用一段 Python 脚本按阈值把所有超标 Key 列出来:
import redis
r = redis.Redis(host="127.0.0.1", port=6379, password="******")
LIMIT_BYTES = 10 * 1024 * 1024
LIMIT_ITEMS = 5000
LEN_CMD = {"hash": r.hlen, "list": r.llen, "set": r.scard, "zset": r.zcard}
for key in r.scan_iter(count=500):
t = r.type(key).decode()
size = r.memory_usage(key, samples=0) or 0
items = LEN_CMD[t](key) if t in LEN_CMD else 1
if size > LIMIT_BYTES or items > LIMIT_ITEMS or (t == "string" and size > 10240):
print(t, key.decode(errors="ignore"), size, items)
注意 MEMORY USAGE 带 samples=0 会遍历全部元素,对超大集合本身就有开销,第一次跑可以先去掉这个参数,用默认采样值粗筛,再对可疑 Key 精确计算。
三、发现热 Key:看分布,再看具体 Key
热 Key 的典型表现是集群实例中某个分片的 CPU、带宽明显高于其他分片。先在监控里按分片对比“CPU 使用率”和“网络输入/输出流量”,锁定异常分片,再找具体 Key:
# 需要将 maxmemory-policy 设置为 LFU 类策略(allkeys-lfu 或 volatile-lfu)
redis-cli -h 127.0.0.1 -p 6379 -a '******' --hotkeys
# 临时抓取几秒命令流,按 Key 统计出现次数(只建议在测试或低峰期短时间使用)
timeout 5 redis-cli -a '******' monitor | awk '{print $5}' | sort | uniq -c | sort -rn | head -20
MONITOR 会显著增加实例负载,生产环境我更推荐在客户端侧统计:在 Redis 访问封装层里按 Key 计数,每分钟上报 Top N,既不影响服务端,也能直接定位到是哪个服务、哪段代码在访问。
四、治理大 Key:拆分是根本
找到大 Key 后,按类型选择拆分方式:
- 大 String:如果是序列化后的大对象,先看是否所有字段都需要,按读取场景拆成多个小 Key;如果只是存储大文本,考虑压缩或者放到对象存储 OBS,Redis 里只存地址。
- 大 Hash:按 field 做哈希分桶,例如
user:profile:{bucket},bucket = crc32(field) % 64,读写时先算桶号。 - 大 List / ZSet:按时间或分数区间分段,例如排行榜按日期拆成
rank:20261024,查询历史时再合并。
分桶示例:
import zlib
BUCKETS = 64
def bucket_key(base, field):
return f"{base}:{zlib.crc32(field.encode()) % BUCKETS}"
def hset_split(r, base, field, value):
r.hset(bucket_key(base, field), field, value)
def hget_split(r, base, field):
return r.hget(bucket_key(base, field), field)
五、安全删除:不要直接 DEL
拆分完成后,旧的大 Key 需要删除。直接 DEL 一个百万元素的集合会阻塞主线程,Redis 4.0 以上优先使用异步删除:
UNLINK big:hash:key
如果版本较老,就用 SCAN 类命令分批删除,例如 Hash 每次删 500 个 field:
cursor = 0
while True:
cursor, data = r.hscan("big:hash:key", cursor, count=500)
if data:
r.hdel("big:hash:key", *data.keys())
if cursor == 0:
break
r.delete("big:hash:key")
另外建议开启 lazyfree-lazy-expire 和 lazyfree-lazy-eviction,避免大 Key 过期或被淘汰时同步释放内存造成卡顿。
六、治理热 Key:分散和前置
热 Key 的思路是把集中访问分散开,或者干脆不让请求打到 Redis:一是在应用内加本地缓存(如 Caffeine),设置 1 到 5 秒的短过期,扛住瞬时峰值;二是把热 Key 复制成多份,例如 hot:item:1001:0 到 hot:item:1001:7,读时随机选一份,写时全部更新;三是对读多写少的场景,使用读写分离实例,把读流量分到只读副本。
七、把检查放进日常流程
工具和方案准备好之后,真正决定效果的是能否持续执行。我们的做法有三条:第一,在代码评审清单里加入“新增 Redis Key 是否评估过元素上限和过期时间”,从源头避免集合无限增长;第二,把控制台缓存分析的结果每天推送到团队群,超过阈值的 Key 自动创建跟踪任务,指定负责人和处理期限;第三,大促或活动上线前,用压测流量预演一次热 Key 场景,提前确认本地缓存和多副本方案是否生效。坚持一段时间后,排查工作会从“出事再查”变成“例行巡检”,值班压力明显下降。
总结
大 Key 和热 Key 的治理,难点不在命令本身,而在于形成闭环:定好阈值、定期扫描、找到来源、拆分改造、安全删除,最后把检查项加进代码评审。按这个流程做下来,我们的实例再没有出现过因单个 Key 导致的分片 CPU 告警,内存使用也更加平稳。希望这套步骤对正在使用 DCS 或自建 Redis 的同学有所帮助。
- 点赞
- 收藏
- 关注作者
评论(0)