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

相关推荐
IT_陈寒10 小时前
Vite5.0性能翻倍秘籍:7个极致优化技巧让你的开发体验飞起来!
前端·人工智能·后端
Edward.W10 小时前
用 Go + HTML 实现 OpenHarmony 投屏(hdckit-go + WebSocket + Canvas 实战)
开发语言·后端·golang
南囝coding10 小时前
Claude 封禁中国?为啥我觉得是个好消息
前端·后端
六边形工程师11 小时前
Docker安装神通数据库ShenTong
后端
六边形工程师11 小时前
快速入门神通数据库
后端
重生成为编程大王11 小时前
FreeMarker快速入门指南
java·后端
Dear.爬虫11 小时前
Golang的协程调度器原理
开发语言·后端·golang
元闰子11 小时前
怎么用CXL加速数据库?· SIGMOD'25
数据库·后端·面试
幂简集成11 小时前
GraphQL API 性能优化实战:在线编程作业平台指南
后端·性能优化·graphql
编码浪子11 小时前
趣味学RUST基础篇(构建命令行程序1)
开发语言·后端·rust