欢迎来到徐庆高(Tea)的个人博客网站
磨难很爱我,一度将我连根拔起。从惊慌失措到心力交瘁,我孤身一人,但并不孤独无依。依赖那些依赖我的人,信任那些信任我的人,帮助那些给予我帮助的人。如果我愿意,可以分裂成无数面镜子,让他们看见我,就像看见自己。察言观色和模仿学习是我的领域。像每个深受创伤的人那样,最终,我学会了随遇而安。
当前位置: 日志文章 > 详细内容

Redis中BigKey的隐患问题小结

2025年07月15日 Redis
一、什么是 bigkey?1、bigkey的定义bigkey是指那些在 redis 中存储了大量数据,或者其序列化后占用大量内存空间的键。它不仅仅是一个值很长的字符串,更常见的是指那些包含巨多元素的集

一、什么是 bigkey?

1、bigkey的定义

bigkey是指那些在 redis 中存储了大量数据,或者其序列化后占用大量内存空间的键。它不仅仅是一个值很长的字符串,更常见的是指那些包含巨多元素的集合类型(如 hashlistsetzset)。

想象一下:

  • 一个 string 类型的 key,存储了一个几 mb 甚至几十 mb 的 json 字符串。

  • 一个 list 类型的 key,里面有几百万个元素,就像一个永无止境的日志队列。

  • 一个 hash 类型的 key,存储了几十万个字段,代表了一个复杂对象的巨量属性。

  • 一个 set 或 zset 类型的 key,包含了数百万的成员。

这些,都是 bigkey。它们就像 redis 内存中的“巨无霸”,吞噬着宝贵的资源。

2、bigkey 为什么是性能杀手?

bigkey 绝不仅仅是占用更多内存那么简单。它会引发一系列连锁反应,严重影响 redis 性能和稳定性:

  • 内存失衡与 oom 风险: 一个 bigkey 就能瞬间吃掉大量内存,可能导致 redis 内存使用率飙升,甚至触发操作系统的 oom(out of memory),进而导致 redis 实例崩溃或频繁发生 swap(内存交换到磁盘),严重拖慢性能。
  • 网络阻塞: 当客户端获取或更新 bigkey 时,需要传输大量数据。这会占用大量的网络带宽,导致其他正常、小巧的请求被阻塞,增加整体延迟。
  • cpu 耗尽与服务阻塞: redis 是单线程模型,对 bigkey 的操作,比如删除 (del)、过期 (expire)、序列化/反序列化等,都会消耗大量的 cpu 资源。这些操作会长时间阻塞 redis 主线程,导致所有其他命令都排队等待,降低 redis 的吞吐量和响应速度。
  • 集群稳定性下降:redis cluster 模式下,bigkey 的迁移(re-sharding)会耗费大量时间,期间可能导致节点卡顿、迁移失败,甚至引起整个集群的不稳定。主从复制时,bigkey 的传输也会占用大量带宽,影响主从同步的效率。
  • 持久化开销增大: rdb 快照或 aof 重写时,bigkey 的处理会显著增加持久化的耗时和生成的文件大小。

二、如何发现 redis 中的 bigkey? 

1、使用redis-cli --bigkeys命令(推荐)

这是 redis 官方推荐且最简单直接的方法。它会遍历 redis 中的所有 key,计算每个 key 的内存大小或元素数量,并按类型进行统计,最后列出每个类型中最大的 n 个 key。它通过 scan 命令分批次遍历,不会阻塞 redis 服务

redis-cli -h <host> -p <port> --bigkeys -i 0.01

命令说明

  • -h <host>: redis 服务器地址。

  • -p <port>: redis 服务器端口。

  • --bigkeys: 启用 bigkey 扫描模式。

  • -i 0.01 (可选): 指定 scan 命令的间隔时间,单位为秒。这可以减小对 redis 服务器的压力,但会延长扫描时间。默认不设置或设置为 0,表示尽可能快地扫描。

  • 优点: 简单易用,对在线 redis 服务的阻塞影响小。
  • 缺点: 只能获取当前时刻的 bigkey 快照,无法实时监控。在大 key 频繁变动的场景下,可能无法及时捕捉。

2. 使用 rdb 工具进行离线分析

redis 的 rdb 文件是内存数据的二进制快照。通过分析 rdb 文件,我们可以离线地获取 redis 中所有 key 的详细信息(包括大小和类型),而不会对在线的 redis 服务造成任何影响。这对于生产环境来说是一个非常安全的分析方式。

常用工具:

  • redis-rdb-tools (python): 这是一个功能强大的 rdb 文件解析器,可以生成报告、csv 文件,帮助你分析 key 的大小、类型、过期时间等。

  • redis-memory-for-json (node.js): 另一个流行的 rdb 分析工具。

使用方式(以 redis-rdb-tools 为例):

  1. 生成 rdb 文件: 在 redis 命令行中执行 bgsave 命令,生成最新的 rdb 文件。

  2. 拷贝 rdb 文件: 将生成的 rdb 文件拷贝到分析工具所在的机器。

  3. 运行分析命令:

rdb --command bigkeys /path/to/dump.rdb
# 或者生成 json 格式报告进行更详细分析
rdb -c json /path/to/dump.rdb > dump.json
  • 优点: 零入侵,对在线 redis 服务无任何性能影响;可以获取历史某个时间点的数据快照。
  • 缺点: 无法实时监控;分析需要额外的工具和环境;rdb 文件可能很大,分析耗时。

3. 实时监控与自定义脚本

