• 给你1亿的Redis key,如何高效统计?

给你1亿的Redis key,如何高效统计?

2025-06-09 10:00:04 栏目:宝塔面板 4 阅读

前言

有些小伙伴在工作中,可能遇到过这样的场景:老板突然要求统计Redis中所有key的数量,你随手执行了KEYS *命令,下一秒监控告警疯狂闪烁——整个Redis集群彻底卡死,线上服务大面积瘫痪。

今天这篇文章就跟大家一起聊聊如果给你1亿个Redis key,如何高效统计这个话题,希望对你会有所帮助。

1.为什么不建议使用KEYS命令?

Redis的单线程模型是其高性能的核心,但也是最大的软肋。

当Redis执行 KEYS * 命令时,内部的流程如下:

图片

Redis的单线程模型是其高性能的核心,但同时也带来一个关键限制:所有命令都是串行执行的。

当我们执行 KEYS * 命令时:

Redis必须遍历整个key空间(时间复杂度O(N))

在遍历完成前,无法处理其他任何命令

对于1亿个key,即使每个key查找只需0.1微秒,总耗时也高达10秒!

致命三连击

  • 时间复杂度:1亿key需要10秒+(实测单核CPU 0.1μs/key)
  • 内存风暴:返回结果太多可能撑爆客户端内存
  • 集群失效:在Cluster模式中只能查当前节点的数据。

如果Redis一次性返回的数据太多,可能会有OOM问题:

127.0.0.1:6379> KEYS *
(卡死10秒...)
(error) OOM command not allowed when used memory > 'maxmemory'

超过了最大内存。

那么,Redis中有1亿key,我们要如何统计数据呢?

2.SCAN命令

SCAN命令通过游标分批遍历,每次只返回少量key,避免阻塞。

Java版基础SCAN的代码如下:

public long safeCount(Jedis jedis) {
    long total = 0;
    String cursor = "0";
    ScanParams params = new ScanParams().count(500); // 每批500个
    
    do {
        ScanResult rs = jedis.scan(cursor, params);
        cursor = rs.getCursor();
        total += rs.getResult().size();
    } while (!"0".equals(cursor)); // 游标0表示结束
    
    return total;
}

使用游标查询Redis中的数据,一次扫描500条数据。

但问题来了:1亿key需要多久?

  • 每次SCAN耗时≈3ms
  • 每次返回500key
  • 总次数=1亿/500=20万次
  • 总耗时≈20万×3ms=600秒=10分钟!

3.多线程并发SCAN方案

现代服务器都是多核CPU,单线程扫描是资源浪费。

看多线程优化方案如下:

图片

多线程并发SCAN代码如下:

public long parallelCount(JedisPool pool, int threads) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(threads);
    AtomicLong total = new AtomicLong(0);
    
    // 生成初始游标(实际需要更智能的分段)
    List cursors = new ArrayList<>();
    for (int i = 0; i < threads; i++) {
        cursors.add(String.valueOf(i));
    }

    CountDownLatch latch = new CountDownLatch(threads);
    
    for (String cursor : cursors) {
        executor.execute(() -> {
            try (Jedis jedis = pool.getResource()) {
                String cur = cursor;
                do {
                    ScanResult rs = jedis.scan(cur, new ScanParams().count(500));
                    cur = rs.getCursor();
                    total.addAndGet(rs.getResult().size());
                } while (!"0".equals(cur));
                latch.countDown();
            }
        });
    }
    
    latch.await();
    executor.shutdown();
    return total.get();
}

使用线程池、AtomicLong和CountDownLatch配合使用,实现了多线程扫描数据,最终将结果合并。

性能对比(32核CPU/1亿key):

方案

线程数

耗时

资源占用

单线程SCAN

1

580s

CPU 5%

多线程SCAN

32

18s

CPU 800%

4.分布式环境的分治策略

如果你的系统重使用了Redis Cluster集群模式,该模式会将数据分散在16384个槽(slot)中,统计就需要节点协同。

流程图如下:

图片

每一个Redis Cluster集群中的master服务节点,都负责统计一定范围的槽(slot)中的数据,最后将数据聚合起来返回。

集群版并行统计代码如下:

public long clusterCount(JedisCluster cluster) {
    Map nodes = cluster.getClusterNodes();
    AtomicLong total = new AtomicLong(0);
    
    nodes.values().parallelStream().forEach(pool -> {
        try (Jedis jedis = pool.getResource()) {
            // 跳过从节点
            if (jedis.info("replication").contains("role:slave")) return; 
            
            String cursor = "0";
            do {
                ScanResult rs = jedis.scan(cursor, new ScanParams().count(500));
                total.addAndGet(rs.getResult().size());
                cursor = rs.getCursor();
            } while (!"0".equals(cursor));
        }
    });
    
    return total.get();
}

这里使用了parallelStream,会并发统计Redis不同的master节点中的数据。

5.毫秒统计方案

方案1:使用内置计数器

如果只想统计一个数量,可以使用Redis内置计数器,瞬时但非精确。

127.0.0.1:6379> info keyspace
# Keyspace
db0:keys=100000000,expires=20000,avg_ttl=3600

优点:毫秒级返回。

