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 去重。

相关推荐
爱吃山竹的大肚肚2 分钟前
Spring Boot 与 Apache POI 实现复杂嵌套结构 Excel 导出
java·spring boot·后端·spring·spring cloud·excel
廋到被风吹走27 分钟前
【Spring】Spring ORM 深度解析
java·后端·spring
嘻哈baby27 分钟前
数据库连接池原理与HikariCP调优实战
后端
自由生长202429 分钟前
系统的雪崩-反脆弱设计
后端
卜锦元40 分钟前
Golang后端性能优化手册(第二章:缓存策略与优化)
开发语言·数据库·后端·性能优化·golang
掘金酱1 小时前
🏆2025 AI/Vibe Coding 对我的影响 | 年终技术征文
前端·人工智能·后端
狗头大军之江苏分军1 小时前
2026年了,前端到底算不算“夕阳行业”?
前端·javascript·后端
宋情写1 小时前
Springboot基础篇01-创建一个SpringBoot项目
java·spring boot·后端
今夕资源网1 小时前
go-tcnat内网端口映射 端口穿透 GO语言 免费开源
开发语言·后端·golang·go语言·端口映射·内网端口映射
踏浪无痕1 小时前
一个 Java 老兵转 Go 后,终于理解了“简单”的力量
后端·程序员·go