写点什么

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

  • 2025-06-10
    福建
  • 本文字数:2893 字

    阅读完需:约 9 分钟

前言


有些小伙伴在工作中,可能遇到过这样的场景:老板突然要求统计 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. 时间复杂度:1 亿 key 需要 10 秒+(实测单核 CPU 0.1μs/key)

  2. 内存风暴:返回结果太多可能撑爆客户端内存

  3. 集群失效:在 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<String> 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<String> 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<String> 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):



4 分布式环境的分治策略


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


流程图如下:



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


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


public long clusterCount(JedisCluster cluster) {    Map<String, JedisPool> 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<String> 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# Keyspacedb0:keys=100000000,expires=20000,avg_ttl=3600
复制代码


优点:毫秒级返回。

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


方案 2:实时增量统计


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


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


@Configurationpublic class 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);            } else if(event.startsWith("__keyevent@0__:del")) {                redisTemplate.opsForValue().decrement("total_keys", 1);            }        }, new PatternTopic("__keyevent@*"));                return container;    }}
复制代码


使用监听器统计数量。


成本分析

  • 内存开销:额外存储计数器

  • CPU 开销:增加 5%-10%处理通知

  • 网络开销:集群模式下需跨节点同步


6 如何选择方案?


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


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



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



硬件法则:

  • CPU 密集型:多线程数=CPU 核心数×1.5

  • IO 密集型:线程数=CPU 核心数×3

  • 内存限制:控制批次大小(count 参数)


常见的业务场景:

  • 电商实时大屏:增量计数器+RedisTimeSeries

  • 离线数据分析:SCAN 导出到 Spark

  • 安全审计:多节点并行 SCAN


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


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


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


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18920430

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
假如给你1亿的Redis key,如何高效统计?_数据库_不在线第一只蜗牛_InfoQ写作社区