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

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

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

前言

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