Redis 键扫描优化:从 KEYS 到 SCAN 的优雅升级

Redis 键扫描优化:从 KEYS 到 SCAN 的优雅升级

简单说一下应用的场景,这个是生产项目的正式需求

用户APP使用时间的上报,每次用户退出后会调用一个接口,存储到redis中。我们将用户 app 使用时长数据存储在 Redis 中,键格式如 user_app_time:{date}:{userId},并定期批量插入数据库(如 MySQL)以持久化

KEYS 的问题:阻塞与资源消耗

在旧实现中,我们先计算前一天日期,形成模式如 user_app_time:*:{date},然后:

ini 复制代码
Set<String> redisKeys = stringRedisTemplate.keys(pattern);

这会一次性获取所有匹配键。对于少量键,这没问题。但想象一个流行 app,有数百万用户------Redis 可能每天持有数十万甚至上百万键。KEYS 命令线性扫描整个键空间,并在执行期间阻塞 Redis 服务器。其他操作无法进行,导致:

  • 服务器阻塞:Redis 大多数操作单线程,KEYS 会暂停一切。如果数据集大,可能引起延迟峰值、超时甚至中断。
  • 内存开销:所有键一次性加载到客户端内存,如果集合巨大,Java 应用可能出现 OutOfMemory 错误。
  • 可扩展性问题:随着数据增长,执行时间线性增加。我们见过 ~50,000 键的任务耗时 30 秒以上,对定时任务不可接受。
  • 无分页:全量或无,無法增量处理。

获取键后,我们循环解析值、构建领域对象,并分批 1000 条插入数据库。错误处理简单,所有记录先收集到一个大列表再切片------内存效率低下。在一次事件中,Redis 其他查询响应时间激增,影响实时用户数据处理。

SCAN 的优雅解决方案:游标增量处理

引入 SCAN:Redis 的非阻塞键迭代替代。它使用游标逐步遍历键空间,允许分批处理而不停止服务器。在优化代码中,我们用 RedisCallback 包装:

arduino 复制代码
List<UserUseAppTimeDomain> buffer = new ArrayList<>(1000);
stringRedisTemplate.execute((RedisConnection connection) -> {
    ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
    try (Cursor<byte[]> cursor = connection.scan(options)) {
        List<UserUseAppTimeDomain> buffer = new ArrayList<>();
        while (cursor.hasNext()) {
            String redisKey = new String(cursor.next(), StandardCharsets.UTF_8);
            // 解析值、构建记录、添加到 buffer
            if (buffer.size() >= 1000) {
                // 批量插入数据库
                buffer.clear();
            }
        }
        // 处理剩余记录
        return null;
    } catch (Exception e) {
        // 错误处理
    }
});

优化后,我改用 SCAN 命令,这是 Redis 推荐的非阻塞替代方案。

它通过游标逐步遍历键空间,不会一次性阻塞服务器。在代码中,我用 RedisCallback 包装了执行逻辑:设置 ScanOptions ,包括 match 模式和 count=1000 来控制每次迭代的批次大小。然后,用 try-with-resources 打开游标,while 循环 hasNext() 来逐个处理键 。关键是引入了一个 buffer 列表,边扫描边解析键和值,构建记录添加到 buffer。当 buffer 达到 1000 条时,就立即批量插入数据库并清空

这样实现流式处理,不用等到所有数据都收集完再操作,内存使用更高效,也减少了峰值占用。处理完所有键后,再插入剩余的 buffer 记录,最后设置一个锁键标记任务完成,以防重复执行。异常处理也更稳当,如果扫描出错,会发邮件警报并抛异常。

这个变化带来的性能提升很明显:在同样的 ~50,000 键场景下,任务时间从 30 秒降到 5 秒以内,Redis 保持负载均衡,其他查询几乎不受影响。

Scan就像是扫描-解析-插入像一条流式管道,无缝衔接。我们还调整了键格式,从原版的 user_app_time:{userId}:{date} 改为 {date}:{userId},以更好地匹配扫描模式

当然,SCAN 有个小注意点:Redis 哈希表扩展时可能返回重复键,但实际影响不大,如果需要可以加 Set 去重。

相关推荐
柳杉12 分钟前
建议收藏 | 2026年AI工具封神榜:从Sora到混元3D,生产力彻底爆发
前端·人工智能·后端
仙俊红20 分钟前
spring的IoC(控制反转)面试题
java·后端·spring
小楼v31 分钟前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地44 分钟前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209251 小时前
Guava Cache 原理与实战
java·后端·spring
Yuer20252 小时前
什么是 Rust 语境下的“量化算子”——一个工程对象的最小定义
开发语言·后端·rust·edca os·可控ai
短剑重铸之日2 小时前
《7天学会Redis》Day 5 - Redis Cluster集群架构
数据库·redis·后端·缓存·架构·cluster
计算机程序设计小李同学2 小时前
基于SSM框架的动画制作及分享网站设计
java·前端·后端·学习·ssm
+VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue小型房屋租赁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Victor3563 小时前
Hibernate(43)Hibernate中的级联删除如何实现?
后端