bigkey 是指 key 对应的 value 所占的内存空间比较大,例如一个字符串类型的 value 可以最大存到 512 MB,一个列表类型的 value 最多可以存储 232-1 个元素。

如果按照数据结构来细分的话,一般分为字符串类型 bigkey 和非字符串类型 bigkey:

  • 字符串类型:单个 value 值很大,一般认为超过 10 kb 就是 bigkey,但是此值域具体的 OPS 有关。
  • 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。

一、危害

  • 占用内存过高:bigkey 会占用更多的内存,可能会导致内存不足,甚至导致内存耗尽,影响系统稳定性。
  • 内存空间不平衡(数据倾斜):例如在 Redis Cluster 中,bigkey 会造成节点的内存空间使用不均匀。频繁的 bigkey 修改可能会导致内存碎片化。
  • 超时阻塞:由于 Redis 单线程的特性,操作 bigkey 比较耗时,也就意味着阻塞 Redis 可能性增大。尤其是执行像 HGETALLSMEMBERSZRANGELRANGE 等命令时,如果操作的是大 key,可能会导致明显的延迟。
  • 网络拥塞:每次获取 bigkey 产生的网络流量较大,假设一个 bigkey 为 1 MB,每秒访问量为 1000,那么每秒产生 1000 MB 的流量,对于普通的千兆网卡(按照字节算是 128 MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个 bigkey 可能会对其他实例造成影响,其后果不堪设想。
  • 主从同步延迟:如果配置了主从同步,大 key 会导致主从同步延迟,由于大 key 占用的内存比较大,当主节点上的大 key 发生变化时,同步到从库时,会导致网络和处理上的延迟。

bigkey 的存在并不是完全致命的,如果这个 bigkey 存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果 bigkey 是一个热点 key(频繁访问)​,那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注 bigkey 的存在

1、bigKey 对持久化的影响

AOF 日志的写入策略决定了 BigKey 对主线程的阻塞程度:

  • always 策略:每次写入 AOF 文件后立即调用 fsync() 同步磁盘。若写入 bigkey,主线程会因大量数据刷盘而长时间阻塞。
  • everysec 策略:fsync() 由后台线程异步执行,BigKey 的持久化不会阻塞主线程,但若造成磁盘 I/O 压力过大,仍然会间接影响性能。
  • no 策略:数据仅写入内核缓冲区,由操作系统异步刷盘,bigkey 不会阻塞主线程,但宕机时可能丢失较多数据。

RDB 快照生成和 AOF 重写都依赖 fork 创建子进程,bigkey 会通过以下机制影响性能:

  • fork 延迟:fork() 需复制父进程的页表(虚拟内存映射)。若 Redis 实例内存占用大(如含多个 bigkey),页表复制耗时增加,导致主线程阻塞。
  • 写时复制:父进程或子进程修改共享内存中的 bigkey 时,触发写时复制(copy-on-write),bigkey 占用内存越大,复制耗时越长,主线程阻塞风险越高。

二、如何发现 bigkey

redis-cli --bigkeys 命令可以统计 bigkey 的分布。

判断一个 key 是否为 bigkey,只需要执行 debug object key 查看 serializedlength 属性即可,它表示 key 对应的 value 序列化之后的字节数。serializedlength 不代表真实的字节大小,它返回对象使用 RDB 编码序列化后的长度,值会偏小,但是对于排查 bigkey 有一定辅助作用。

在实际生产环境中发现 bigkey 有如下两种方式:

  • 被动收集:bigkey 一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员通过对异常的分析通常能找到异常原因可能是 bigkey,这种方式虽然不是被笔者推荐的,但是在实际生产环境中却大量存在,建议修改 Redis 客户端,当抛出异常时打印出所操作的 key,方便排查 bigkey 问题。
  • 主动检测:使用 scan + debug object,如果怀疑存在 bigkey,可以使用 scan 命令渐进的扫描出所有的 key,分别计算每个 key 的 serializedlength,找到对应 bigkey 进行相应的处理和报警,这种方式是比较推荐的方式。

注意

  • 如果键值个数比较多,scan + debug object 会比较慢,可以利用 Pipeline 机制来完成。
  • 对于元素个数较多的数据结构,debug object 执行速度比较慢,存在阻塞 Redis 的可能。
  • 如果有从节点,可以考虑在从节点上执行。

三、处理 bigkey

1、不可删除的 bigkey

对于不可删除的 bigkey,可以将 bigkey 进行分解,将 bigkey 拆分为多个小 key,降低单 key 的大小,读取时可以使用 mget 来批量读取。

如果是 String 类型,可以采用压缩算法进行压缩处理。

2、可删除的 bigkey

当发现 Redis 中有 bigkey 并且确认要删除时,无论是什么数据结构,del 命令都将其删除。但是删除 bigkey 通常来说会阻塞 Redis 服务。

对于字符串类型来说,由于字符串类型结构相对简单,删除速度比较快,但是随着 value 值的不断增大,删除速度也逐渐变慢。比如 512 KB 的字符串删除耗时 0.22 ms,10 MB 的字符串删除耗时 1 ms。

非字符串类型的数据结构在不同数量级、不同元素大小下对 bigkey 执行 del 命令总体上看元素个数越多、元素越大,删除时间越长,相对于字符串类型,这种删除速度已经足够可以阻塞 Redis。比如 100 万条元素的 hash、list、set、sorted set 分别耗时 2000 ms、266 ms、1319 ms、969 ms。

除了 string 类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞 Redis 的可能性。可以使用 scan 命令及其衍生命令 sscanhscanzscan,每次获取部分数据再进行删除。

2.1 异步删除

在 Redis 4.0 中,引入了 Lazy Free (惰性删除)模式,这是一种优化内存回收的机制,旨在解决 Redis 在处理大规模数据删除时可能导致的性能问题。Lazy Free 模式通过延迟删除操作,避免阻塞主线程,从而提升 Redis 的响应速度和整体性能。

Lazy Free 将删除操作从主线程中剥离出来,改为异步执行。Redis 不会立即释放被删除键的内存,而是将其标记为“待删除”,并在后台线程中逐步释放内存。这样可以避免主线程被长时间阻塞。

Redis 4.0 引入了 UNLINK 命令,专门用于异步删除键。

四、最佳实践

作为开发人员在业务开发时应注意不能将 Redis 简单暴力的使用,应该在数据结构的选择和设计上更加合理,例如出现了 bigkey,要思考一下可不可以做一些优化(例如拆分数据结构)尽量让这些 bigkey 消失在业务中,如果 bigkey 不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是 hgetall)​。最后,可喜的是,Redis 将在 4.0 版本支持 lazy delete free 的模式,那时删除 bigkey 不会阻塞 Redis。

相关链接

OB tags

#Redis