1. 完整回答
BigKey 指的是 Redis 中单个 Key 对应的 Value 过大,或者集合类型包含的元素过多。 比如一个 String 存储了几 MB 的数据,或者一个 Hash 包含几十万个字段,都可能形成 BigKey。
BigKey 主要有三个问题。
第一是性能问题。Redis 的命令执行主要由主线程完成,如果对大集合执行 HGETALL、SMEMBERS 或同步删除,就可能长时间占用主线程,导致其他请求延迟增加。
第二是网络问题。一次读取大量数据会占用网络带宽,还可能造成客户端超时。
第三是内存和集群问题。BigKey 容易造成节点内存分布不均,也会增加主从同步和集群迁移的成本。
解决 BigKey 通常分为三个步骤。
首先是发现 BigKey,可以使用 redis-cli --bigkeys 排查大集合,使用 --memkeys 或 MEMORY USAGE 查看实际内存占用。
其次是治理 BigKey。如果数据已经不需要,可以使用 UNLINK 异步删除,避免同步释放大量内存。如果数据还需要保留,就按照业务维度拆分成多个小 Key,例如将一个大 Hash 按用户 ID 或分桶规则拆分。
最后是预防 BigKey。限制单个 Key 的数据规模,避免全量读取大集合,采用分页或分批操作,并建立内存大小和元素数量的监控。
实际项目中,最核心的方案是业务拆分,UNLINK 主要解决存量数据的删除问题。
2. 核心原理与关键概念
2.1 核心原理
BigKey 的治理流程可以概括为:
发现问题 → 分析影响 → 删除或拆分 → 验证效果 → 持续监控
步骤 A:发现 BigKey
使用 SCAN 增量遍历键空间,再根据数据类型检查:
-
String:
STRLEN查看字符串长度。 -
Hash:
HLEN查看字段数量。 -
List:
LLEN查看列表长度。 -
Set:
SCARD查看成员数量。 -
ZSet:
ZCARD查看成员数量。 -
实际内存:
MEMORY USAGE查看单键内存占用。
redis-cli --bigkeys 和 --memkeys 可以简化上述排查过程。
步骤 B:分析影响
结合慢日志、请求延迟、网络流量和节点内存判断问题的严重程度。
关键在于区分两种开销:
-
命令执行开销:例如
HGETALL遍历大量字段,可能阻塞主线程。 -
数据传输开销:例如读取一个很大的 String,即使命令查找复杂度很低,仍然需要传输大量数据。
步骤 C:治理 BigKey
根据业务需求选择处理方式:
-
无用数据:
UNLINK异步删除。 -
大集合:按业务维度、哈希取模或时间范围拆分。
-
大字符串:拆分字段、按需缓存,必要时评估压缩。
-
全量读取:改成分页、范围查询或增量遍历。
步骤 D:验证与预防
检查单键内存、元素数量、请求延迟和节点负载是否下降,并在写入链路上限制数据持续增长。
2.2 关键概念
| 概念 | 含义 |
|---|---|
| BigKey | 单个 Key 的 Value 过大,或集合成员数量过多 |
| 内存大小 | Key 和 Value 实际占用的 Redis 内存,包括数据及管理开销 |
| 元素数量 | Hash、List、Set、ZSet 等集合包含的成员数量 |
| 命令复杂度 | Redis 执行一次操作需要处理的数据规模 |
| 业务分片 | 将一个逻辑数据集合拆成多个物理 Key |
BigKey 没有统一的大小阈值。 判断时需要结合数据类型、单次操作范围、请求频率、网络带宽和业务延迟要求。
3. 高频面试追问
3.1 Redis 的 BigKey 会造成哪些具体问题?
主要影响命令执行、网络传输和集群运行。
首先,Redis 的命令执行主要由主线程串行处理。如果对一个包含几十万个元素的 Hash 执行 HGETALL,需要遍历并返回所有字段,可能导致其他命令排队。
其次,大 Value 会增加网络传输量。例如一个 Key 每次返回 1MB 数据,每秒读取 1000 次,仅响应数据就接近 1GB/s。
最后,BigKey 会使集群节点的内存分布不均衡,并增加数据迁移、复制和持久化过程中的资源开销。
因此,BigKey 的风险与数据规模、访问方式以及操作频率都有关系。
3.2 如何在线上发现 BigKey?
通常结合 redis-cli、内存统计和慢日志排查。
首先可以执行:
redis-cli --bigkeys
redis-cli --memkeys
--bigkeys 主要查找字符串长度较大或集合元素数量较多的 Key,--memkeys 主要查找实际内存占用较大的 Key。
定位具体 Key 后,可以执行:
MEMORY USAGE key
查看内存占用。如果还存在请求延迟问题,可以结合 SLOWLOG GET 和延迟监控,判断是否存在全量读取、批量删除等耗时操作。
生产环境排查时需要控制扫描速度,避免持续扫描增加额外负载。Redis Cluster 还需要覆盖各个主节点的数据。
继续追问:MEMORY USAGE 得到的一定是精确值吗?
对于集合类型,默认结果可能是采样估算值。 MEMORY USAGE 默认抽取部分元素估算总内存,使用 SAMPLES 0 可以统计全部元素,但对于大集合开销较高,线上需要谨慎使用。
3.3 为什么使用 SCAN 排查,而不使用 KEYS?
因为 SCAN 支持增量遍历,可以降低单次操作对 Redis 主线程的影响。
KEYS * 会一次性遍历整个键空间,时间复杂度为 O(N)。当 Key 数量非常多时,容易造成明显的请求阻塞。
SCAN 使用游标分批遍历,每次处理有限范围,能够将遍历工作分散到多次请求中。
不过,SCAN 也有两个限制:
-
COUNT只是每次扫描数量的提示,不保证严格返回指定数量。 -
遍历期间如果数据发生变化,可能出现重复结果或遗漏变化中的数据。
因此,扫描程序需要处理重复数据,并控制扫描频率。
3.4 一个 Hash 有几十万个字段,执行 HGET 一定会很慢吗?
不一定,关键取决于具体命令和返回的数据量。
对于使用哈希表编码的大 Hash,HGET 查询单个字段,平均时间复杂度为 O(1),通常仍然比较高效。
但如果执行 HGETALL,就需要遍历整个 Hash,时间复杂度为 O(N),还要将全部数据返回客户端,容易引发延迟。
因此,大集合需要重点关注全量操作,例如:
-
HGETALL:获取所有字段。 -
SMEMBERS:获取所有集合成员。 -
LRANGE key 0 -1:获取整个 List。
对于有明确访问范围的业务,可以优先使用 HGET、HMGET 或分页、范围查询。需要遍历集合时,可以使用 HSCAN、SSCAN、ZSCAN 分批处理。
3.5 为什么删除 BigKey 推荐 UNLINK,而不是 DEL?
因为 UNLINK 能将主要的内存释放工作放到后台线程执行,降低主线程阻塞时间。
DEL 通常会同步删除对象并释放内存。对于包含大量成员的 Hash、Set、ZSet 或 List,内存释放成本可能比较高。
UNLINK 从 Redis 4.0 开始支持,它的处理过程是:
主线程将 Key 从键空间移除 → 返回删除结果 → 后台线程回收相关内存。
这样 Key 可以很快对客户端变为不可见,耗时的内存回收则在后台进行。
例如:
UNLINK user:followers:1001
对于仍然需要保留的集合,如果只想逐步清理部分数据,可以使用 HSCAN 配合分批 HDEL 等方式。
继续追问:为什么执行 UNLINK 后内存没有立即下降?
因为 Key 的逻辑删除和实际内存回收分成了两个阶段。后台线程需要时间释放内存。另外,内存分配器也可能保留已释放的内存页,因此 Redis 进程的 RSS 不一定立即降低。
3.6 如果 BigKey 需要长期保留,应该怎么拆分?
最直接的方式是按照业务维度或分桶规则,将一个大 Key 拆成多个小 Key。
例如,一个用户的粉丝数据全部存储在:
user:followers:1001
随着粉丝数量不断增加,这个集合可能变得非常大。
可以按粉丝 ID 哈希取模,将其拆成:
-
user:followers:1001:0 -
user:followers:1001:1 -
user:followers:1001:2 -
user:followers:1001:3
写入时,根据粉丝 ID 计算所属分桶,读取时按照相同规则定位。
这样每个 Key 的规模更容易控制,也方便分批操作。
不过,拆分之后需要处理查询聚合、数据一致性和过期时间等问题。
如果业务每次都必须读取全部粉丝,那么拆分后仍然需要传输全部数据。因此,还应该从业务层面改成分页查询或按需获取。
3.7 Redis 集群扩容能直接解决 BigKey 吗?
不能从根本上解决单个 BigKey 的问题。
Redis Cluster 根据 Key 计算哈希槽,一个 Key 只能归属一个哈希槽,由对应的节点负责存储。
即使增加 Redis 节点,原来的 BigKey 仍然整体存储在某个节点上,无法自动拆成多个小 Key。
扩容可以缓解整个集群的资源压力,但单个 BigKey 的操作成本仍然存在。
因此,要解决单键过大的问题,需要从业务层面拆分 Key,让不同分片能够分布在不同哈希槽上。
另外,如果拆分后的 Key 使用了相同的哈希标签 {...},它们仍然可能落在同一个槽中,需要根据实际分布目标设计 Key 名称。
3.8 已经存在的 BigKey,怎样在线上平滑拆分?
可以采用双写、增量迁移和逐步切换的方式。
整体流程是:
新增分片结构 → 新旧数据双写 → 分批迁移存量数据 → 校验一致性 → 切换读取 → 清理旧 Key。
首先让新增或更新的数据同时写入旧结构和新结构,再通过 HSCAN 等命令分批迁移旧数据。
迁移完成后,校验新旧数据的数量和内容,再将读流量逐步切换到新结构。
确认新结构稳定后,通过 UNLINK 删除旧 Key。
这里最重要的是迁移期间的数据一致性。如果业务持续写入,存量回填可能覆盖新数据,需要结合版本号、变更日志或其他同步机制防止旧数据覆盖新数据。
对于可以从数据库重新生成的缓存,也可以让新结构逐步预热,从而降低直接迁移大集合的复杂度。
3.9 BigKey 和 HotKey 有什么区别?处理方式一样吗?
两者关注的问题不同,处理方式也不同。
BigKey 主要指单个 Key 的数据量过大,HotKey 主要指单个 Key 的访问频率过高。
例如,一个保存几十万条评论的集合可能是 BigKey;一个只有几个字节、每秒被访问几十万次的商品库存 Key,则可能是 HotKey。
BigKey 主要通过拆分数据、减少单次操作范围、异步删除来解决。
HotKey 通常通过本地缓存、请求合并、读副本分流,以及合理的数据副本设计来缓解。
同一个 Key 也可能既是 BigKey 又是 HotKey,这种情况下需要同时控制数据规模和访问压力。
3.10 如何避免 BigKey 再次出现?
关键是从数据模型、操作方式和监控三个方面进行限制。
数据模型上,要限制单个 Key 的最大容量和集合元素数量。对于持续增长的日志、消息、排行榜等数据,可以采用时间分片、数量分桶或定期清理。
操作方式上,避免全量读取大集合,尽量使用分页、范围查询以及小批量操作。
监控方面,可以定期统计单键内存和元素数量,并结合慢日志、请求延迟、网络流量和节点内存使用率设置告警。
另外,还需要关注 BigKey 的过期和内存淘汰过程。设置 TTL 并不能消除大对象释放内存时的成本,可以结合 Redis 版本和业务需求评估惰性释放相关配置。
4. 易混淆概念对比
| 对比概念 | 核心区别 | 适用场景或注意事项 |
|---|---|---|
| BigKey vs HotKey | BigKey 是数据量大;HotKey 是访问频率高 | 分别关注单次操作成本和访问压力 |
--bigkeys vs --memkeys |
前者侧重长度和成员数量;后者侧重实际内存占用 | 排查 BigKey 时可以结合使用 |
| SCAN vs KEYS | SCAN 增量遍历;KEYS 一次遍历整个键空间 | 生产环境优先使用 SCAN 并控制扫描速度 |
| DEL vs UNLINK | DEL 通常同步释放内存;UNLINK 将主要回收工作异步执行 | 删除大集合优先考虑 UNLINK |
| 集群扩容 vs 业务拆分 | 扩容分散不同 Key;业务拆分降低单个 Key 的数据规模 | 解决单个 BigKey 需要业务拆分 |
5. 总结
Redis BigKey 指的是单个 Key 存储的数据过大,或者集合元素数量过多。它主要会带来命令执行延迟、网络传输压力和集群内存分布不均等问题。处理时先使用 --bigkeys、--memkeys 和 MEMORY USAGE 定位问题,再根据业务需求选择方案:不需要的数据通过 UNLINK 异步删除,需要保留的数据按照业务维度或分桶规则拆分,同时避免全量读取,改用分页或增量操作。业务拆分是长期治理方案,异步删除主要用于清理存量 BigKey。 此外,Redis 集群扩容不会自动拆分单个 Key,因此还需要从数据模型和写入链路上控制单键规模。