缺点:包含已过期未删除的key,法按模式过滤数据。

方案2:实时增量统计

实时增量统计方案精准但复杂。

基于键空间通知的实时计数器,具体代码如下:

@Configuration
publicclass KeyCounterConfig {
    
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        
        container.addMessageListener((message, pattern) -> {
            String event = new String(message.getBody());
            if(event.startsWith("__keyevent@0__:set")) {
                redisTemplate.opsForValue().increment("total_keys", 1);
            } elseif(event.startsWith("__keyevent@0__:del")) {
                redisTemplate.opsForValue().decrement("total_keys", 1);
            }
        }, new PatternTopic("__keyevent@*"));
        
        return container;
    }
}

使用监听器统计数量。

成本分析

  • 内存开销:额外存储计数器
  • CPU开销:增加5%-10%处理通知
  • 网络开销:集群模式下需跨节点同步

6.如何选择方案?

本文中列举出了多个统计Redis中key的方案,那么我们在实际工作中如何选择呢?

下面用一张图给大家列举了选择路线:

图片

各方案的时间和空间复杂度如下:

方案

时间复杂度

空间复杂度

精度

KEYS命令

O(n)

O(n)

精确

SCAN遍历

O(n)

O(1)

精确

内置计数器

O(1)

O(1)

不精确

增量统计

O(1)

O(1)

精确

硬件法则:

  • CPU密集型:多线程数=CPU核心数×1.5
  • IO密集型:线程数=CPU核心数×3
  • 内存限制:控制批次大小(count参数)

常见的业务场景:

  • 电商实时大屏:增量计数器+RedisTimeSeries
  • 离线数据分析:SCAN导出到Spark
  • 安全审计:多节点并行SCAN

终极箴言:✅ 精确统计用分治✅ 实时查询用增量✅ 趋势分析用采样❌ 暴力遍历是自杀

真正的高手不是能解决难题的人,而是能预见并规避难题的人

在海量数据时代,选择比努力更重要——理解数据本质,才能驾驭数据洪流。


本文地址:https://www.yitenyun.com/276.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK 双主架构 循环复制 Web 应用 异步数据库 序列 核心机制 生命周期 Deepseek 宝塔面板 Linux宝塔 Docker JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS Windows宝塔 Mysql重置密码 无法访问宝塔面板 HTTPS加密 连接控制 机制 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 ES 协同 修改DNS Centos7如何修改DNS scp Linux的scp怎么用 scp上传 scp下载 scp命令 防火墙 服务器 黑客 Serverless 无服务器 语言 Oracle 处理机制 存储 Spring SQL 动态查询 Linux 安全 RocketMQ 长轮询 配置 加密 场景 网络架构 工具 网络配置 Rsync MySQL 9.3 缓存方案 缓存架构 缓存穿透 开源 PostgreSQL 存储引擎 HexHub Canal 架构 InnoDB 日志文件 MIXED 3 信息化 智能运维 响应模型 线上 库存 预扣 索引 数据 业务 监控 数据库锁 单点故障 B+Tree ID 字段 AI 助手 聚簇 非聚簇 Redis 自定义序列化 云原生 Redis 8.0 openHalo GreatSQL Hash 字段 分库 分表 SpringAI 优化 万能公式 分页查询 OB 单机版 DBMS 管理系统 数据集成工具 prometheus Alert ​Redis 机器学习 推荐模型 SVM Embedding 虚拟服务器 虚拟机 内存 RDB AOF 排行榜 排序 查询 SQLite Redka SQLite-Web 数据库管理工具 sqlmock sftp 服务器 参数 共享锁 PostGIS 系统 SQLark • 索引 • 数据库 Netstat Linux 服务器 端口 技术 Doris SeaTunnel 向量数据库 大模型 不宕机 自动重启 运维 同城 双活 容器化 缓存 Postgres OTel Iceberg OAuth2 Token Entity 开发 Testcloud 云端自动化 EasyExcel MySQL8 分布式架构 分布式锁​ 电商 聚簇索引 非聚簇索引 崖山 新版本 高可用 StarRocks 数据仓库 人工智能 推荐系统 分页 数据结构 redo log 重做日志 IT AIOPS 数据备份 MongoDB 容器 数据类型 Milvus 向量库 MVCC IT运维 池化技术 连接池 Python Web LRU mini-redis INCR指令 悲观锁 乐观锁 Ftp Caffeine CP 部署 流量 数据脱敏 加密算法 MCP 开放协议 Web 接口 ZODB 微软 SQL Server AI功能 单线程 线程 磁盘架构 事务隔离 窗口 函数 字典 RAG HelixDB 原子性 PG DBA 对象 速度 服务器中毒 R2DBC QPS 高并发 双引擎 1 主库 Order SSH 网络 Pottery 频繁 Codis 引擎 优化器 性能 LLM Crash 代码 模型 工具链 Undo Log List 类型 dbt 数据转换工具 发件箱模式 意向锁 记录锁 事务同步 Redisson 锁芯 InfluxDB 传统数据库 向量化 网络故障 线程安全 UUIDv7 主键 INSERT COMPACT 连接数 仪表盘 订单 JOIN filelock 管理口 高效统计 今天这篇文章就跟大家 分页方案 排版