scan是唯一安全、可上线的替代方案,但必须配合游标重试、结果去重、分批处理三要素,否则照样卡顿甚至丢数据;keys因o(n)全量阻塞扫描,在50万+ key时可致200ms以上主线程停摆,引发雪崩,官方明确禁用生产环境。

直接说结论:SCAN 是唯一安全、可上线的替代方案,但必须配合游标重试、结果去重、分批处理三要素,否则照样卡顿甚至丢数据。
为什么 keys("*user:*") 在线上等于埋雷
Redis 单线程模型下,KEYS 命令会遍历整个键空间,时间复杂度 O(N)。当 DB 中有 50 万+ key 时,一次 KEYS *user:* 可能阻塞主线程 200ms 以上,期间所有读写请求排队等待——这不是慢,是“雪崩前兆”。官方文档明确标注 KEYS 仅限调试环境使用,生产环境禁用。
常见错误现象包括:
- Redis 响应延迟突增至 1s+,监控看到
latency指标尖刺 - Spring Boot 应用大量
RedisConnectionFailureException或超时熔断 - 集群中某节点 CPU 100%,其他节点负载不均(因 SCAN 不跨 slot,而 KEYS 会触发全量扫描)
ScanOptions.match() 的坑:通配符和大小写敏感性
ScanOptions.scanOptions().match("user:*") 看似简单,但实际匹配行为受 Redis 版本和编码影响:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 通配符只支持
*(任意字符)、?(单字符)、[abc](字符类),不支持正则 - 匹配区分大小写:
"User:*"和"user:*"是两个不同 pattern - 如果 key 是通过
StringRedisTemplate存入(默认 UTF-8 序列化),但部分服务用JdkSerializationRedisTemplate写入二进制 key,则SCAN无法匹配——因为字节内容不一致 - 集群环境下,
MATCH只在当前 slot 执行,不会跨 slot 聚合结果,所以不能假设“扫完一次就全了”
Spring Data Redis 中 Cursor.next() 报 NoSuchElementException 怎么办
这是 Spring Data Redis 早期版本(1.6.x 之前)的经典 bug:游标已到末尾,但 cursor.hasNext() 返回 true,紧接着调 cursor.next() 就抛 NoSuchElementException。
实操建议:
- 升级到
spring-data-redis >= 2.1.0(对应 Spring Boot 2.1+),该问题已在ScanCursor中修复 - 若无法升级,改用 try-catch 包裹
cursor.next(),捕获NoSuchElementException后主动 break - 永远检查游标返回值:
if (cursor.getCursor().equals("0"))才算真正结束,不能只依赖hasNext() - 别用
COUNT Integer.MAX_VALUE——这会让 SCAN 退化成 KEYS,失去渐进式优势
BigKey 场景下 Scan 必须加限流和分页包装
即使用了 SCAN,面对百万级匹配 key,一次性拉取仍可能 OOM 或拖垮网络。真实生产环境必须做两层隔离:
- 外层按业务维度切分:比如按日期前缀
user:202605:*缩小扫描范围 - 内层用
COUNT 100控制单次扫描量,并在每次迭代后Thread.sleep(5)(非必须,但对高负载集群很友好) - 结果必须用
Set<string></string>去重——SCAN 允许重复返回同一 key(尤其在 rehash 期间) - 删除操作禁止直接
redisTemplate.delete(matchedKeys),应拆成每批 ≤ 1000 个 key 异步批量删,避免命令队列积压
最易被忽略的一点:SCAN 不保证原子性,过程中 key 可能被新增或删除。如果你需要强一致性快照,就得接受“最终一致性”——要么加分布式锁临时冻结写入,要么接受漏扫/多扫,没有银弹。










