上一篇通过 TTL 抖动与请求合并压住集中回源,但缓存内部仍可能严重倾斜。本篇把两个常被混称的问题拆开:热 key 是访问频率异常,大 key 是单个 value 或集合规模异常。前者消耗执行与网络吞吐,后者放大传输、复制、持久化和释放成本;只有分别测量,治理动作才不会南辕北辙。
一、痛点:平均值会隐藏最危险的 key
热 key 指访问频率显著高于其他 key,不一定占很多内存;大 key 指 value 或集合元素过大,不一定经常访问。一个 20 字节的秒杀开关可能每秒读取十万次,是热 key;一个含百万成员但每天读取一次的 Set 是大 key。若只看实例平均 CPU、平均延迟和总内存,就会错过局部倾斜。
热 key 会集中在 Redis 单线程命令执行路径,并在 Cluster 中集中到一个分片。副本分担读能缓解只读流量,但带来复制延迟和一致性取舍。大 key 的GET虽是 O(1),返回几十 MB 仍会阻塞网络与客户端解析;HGETALL、SMEMBERS、LRANGE 0 -1的成本与元素数相关。同步DEL巨型对象还可能在释放内存时卡住事件循环。
二、发现:从实例信号下钻到对象
先看INFO commandstats、INFO stats、延迟和网络吞吐,判断是命令计算、输出缓冲还是内存压力。SLOWLOG只记录服务器执行阶段,不包含网络传输,因此客户端很慢而慢日志为空并不矛盾。redis-cli --hotkeys依赖 LFU 相关计数,适合辅助采样,不应当作精确全量榜单;应用侧按规范化 key 前缀统计请求频率通常更可靠。
大 key 可用redis-cli --bigkeys做采样扫描,它按类型报告“最大元素数”等信息,并非所有类型都直接按字节比较。对候选 key 再用MEMORY USAGE key SAMPLES n、STRLEN、HLEN、LLEN、SCARD、ZCARD核实。扫描也会消耗资源,应在副本或低峰进行,设置节奏,避免把诊断变成事故。
下面程序按访问计数的中位数检测热点,并按估算字节识别大对象。生产阈值应按实例容量和 SLO 校准;示例强调两张榜单不能混为一谈。
fromstatisticsimportmedian samples={"config:flash-sale":{"qps":12000,"bytes":32},"user:42":{"qps":80,"bytes":2048},"catalog:all":{"qps":20,"bytes":8_500_000},"user:43":{"qps":70,"bytes":1900},"product:9":{"qps":90,"bytes":1200},}baseline=median(item["qps"]foriteminsamples.values())hot_threshold=baseline*20large_threshold=1_000_000hot=sorted((keyforkey,valueinsamples.items()ifvalue["qps"]>=hot_threshold),key=lambdakey:samples[key]["qps"],reverse=True,)large=sorted((keyforkey,valueinsamples.items()ifvalue["bytes"]>=large_threshold),key=lambdakey:samples[key]["bytes"],reverse=True,)print(f"baseline_qps={baseline}")print(f"hot_keys={hot}")print(f"large_keys={large}")print(f"overlap={sorted(set(hot)&set(large))}")运行输出:
baseline_qps=80 hot_keys=['config:flash-sale'] large_keys=['catalog:all'] overlap=[]三、治理:拆分、复制、限界和渐进删除
热读数据可放应用进程本地缓存,使用很短 TTL 或版本通知收敛;这样请求不再全部穿过网络。允许轻微陈旧时,可将同一值复制到多个带后缀的 key,由客户端随机读取以分散 Cluster 槽位,但更新必须写全副本并容忍短暂不一致。热写计数器可按时间或随机桶分片,读取时汇总;这把写压力换成读放大,只适合可合并数据。
大 String 应按业务边界拆分,不要机械切字节导致每次读取仍需全量拼接。大 Hash/Set/Sorted Set 可按用户、月份或哈希桶拆 key,并提供分页 API。集合遍历使用HSCAN、SSCAN、ZSCAN,接受游标期间元素变化和可能重复;SCAN 不是快照。列表或日志必须设置上限、归档周期和生产者背压。
下面脚本创建一个受控测试 Hash,用游标分批扫描,然后以UNLINK异步释放。它不在共享命名空间运行,也不会扫描整个数据库。
#!/usr/bin/env bashset-euopipefailredis_url="${REDIS_URL:-redis://127.0.0.1:6379/0}"key='demo:bigkey:users'redis-cli-u"$redis_url"DEL"$key">/dev/nullforstartin0100200300;doargs=()for((i=start;i<start+100;i++));doargs+=("user:$i""score:$((i%17))")doneredis-cli-u"$redis_url"HSET"$key""${args[@]}">/dev/nulldonecount="$(redis-cli-u"$redis_url"--rawHLEN"$key")"cursor=0seen=0while:;domapfile-treply<<(redis-cli-u"$redis_url"--rawHSCAN"$key""$cursor"COUNT80)cursor="${reply[0]}"seen=$((seen+(${#reply[@]}-1)/2))[["$cursor"==0]]&&breakdoneprintf'hash_fields=%s\n'"$count"printf'scanned_fields=%s\n'"$seen"printf'unlinked=%s\n'"$(redis-cli-u"$redis_url"--rawUNLINK"$key")"四、迁移:治理动作本身也要限流
拆 key 不能一次切换。先让写路径双写旧、新结构;后台按游标回填历史数据;读取路径优先新结构,缺失时回退旧结构并补写;核对数量、校验和与业务抽样后停止旧写;最后等待回滚窗口再UNLINK。双写不是事务,必须幂等并记录失败。Cluster 跨槽双写还不能依赖普通事务保证原子。
随机过期旧 key,或一次性删除大量 key,都可能制造内存与回源尖峰。设置每秒迁移条数和字节预算,观察used_memory、事件循环延迟、复制积压与副本 lag。若副本追不上,继续迁移只会扩大故障域。生产者写入无界集合时,治理重点不是定期清理,而是把容量上限写进数据模型。
热 key 的本地缓存也有代价:进程越多,总内存越大,失效广播越复杂。极高价值且很小的配置适合这种方式;用户权限等敏感数据必须用短 TTL、版本校验,不能让撤权长期不生效。复制热点 key 时避免使用哈希标签把副本又固定到同一 Cluster 槽。
五、验证:建立按前缀的容量与热度预算
每类 key 应有 owner、预计数量、单项大小、最大基数、TTL、读写 QPS 和删除方式。监控按业务前缀聚合,而不是把完整用户 ID 作为指标标签造成高基数。报警后保留候选 key、命令、客户端和时间窗口,才能复盘流量来源。
压测要同时模拟请求分布和 value 大小。均匀随机流量无法复现热点;只有小 value 的基准也看不到网络阻塞。观察 p99/p999、Redis CPU、网卡、客户端连接池等待和复制延迟。治理后若实例平均 QPS 不变但最热分片下降、尾延迟收敛,才说明目标达成。
当热点操作需要“只有一个客户端执行”时,人们常顺手写SETNX,却忽略租约、误删和故障恢复。下一篇将把这些风险收束成一把有明确安全边界的分布式锁。
治理验收不要只比较一次MEMORY USAGE。至少跨一个业务高峰记录最热节点 CPU、每秒出站字节、命令 p99、复制 lag 和候选 key 的访问分布;拆分后还要确认总 key 元数据没有反向吞掉节省的内存。若采用本地缓存,额外测量配置变更传播的最长时间,并在通知系统中断时验证 TTL 能自动收敛。对大集合分页接口设置最大页大小和游标有效期,防止调用者绕过保护重新发起全量导出。
参考来源
- Redis 官方文档:诊断延迟问题
- Redis 官方文档:MEMORY USAGE
- Redis 官方文档:UNLINK
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《Redis 应用实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。