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

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

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

前言

有些小伙伴在工作中,可能遇到过这样的场景:老板突然要求统计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 Web 应用 异步数据库 数据同步 ACK 双主架构 循环复制 JumpServer SSL 堡垒机 跳板机 HTTPS TIME_WAIT 运维 负载均衡 HexHub Docker JumpServer安装 堡垒机安装 Linux安装JumpServer Deepseek 宝塔面板 Linux宝塔 生命周期 esxi esxi6 root密码不对 无法登录 web无法登录 服务器 管理口 序列 核心机制 HTTPS加密 Windows Windows server net3.5 .NET 安装出错 服务器性能 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 宝塔面板打不开 宝塔面板无法访问 开源 PostgreSQL 存储引擎 Windows宝塔 Mysql重置密码 Oracle 处理机制 无法访问宝塔面板 连接控制 机制 InnoDB 数据库锁 监控 Serverless 无服务器 语言 Spring Redis 异步化 ES 协同 技术 Undo Log group by 索引 SQL 优化 万能公式 分页查询 缓存方案 缓存架构 缓存穿透 高可用 动态查询 GreatSQL 连接数 机器学习 工具 响应模型 查询 scp Linux的scp怎么用 scp上传 scp下载 scp命令 日志文件 MIXED 3 R edis 线程 SVM Embedding 加密 场景 R2DBC 锁机制 数据 主库 Netstat Linux 服务器 端口 openHalo Linux 安全 Postgres OTel Iceberg RocketMQ 长轮询 配置 云原生 存储 AI 助手 ​Redis 推荐模型 SQLite-Web SQLite 数据库管理工具 Recursive 自定义序列化 SQLark 共享锁 向量数据库 大模型 Hash 字段 PG DBA 电商 系统 Ftp OB 单机版 国产数据库 架构 启动故障 MySQL 9.3 修改DNS Centos7如何修改DNS 人工智能 推荐系统 数据分类 • 索引 • 数据库 磁盘架构 流量 Rsync sftp 服务器 参数 防火墙 黑客 线上 库存 预扣 redo log 重做日志 分库 分表 业务 同城 双活 信息化 智能运维 Python mini-redis INCR指令 MVCC 不宕机 聚簇 非聚簇 传统数据库 向量化 向量库 Milvus Canal PostGIS 高效统计 今天这篇文章就跟大家 行业 趋势 INSERT COMPACT 缓存 Redisson 锁芯 网络架构 网络配置 Doris SeaTunnel prometheus Alert 数据备份 虚拟服务器 虚拟机 内存 filelock 事务 Java 开发 语句 Web 窗口 函数 ZODB RDB AOF 数据脱敏 加密算法 MongoDB 数据结构 读写 引擎 性能 容器 IT运维 核心架构 订阅机制 Go 数据库迁移 失效 OAuth2 Token 自动重启 数据类型 频繁 Codis B+Tree ID 字段 模型 Redis 8.0 发件箱模式 容器化 分布式 集中式 崖山 新版本 网络故障 SSH 微软 SQL Server AI功能 MCP 开放协议 DBMS 管理系统 聚簇索引 非聚簇索引 播客 SpringAI 数据集成工具 Web 接口 部署 QPS 高并发 JOIN Entity 数据页 排行榜 排序 速度 服务器中毒 Caffeine CP 原子性 Testcloud 云端自动化 网络 Pottery StarRocks 数据仓库 工具链 池化技术 连接池 Redka 主从复制 代理 事务隔离 分布式架构 分布式锁​ 分页方案 排版 1 大表 业务场景 dbt 数据转换工具 悲观锁 乐观锁 LRU 分页 AIOPS 日志 优化器 EasyExcel MySQL8 单点故障 sqlmock Order 意向锁 记录锁 仪表盘 数据字典 兼容性 事务同步 InfluxDB 对象 UUIDv7 主键 ReadView 单线程 RAG HelixDB Ansible UUID ID 订单 Crash 代码 IT 双引擎 LLM 字典 Weaviate 产业链 编程 Valkey Valkey8.0 恢复数据 千万级 MGR 分布式集群 分布式锁 Zookeeper 线程安全 Pump List 类型 关系数据库 拦截器 动态代理 Next-Key 表空间 解锁 调优 慢SQL优化 快照读 当前读 视图 国产 用户 RR 互联网 GitHub Git 神经系统 矢量存储 数据库类型 AI代理 算法 查询规划 count(*) count(主键) 行数 技巧 CAS 并发控制 恢复机制 多线程 闪回