很多 Redis 问题,表面上看是“偶发卡顿”“接口突然慢”“某个实例内存涨得很快”,最后往下查,常常会落到一个很朴素的原因:某些 key 太大。
这里的“大”不是一个绝对标准。字符串 value 很大,当然是 BigKey;一个 hash、set、zset、list 里元素数量特别多,也一样是 BigKey。它危险的地方不只是占内存,而是它会把 Redis 单线程模型、网络传输、删除释放、持久化和复制这些链路一起拖慢。
这篇文章不想把 BigKey 讲成一句“不要用大 key”。真正有用的是:知道它会在哪里出问题,线上应该怎么判断,已经存在时怎么处理。
一、BigKey 不是一种类型,而是一种风险形态
BigKey 常见有两类。
第一类是 value 本身很大。
例如一个 string key 里塞了几百 KB、几 MB 的 JSON。这个 key 可能只对应一个业务对象,但每次读取都要把整块数据从 Redis 拿出来,网络和客户端反序列化都会被放大。
第二类是集合元素很多。
例如一个 hash 里有几十万个 field,一个 set 里有上百万个 member,一个 zset 里存了大量排序数据。单个元素可能不大,但整个结构很大,而且很多命令会和元素数量相关。
所以 BigKey 的判断不能只看“key 名是什么”,要看两个维度:
- 单个 key 占用多少内存。
- 对这个 key 的常用命令复杂度是多少。
一个几 MB 的字符串会带来网络和内存问题;一个几十万元素的集合还可能带来慢命令和删除阻塞。
二、为什么 BigKey 会让 Redis 变慢
Redis 快,一个重要前提是主线程每次处理的命令都足够短。BigKey 会破坏这个前提。
1. 读取成本变高
如果业务每次都 GET 一个很大的 value,Redis 需要把整块数据从内存拷贝到网络缓冲区,再发给客户端。
这时慢的不一定是 Redis 查找 key,而是后面的数据搬运:
- Redis 输出缓冲区压力变大。
- 网络传输时间变长。
- 客户端解析和反序列化变慢。
- 并发一上来,多个大 value 会一起挤占带宽。
这也是为什么有些接口在本机压测看着还行,到了线上就抖动:线上有真实网络、真实并发、真实 payload。
2. 某些集合命令会放大主线程耗时
Redis 不是所有命令都是 O(1)。对大集合执行全量命令,非常容易把主线程拖住。
风险比较高的操作包括:
- 对超大 hash 执行 HGETALL。
- 对超大 set 执行 SMEMBERS。
- 对超大 zset 执行 ZRANGE 大范围查询。
- 对超大 list 执行 LRANGE 0 -1。
这些命令不是“不能用”,而是不能在集合很大的情况下无脑全量取。一次命令处理时间过长,后面的请求就只能排队。
Redis 的单线程不是缺点,前提是每个命令都短平快。一旦业务把一个大集合当成本地 Map 一样全量拉取,问题就开始了。
3. 删除 BigKey 也可能卡
很多人只关注读写,忽略删除。
删除一个很大的集合时,Redis 需要释放大量内存对象。如果使用 DEL,同步释放可能会让主线程停顿。这个停顿在低峰期可能看不出来,在高峰期就会变成接口毛刺。
更稳妥的做法是优先使用 UNLINK。它会把内存释放交给后台线程异步处理,主线程只做摘除 key 的动作。
但要注意,UNLINK 也不是让成本消失,只是把释放工作从主线程挪走。后台释放太多,实例仍然会有资源压力。所以真正的治理还是要减少 BigKey 本身。
4. 持久化和复制也会被影响
BigKey 还会影响 Redis 周边链路。
RDB 快照时,大对象会增加 fork 后写时复制的压力;AOF 重写时,大对象相关命令会让重写成本变高;主从复制时,大 value 也会占用复制链路。
所以有些问题不是马上表现为慢命令,而是表现为:
- RDB 或 AOF 重写期间内存抖动。
- 从库复制延迟增加。
- 网络出口打满。
- 实例内存碎片和峰值变得不可控。
这也是 BigKey 难处理的地方:它不一定每天报错,但会让系统在流量波峰时更脆。
三、怎么判断一个 key 是不是 BigKey
判断 BigKey 不能只凭感觉。比较实用的方式有三层。
1. 先看实例层面的信号
如果实例有这些现象,就值得怀疑是否存在 BigKey 或热 key:
- used_memory 增长很快,但 key 数量增长不明显。
- 慢查询里出现 HGETALL、SMEMBERS、LRANGE、ZRANGE 大范围命令。
- 客户端超时集中发生在某几个接口。
- 网络输入输出带宽经常打高。
- 主从复制延迟偶尔拉大。
这些信号不能直接证明 BigKey,但能告诉你该往哪个方向查。
2. 用扫描方式找可疑 key
线上不要用 KEYS 去全量扫库。KEYS 会阻塞 Redis,数据量大时风险很高。
更合适的是 SCAN 思路:分批扫描,逐步检查。Redis 自带的 redis-cli 也有 bigkeys 模式,可以粗略找出每种数据类型里较大的 key。
不过 bigkeys 更偏“元素数量”视角,不一定能精确反映内存占用。比如一个 string 可能只有一个 key,但 value 很大;一个 hash 可能 field 不多,但每个 field 的 value 很长。
所以排查时最好组合使用:
- 先扫描出疑似大 key。
- 再看它的类型和长度。
- 必要时用 MEMORY USAGE 看内存占用。
- 最后结合业务访问方式判断风险。
3. 不要只看大小,也要看访问模式
一个 2 MB 的 key,如果一天只在离线任务里读一次,风险可能可控。
一个 500 KB 的 key,如果核心接口每秒读几百次,那就是高风险。
一个包含十万元素的 zset,如果每次只查小范围排名,可能还能接受;如果业务经常全量拉出来排序、过滤、分页,那就很危险。
所以 BigKey 治理不能只列一个固定阈值。更合理的是:
- string value 超过几百 KB 就要关注。
- 集合元素数达到几千、几万后就要看访问方式。
- 读写频率越高,阈值越要保守。
- 核心链路上的 key,要比非核心链路更严格。
四、已经有 BigKey 了,怎么处理
1. 大字符串:拆字段、拆对象、减少整包读
如果一个 string 里塞了完整 JSON,先问一个问题:业务真的每次都需要整份数据吗?
很多时候并不需要。比如用户画像、商品详情、配置对象,接口可能只用其中几个字段,但缓存里放的是整块 JSON。
可选处理方式:
- 把稳定字段和高频字段拆开。
- 用 hash 存结构化字段,按需 HGET。
- 把大对象拆成多个小 key。
- 对冷门大字段延迟加载。
- 减少把数据库整行或整份响应原样塞进 Redis。
拆分后要注意一致性。不要为了拆 BigKey,把一次更新拆成很多个没有约束的小操作,最后让业务读到半新半旧的数据。必要时可以用版本号、短 TTL、双写顺序或 Lua 来控制。
2. 大集合:分桶、分页、限制单次返回量
大集合的核心思路是:不要让一个 key 承担无限增长的数据。
常见拆法包括:
- 按用户、业务维度、时间分桶。
- 按 hash slot 或取模拆成多个子 key。
- 只缓存最近一段时间或 Top N 数据。
- 全量数据放数据库或搜索系统,Redis 只做索引或热点缓存。
例如“某个用户的全部行为记录”不适合一直塞进一个 list;“某个活动的全部参与用户”也不适合无限塞进一个 set。更稳的是按日期、分页游标、分片 key 去组织。
同时,接口层面要避免全量命令:
- 用 HSCAN 替代 HGETALL 的全量遍历。
- 用 SSCAN 替代 SMEMBERS 的一次性全取。
- 用 ZRANGE 小范围分页,不要一次取完整榜单。
- 对 list 使用固定窗口,不要 LRANGE 0 -1。
这里的重点不是“SCAN 一定更快”,而是它可以把一次大阻塞拆成多次小操作,让系统更可控。
3. 删除:优先异步删除,避开流量高峰
如果已经确认某个 key 很大,删除时不要随手 DEL。
更稳的流程是:
- 先确认 key 类型和大致大小。
- 如果业务允许,先停止继续写入或切换到新 key。
- 使用 UNLINK 做异步删除。
- 大批量清理时分批执行,避开高峰。
- 清理后观察延迟、内存和复制状态。
如果要删除的是一组大 key,也不要写个脚本瞬间全部删掉。分批、限速、可回滚,比“我一次性清干净”更适合线上系统。
五、哪些设计容易制造 BigKey
BigKey 很多时候不是 Redis 的问题,而是缓存建模的问题。
几个常见坑:
- 把 Redis 当数据库,什么都往一个 key 里堆。
- 为了减少 key 数量,把大量字段塞进一个 hash。
- 为了接口省事,把完整响应 JSON 缓存起来。
- 排行榜、粉丝列表、活动参与列表只增不裁剪。
- 日志、消息、行为记录用 list 一直追加。
- 缺少 TTL,历史数据长期留在内存里。
Redis 适合保存热点、短链路、访问模式明确的数据。它不适合承接无限增长的数据湖。
如果一个 key 的数据规模没有上限,基本就应该提前设计拆分策略。
六、一个比较实用的排查顺序
线上怀疑 BigKey 时,我会按这个顺序看。
第一步,看 Redis 指标。
重点看内存、慢查询、客户端输出缓冲区、网络流量、主从复制延迟。如果某个时间段接口慢,就对齐那个时间段的 Redis 指标。
第二步,看业务接口。
找出超时或耗时抖动最明显的接口,看它们访问了哪些 Redis key,是否有全量读取、全量集合查询、大对象反序列化。
第三步,扫描疑似 key。
用 SCAN 类方式分批扫描,结合类型长度和 MEMORY USAGE 找出大 key。不要在线上直接 KEYS。
第四步,判断处理策略。
如果是冷数据,考虑迁移或删除;如果是热数据,优先拆 key、拆字段、分页读取;如果短期不能改业务,至少先限制单次返回量,避免继续放大。
第五步,清理和观察。
删除用 UNLINK,批量清理要限速。清理后观察延迟、内存、慢查询、复制延迟,不要只看 key 没了就结束。
七、总结
BigKey 的本质不是“Redis 里有一个大东西”,而是这个大东西会让很多成本从 O(1) 变成不可控。
它可能让一次读取占满网络,让一个集合命令拖住主线程,让一次删除造成延迟毛刺,也可能在持久化和复制时把风险放大。
处理 BigKey 的关键不是背命令,而是建立三个习惯:
- 缓存建模时给数据规模设上限。
- 访问集合时避免一次性全量操作。
- 清理大 key 时考虑主线程、后台释放和业务高峰。
Redis 很快,但它快在“每个操作都足够轻”。当一个 key 变得越来越重,问题迟早会从缓存层冒到业务接口上。