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

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

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

前言

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