资讯动态

Redis大key治理全攻略:识别、定位、治理与预防

发布时间:2026/10/29 23:36:41 来源:尧图企业网站定制
缓存里冒出个大 key这事在监控面板上看着不起眼真炸起来能让人半夜从床上弹起来。我见过最典型的一次某个业务线的 Redis 实例内存曲线本来平稳某天开始缓慢爬坡运维同学以为是正常增长没在意结果三天后主从同步延迟飙到几十秒读请求大面积超时最后定位下来就是一个 Hash 类型的 key 悄悄涨到了两百多万个 field。这类问题不分团队大小只要你在用 Redis 做缓存早晚会撞上。所谓大 key通常指单个 value 体积过大比如 String 超过 10KB、集合类元素数量过万或者单 key 的访问频率畸高造成热点。它带来的麻烦是连锁的内存分布不均、网络带宽被单个请求吃满、主从复制卡顿、集群模式下数据倾斜、删除时阻塞主线程。这篇内容我打算把大 key 从识别、定位、治理到预防的完整链路讲透适合正在维护缓存中间件的后端同学、做容量规划的运维同学以及被大 key 坑过一次想彻底搞明白的开发者。下面这些手段都是我在实际项目里反复验证过的能直接抄作业。1. 先搞清楚大 key 到底大在哪很多人一上来就问多大算大这个问题其实没有标准答案因为判断维度不止一个。我一般从三个角度去界定体积维度、元素数量维度、访问热度维度。体积维度看的是单个 value 序列化后的字节数String 类型超过 10KB 就该警惕超过 100KB 基本可以判定为大 key元素数量维度针对 List、Hash、Set、ZSet 这类集合元素个数过万就要纳入观察名单过十万基本跑不掉访问热度维度则是 QPS 层面的概念单个 key 每秒被访问几万次哪怕它体积不大也会因为流量集中变成事实上的热点大 key。1.1 为什么不能只用一个阈值判断只按体积判断会漏掉两类情况。第一类是小而热的 key比如某个配置项 key 只有几百字节但全站所有请求都要读它这种 key 在集群里会把单个分片的 CPU 打满表现和大 key 一模一样。第二类是元素多但总体积不大的集合比如一个 Set 存了五十万个用户 ID每个 ID 才十几个字节总体积不到 10MB看起来不算夸张但你要对它做一次 SMEMBERS 全量读取返回的响应包能把客户端内存撑爆。所以我在做容量评估时会把这三个维度做成一张检查表任何一个维度触线都要单独处理。判断维度警戒线危险线典型后果String 体积10KB100KB网络传输慢、序列化开销大集合元素数1 万10 万全量操作阻塞、响应包过大单 key QPS1 万5 万分片 CPU 打满、热点倾斜单 key 内存占用1MB10MB内存分布不均、淘汰策略失效这张表不是拍脑袋定的是我根据多个线上实例的监控数据反推出来的经验值。警戒线意味着开始关注并纳入巡检危险线意味着必须尽快治理。你可以根据自己的实例规格和业务特点微调但量级上不会有太大偏差。1.2 大 key 的危害为什么是连锁反应单独看一个大 key好像只是占点内存、慢一点而已但它在真实系统里引发的是一连串问题。内存层面Redis 的内存分配是按实例整体算的一个大 key 占了几百 MB其他正常 key 的可用空间就被挤压触发淘汰策略时可能把有用的热数据挤出去造成缓存命中率下降。网络层面读取大 key 的响应包动辄几 MB在千兆网卡上单次传输就要几十毫秒并发一高直接把带宽打满其他请求排队等待。复制层面最要命主从同步是把写命令打包传输的一个大 key 的写入或删除命令体积巨大从库执行时主线程被阻塞复制延迟飙升严重的会导致全量同步反复触发。集群层面的问题更隐蔽。Redis Cluster 按 key 做槽位分配一个大 key 落在哪个分片那个分片的内存和 CPU 就明显高于其他节点形成数据倾斜。运维做扩容时迁移这个大 key 本身又是一次大流量操作迁移过程中还可能阻塞。删除层面是很多人忽略的坑直接 DEL 一个大集合Redis 要逐个释放元素内存主线程被卡住几百毫秒甚至几秒这段时间所有请求都超时。我见过一个团队在业务低峰期删大 key以为没事结果还是引发了短暂的服务抖动因为低峰期也有长连接在跑。2. 定位大 key从被动告警到主动扫描发现大 key 有两条路被动等告警或者主动去扫。我的建议是两条腿走路被动告警做兜底主动扫描做常态巡检。被动告警依赖监控体系比如内存使用率突增、慢查询日志里出现大响应、主从延迟告警这些信号出现时往往大 key 已经造成影响了。主动扫描则是定期跑任务把大 key 揪出来在它还没惹事之前处理掉。2.1 用 SCAN 命令做安全的全量扫描很多人第一反应是用 KEYS * 找 key这个命令在生产环境是禁忌它会一次性遍历所有 key 并阻塞主线程key 数量上百万时能卡住好几秒。正确做法是用 SCAN 命令做游标式增量遍历每次只返回一小批对主线程影响可控。基本用法是SCAN cursor MATCH pattern COUNT countcursor 从 0 开始返回结果里带着下一次的游标直到游标回到 0 表示遍历完成。# 分批扫描每次返回约 100 个 key SCAN 0 COUNT 100 # 假设返回游标 128继续 SCAN 128 COUNT 100这里有个细节要注意COUNT 只是提示 Redis 大概扫多少个槽位实际返回数量可能多也可能少不要假设每次返回固定条数。另外 SCAN 在遍历过程中如果数据有增删可能重复返回某些 key也可能漏掉这是它的设计特性做统计时要能容忍这种不精确。我一般写个脚本循环 SCAN对每个 key 用 TYPE 判断类型再用 STRLEN、HLEN、LLEN、SCARD、ZCARD 拿到大小或元素数超过阈值的记录下来。2.2 借助 MEMORY USAGE 精确算内存光看元素数量还不够有时候元素少但单个元素特别大比如一个 Hash 只有十个 field但每个 field 的 value 是几百 KB 的 JSON。这时候要用MEMORY USAGE key命令它返回这个 key 及其 value 占用的字节数采样估算默认采样 5 个元素。用法很简单MEMORY USAGE mykey # 返回类似 (integer) 1048576这个命令本身有一定开销全量扫描时不要对每个 key 都调我的做法是先用元素数量筛一遍对接近阈值的 key 再补一次 MEMORY USAGE 确认。还有个技巧是用MEMORY USAGE key SAMPLES 0做全量精确计算但 SAMPLES 0 会遍历所有元素开销大只适合对少量可疑 key 做精确核实。2.3 用 redis-cli 的 bigkeys 模式快速摸底如果不想自己写脚本redis-cli 自带一个--bigkeys参数能快速扫出各类型最大的 key。它的原理就是内部用 SCAN 遍历按类型统计最大的几个 key。redis-cli -h 127.0.0.1 -p 6379 --bigkeys输出会按 String、List、Hash、Set、ZSet 分类告诉你每类里最大的 key 和它的元素数量。这个工具适合快速摸底但它只统计最大的那一个不给你完整的分布而且对元素数量的统计是采样估算。我一般用它做初步排查真要精细治理还是得自己写扫描脚本。另外它扫描期间会给实例带来一定压力建议在低峰期跑或者对从库跑。2.4 从慢查询和监控指标反推有时候大 key 不是靠扫描发现的而是从现象反推。慢查询日志里如果出现command是 HGETALL、SMEMBERS、LRANGE 且耗时很长的记录基本就是操作了大集合。监控上如果看到某个时间点网络出流量突增、主从延迟同步上涨、某个分片内存明显高于其他分片都值得去查对应时间段的 key 访问情况。我习惯在慢查询配置里把阈值设成 10msslowlog-log-slower-than 10000这样大 key 操作很容易被抓到。提示扫描大 key 的任务不要在主库高峰期跑优先在从库执行或者用独立的只读实例。SCAN 虽然不阻塞但大量 MEMORY USAGE 和 TYPE 调用累积起来仍有开销。3. 治理手段不同场景用不同刀法定位到大 key 之后治理手段要分场景选。有的大 key 是设计问题得改数据结构有的是访问模式问题得加本地缓存或拆分有的是历史遗留得想办法安全删除。下面这几套刀法我按适用场景分开讲你对号入座。3.1 拆分把一个大 key 拆成多个小 key这是最根本的治理方式适用于集合类大 key。核心思路是给原来的 key 加一个分片维度把元素打散到 N 个子 key 里。比如一个存用户订单的 List原来 key 叫user:orders:123可以拆成user:orders:123:0到user:orders:123:9十个分片写入时按订单 ID 取模决定落到哪个分片读取时按需读对应分片或者并发读所有分片再合并。拆分的关键是分片键的选择和分片数量的确定。分片键要保证分布均匀用元素 ID 的哈希取模是最常见的做法。分片数量不是越多越好太多会导致读取时要并发请求多个 key增加客户端复杂度太少又起不到打散效果。我的经验是按预估元素总量除以单分片目标元素数来算比如预估一百万元素、单分片控制在五万那就分 20 片。分片数量定下来后尽量别改因为改分片数意味着所有数据要重新分布迁移成本很高。拆分的代价是读取逻辑变复杂。原来一次 HGETALL 搞定的事现在可能要读多个 key 再合并如果业务需要全量数据客户端要写并发读取和结果聚合的逻辑。所以拆分适合那些按部分读取的场景比如按时间范围查订单、按用户分组查数据。如果业务确实需要全量读取拆分反而可能让问题更复杂得考虑别的方案。3.2 压缩减小 value 体积有些大 key 是 value 本身太大比如存了一个巨大的 JSON 字符串。这种情况下可以考虑压缩。常见做法是用 Gzip、Snappy、LZ4 这类算法在写入前压缩、读取后解压。JSON 文本的压缩比通常很可观重复字段多的 JSON 能压到原来的三分之一甚至更少。选压缩算法要看权衡Gzip 压缩率高但 CPU 开销大适合写入不频繁、读取也不频繁的冷数据LZ4 和 Snappy 压缩率稍低但速度快适合读写频繁的热数据。我在实际项目里对配置类、报表类这种读多写少且体积大的数据用 Gzip对高频访问的会话数据用 LZ4。压缩的代价是每次读写都要多一次 CPU 运算如果这个 key 的 QPS 很高压缩开销可能抵消掉体积减小带来的收益得实测评估。还有一种压缩是精简数据结构。比如用 Hash 存对象时field 名如果很长几十万个 field 光 field 名就占不少内存。可以把 field 名改成短编码或者改用更紧凑的序列化格式比如用二进制协议替代 JSON 文本。这类优化不改变业务逻辑纯粹是存储层面的瘦身性价比往往很高。3.3 本地缓存挡住热点 key 的流量对于小而热的热点大 key拆分和压缩都不太对症因为问题不在体积而在访问集中。这时候本地缓存是最有效的手段。思路是在应用进程内用 Caffeine、Guava Cache 这类工具缓存一份热点数据请求先查本地未命中再查 Redis。本地缓存命中时完全不碰网络能把 Redis 的 QPS 压力降下来。本地缓存的关键是过期策略和一致性。过期时间不能太长否则数据更新后本地迟迟不刷新业务读到脏数据也不能太短否则命中率上不去。我一般设 1 到 5 秒的短过期配合 Redis 的发布订阅做主动失效——数据变更时发一条消息各应用实例收到后清掉本地对应缓存。这样既保证了较短时间内的最终一致又挡住了绝大部分读流量。本地缓存的坑在于内存占用和实例间不一致。每个应用实例都存一份实例多了总内存占用不小要控制缓存条目数和单条大小。实例间不一致在短过期加主动失效的组合下基本可控但对强一致要求高的场景不适用得评估业务容忍度。3.4 安全删除别让 DEL 卡住主线程历史遗留的大 key 总得删掉但直接 DEL 是危险的。Redis 4.0 之后提供了UNLINK命令它是异步删除先把 key 从键空间摘除实际的内存释放放到后台线程做主线程不会被阻塞。所以删除大 key 首选 UNLINK。UNLINK bigkey如果版本较低没有 UNLINK或者要删的是超大集合可以用分批删除的思路用 HSCAN、SSCAN、ZSCAN 配合 HDEL、SREM、ZREM 每次删一批删完一批 sleep 一小会儿再删下一批把删除压力摊平。比如每次删 100 个元素间隔 10ms一个百万元素的集合删完大概需要一百秒但主线程始终流畅。# 分批删除 Hash 的示例思路伪代码 while true: fields HSCAN bigkey 0 COUNT 100 if fields is empty: break HDEL bigkey fields... sleep 10ms注意删除大 key 前先确认没有业务在依赖它最好在低峰期操作并且观察主从延迟和慢查询指标。删除过程中如果发现延迟上涨立刻暂停调整批次大小和间隔。4. 预防机制把大 key 挡在写入之前治理是事后补救真正省心的是事前预防。大 key 的产生往往是因为写入时没人管日积月累涨起来的。建立一套预防机制能在 key 变大的早期就拦住它比事后治理成本低得多。4.1 写入侧加体积和数量校验最直接的办法是在业务代码的缓存写入层加一道校验。写 String 之前判断序列化后的字节数超过阈值就拒绝写入并打日志告警写集合之前判断当前元素数量超过上限就触发告警或者走降级逻辑。这道校验相当于给缓存加了个限高杆从源头上防止 key 无限膨胀。校验阈值怎么定参考前面那张表的警戒线String 设 10KB、集合设 1 万超过就告警。告警不是目的目的是让开发知道这个 key 在异常增长去查是业务逻辑有问题还是数据本身就该这么大。如果确实需要存大对象那就走对象存储或者拆分方案而不是硬塞进 Redis。校验的实现位置很关键。放在业务代码里侵入性强每个写入点都要改更好的做法是封装一个统一的缓存客户端所有写入都经过它校验逻辑集中在一处。我们项目里就是这么做的缓存客户端内部对写入的 key 做体积和数量检查超限时按配置决定是告警放行还是直接拒绝。4.2 设置合理的过期时间和淘汰策略很多大 key 是忘了设过期时间导致的。临时数据、会话数据、计算结果这类缓存都应该设 TTL让它在不再需要时自动消失。TTL 的设置要结合业务的数据生命周期比如会话数据设 30 分钟报表缓存设 1 小时别一律设成永久。淘汰策略也要配对。maxmemory-policy建议用allkeys-lru或volatile-lru前者在所有 key 里按 LRU 淘汰后者只在设了过期时间的 key 里淘汰。如果业务 key 都设了 TTL用 volatile-lru 更精准如果混着永久 key用 allkeys-lru 更安全。要注意淘汰策略是内存满了才触发它不能替代主动的大 key 治理只是最后一道防线。4.3 建立常态化的巡检和告警预防机制里最容易被忽略的是巡检。大 key 不是一天长成的定期巡检能在它还是中 key的时候就发现趋势。我的做法是每天低峰期跑一次全量扫描脚本把超过警戒线的 key 记录到一张表里对比前一天的记录看增长趋势。某个 key 连续几天元素数在涨就提前介入查原因而不是等它涨到危险线。告警维度我一般设三个单 key 内存超过 1MB 告警、集合元素数超过 1 万告警、单 key QPS 超过 1 万告警。这三个维度覆盖了体积、数量、热度三类大 key。告警发到值班群附上 key 名、类型、大小、所属业务方便快速定位责任人。巡检脚本本身要控制好扫描速率别把巡检变成新的压力源。预防手段实施位置拦截时机适用场景写入校验缓存客户端写入时所有新写入的 keyTTL 淘汰策略Redis 配置内存满时临时性、会话类数据定期巡检独立扫描任务每日低峰存量 key 的趋势监控慢查询监控Redis 配置实时大 key 操作的即时发现5. 几个容易踩的坑和实操心得讲完方法论说几个我在实际项目里踩过的坑这些细节文档里不会写但真遇到了很耽误事。5.1 集群模式下的大 key 迁移更麻烦单机 Redis 删大 key 用 UNLINK 就完事了集群模式下还要考虑槽位迁移。当你扩容或者做 rebalance 时大 key 所在的槽要迁到新节点迁移过程是把 key 序列化后传输一个大 key 的迁移可能耗时很久期间这个槽的请求会受影响。更麻烦的是迁移可能因为超时失败反复重试。所以集群环境治理大 key 要更早别等扩容时才发现迁不动。我的建议是集群里的大 key 阈值比单机更严格单机警戒线 10KB 的话集群可以收到 5KB。5.2 别在业务高峰期做全量扫描SCAN 虽然不阻塞但配合 TYPE、MEMORY USAGE 这些命令全量扫一遍百万级 key 的实例累积开销不小。我有次在下午高峰期跑扫描脚本结果慢查询数量明显上升虽然没到故障级别但被监控告警了。后来改成固定凌晨跑并且限制扫描速率每批之间加 sleep就平稳多了。扫描任务要当成一个有压力的后台任务来对待给它限速、限时段。5.3 大 key 的判定要结合业务语义纯技术指标判断大 key 有时会误伤。比如一个存商品详情的大 JSON体积 50KB按阈值算超标了但业务上它就是需要这么大而且访问频率不高那它其实不构成实际危害。反过来一个只有 2KB 的 key如果每秒被读十万次它才是真正的问题。所以治理前要结合业务语义判断这个 key 为什么这么大、访问模式是什么、拆了会不会影响业务。别为了指标好看去拆一个本来没问题的 key徒增复杂度。5.4 删除前先确认依赖关系删大 key 之前一定要确认没有业务在依赖它。我见过一个案例某同学扫描到一个大 Hash 觉得是垃圾数据直接 UNLINK 了结果那是另一个团队的配置缓存删完那边业务报错。确认依赖关系的方法查 key 的命名规范推断归属、看访问日志确认最近有没有读、跟相关团队确认。宁可多问一句别删错了。5.5 监控指标要能区分大和热很多监控面板只展示内存和 QPS看不出大 key 和热点 key 的区别。建议在监控里单独加两个维度按 key 前缀统计的平均 value 大小分布、按 key 前缀统计的 QPS 分布。这样一眼能看出是哪个业务线的 key 在体积上异常哪个在热度上异常。我们后来在缓存客户端埋了上报点把每次读写的 key 前缀、value 大小、耗时都上报到监控系统排查大 key 时直接看面板就行不用每次都跑扫描脚本。这套东西落地下来大 key 从半夜炸雷变成了日常可管。核心就三件事写入时拦住、巡检时发现、治理时选对刀法。真正难的不是技术手段是让团队养成习惯——新加缓存写入点时想着加校验看到告警时愿意花十分钟查一下趋势。我个人的体会是大 key 治理最有效的投入不在治理本身而在预防那一道校验和每天那一次巡检这两件事做到位后面九成的麻烦都不会发生。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