对于需要实时或近实时发现 bigkey 的场景,结合 redis 的监控数据和自定义脚本是更灵活的选择。

  • 监控内存指标: 持续监控 redis 实例的内存使用情况 (used_memory) 和 key 数量 (db0:keys)。如果内存突然飙升但 key 数量变化不大,很可能是有 bigkey 产生。

  • info 命令: 定期执行 info memoryinfo keyspace 命令,收集内存和 key 空间的信息。虽然不能直接定位 bigkey,但可以作为 bigkey 产生的预警信号。

  • scan 结合类型特有命令: 编写脚本(如 python、java 等),使用 scan 命令分批遍历 key。对于每个 key,先用 type 命令判断其类型,然后根据类型使用对应的命令来获取其大小或元素数量。一旦发现超过预设阈值的 key,就记录下来并触发告警。

java 伪代码示例(使用 jedis 客户端):

添加 jedis 依赖

<dependency>
    <groupid>redis.clients</groupid>
    <artifactid>jedis</artifactid>
    <version>5.1.0</version> 
</dependency>

java 代码

import redis.clients.jedis.jedis;
import redis.clients.jedis.params.scanparams;
import redis.clients.jedis.resps.scanresult;
import java.util.set;
public class redisbigkeyscanner {
    private static final string redis_host = "localhost";
    private static final int redis_port = 6379;
    private static final string redis_password = null; // 如果有密码则填写
    // 定义 bigkey 的阈值
    // 字符串 10mb
    private static final long big_string_threshold_bytes = 10 * 1024 * 1024; 
    // 集合类型 10万元素
    private static final long big_collection_threshold_elements = 100000;  
    public static void main(string[] args) {
        // 使用 try-with-resources 确保 jedis 连接被正确关闭
        try (jedis jedis = new jedis(redis_host, redis_port)) {
            // 如果 redis 服务器有密码,进行认证
            if (redis_password != null && !redis_password.isempty()) {
                jedis.auth(redis_password);
            }
            system.out.println("scanning for bigkeys...");
            // 初始化 scan 命令的游标,从头开始扫描
            string cursor = scanparams.scan_pointer_start;
            // 设置每次 scan 命令返回的 key 数量
            scanparams scanparams = new scanparams().count(1000);
            // 循环执行 scan 命令,直到游标回到起点(表示所有 key 都已遍历)
            do {
                scanresult<string> scanresult = jedis.scan(cursor, scanparams);
                // 更新游标
                cursor = scanresult.getcursor();
                // 获取当前批次扫描到的 key 集合
                set<string> keys = scanresult.getresult();
                // 遍历当前批次获取到的所有 key
                for (string key : keys) {
                    string keytype = jedis.type(key);
                    // 根据 key 类型,使用不同的命令来判断是否是 bigkey
                    switch (keytype) {
                        case "string":
                            // 获取字符串的长度(字节数)
                            long stringsize = jedis.strlen(key);
                            if (stringsize > big_string_threshold_bytes) {
                                system.out.printf("  [big key] string: %s (size: %.2f mb)%n", key, (double) stringsize / (1024 * 1024));
                            }
                            break;
                        case "list":
                            // 获取列表的元素数量
                            long listlength = jedis.llen(key);
                            if (listlength > big_collection_threshold_elements) {
                                system.out.printf("  [big key] list: %s (elements: %d)%n", key, listlength);
                            }
                            break;
                        case "hash":
                            // 获取哈希表的字段数量
                            long hashfields = jedis.hlen(key);
                            if (hashfields > big_collection_threshold_elements) {
                                system.out.printf("  [big key] hash: %s (fields: %d)%n", key, hashfields);
                            }
                            break;
                        case "set":
                            // 获取集合的成员数量
                            long setmembers = jedis.scard(key);
                            if (setmembers > big_collection_threshold_elements) {
                                system.out.printf("  [big key] set: %s (members: %d)%n", key, setmembers);
                            }
                            break;
                        case "zset":
                            // 获取有序集合的成员数量
                            long zsetmembers = jedis.zcard(key);
                            if (zsetmembers > big_collection_threshold_elements) {
                                system.out.printf("  [big key] zset: %s (members: %d)%n", key, zsetmembers);
                            }
                            break;
                        // 可以根据需要添加其他 redis 数据类型的判断或跳过
                        default:
                            break;
                    }
                }
            // 当游标回到起始点 "0" 时,表示遍历完成
            } while (!cursor.equals(scanparams.scan_pointer_start));
            system.out.println("bigkey scan complete.");
        } catch (exception e) {
            system.err.println("error connecting to redis or during scan: " + e.getmessage());
            e.printstacktrace();
        }
    }
}
  • 优点: 灵活性高,可以根据业务需求自定义 bigkey 的判断标准和告警策略;能够实现实时或准实时监控。
  • 缺点: 脚本开发和维护成本较高;需要考虑对 redis 性能的影响,合理设置 scancount 参数和扫描频率。 

4. 业务层面排查

有时 bigkey 的产生源于业务逻辑的缺陷,比如某个业务 id 对应的 key 不断积累数据,从未清理。

  • 慢查询日志: 定期检查 redis 的慢查询日志(slowlog),看是否有针对特定 key 的操作耗时过长。这往往是 bigkey 的一个重要信号。

  • 业务梳理: 定期梳理业务中数据量可能持续增长的场景,例如用户操作日志、动态列表、排行榜等,评估其是否可能产生 bigkey,并提前设计好清理或拆分方案。

  • 代码审查: 检查应用程序代码中是否有不合理的数据结构使用,例如将一个复杂对象直接序列化成一个大字符串存储,或者在单个 key 下无限追加数据。

到此这篇关于redis中bigkey的隐患的文章就介绍到这了,更多相关redis bigkey内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!