Redis SCAN 为什么会返回重复键
来源:17golang原创
时间:2026-09-06 01:15:58 285浏览 收藏
Redis SCAN 返回重复键并不一定是 Redis 出错。它是游标式、弱状态的渐进遍历:一次调用只带回一小批结果,服务端只记住游标,不为客户端保存“已经见过哪些键”的集合。因此,同一个键在一次完整遍历中可能出现多次。真正需要修复的通常不是“让 SCAN 绝不重复”,而是让消费端能安全地重复处理。
只要从游标0开始,并持续使用上一轮返回的游标直到再次得到0,才能算完成一次完整遍历;业务侧必须接受重复、空批次和遍历期间数据变化。
- 重复返回是 SCAN 的已知语义,不等同于循环代码重复调用。
COUNT只影响每次工作的量,不是分页大小,也不能消除重复。- 删除、迁移、刷新 TTL 等副作用要做成幂等;需要严格一次时,把任务状态放到独立集合或表中。
先确认是重复返回,还是重复处理
排查时先给每轮响应记录 cursor、返回数量和键名,不要只看最终日志里的“处理次数”。下面的命令可以观察重复键,但它本身没有修改 Redis 数据:
SCAN 返回重复键的核心原因是 Redis 采用了增量迭代的设计,全程不做全表阻塞遍历,服务端不会主动过滤已经返回过的键,再配合哈希槽的扩容缩容、遍历中途发生键值变更等场景,就很容易出现重复输出的情况。这类特性是 Redis 官方明确说明的,业务侧自己做去重就能正常使用,不用额外修改服务端逻辑。
redis-cli SCAN 0 MATCH 'cache:user:*' COUNT 100
# 中文说明:从 0 开始,只筛选用户缓存键;COUNT 100 只是每轮工作量提示。
如果同一个键在不同游标批次出现,属于 SCAN 允许的重复返回;如果响应没有重复、业务表却重复写入,问题在消费端。另一个常见误判是把空数组当成结束:只要游标还不是 0,空批次也必须继续请求。

重复从哪里来:游标不是快照位置
SCAN 的游标描述的是服务端下一段遍历位置,不是固定页码,也不是某个键的唯一编号。底层字典发生扩容或缩容时,桶位置会重新分布;渐进式遍历跨越这些变化,就可能再次触达已经返回过的键。即使没有明显扩缩容,也不能把返回顺序当成稳定排序。
MATCH 是取出元素后的过滤条件,匹配很稀疏时,很多轮可能返回空结果;COUNT 是提示,服务端可以返回少于、多于甚至暂时为零的元素。小集合使用紧凑编码时,还可能一次返回全部元素。以上都不会改变“用游标直到 0”的结束条件。
| 现象 | 正确判断 | 处理方式 |
|---|---|---|
| 同一键出现两次 | 允许发生 | 消费端去重或保证幂等 |
| 某轮返回空数组 | 不代表结束 | 继续使用返回游标 |
| 改大 COUNT 后重复减少 | 只是批次变化 | 不能据此依赖唯一返回 |
| 遍历中新增或删除键 | 结果存在边界不确定性 | 限定窗口或接受最终一致 |
用游标闭环,并把副作用做成幂等
以 go-redis 为例,循环的关键是把本次返回的 nextCursor 原样交给下一次调用。示例用本地集合记录已处理键,避免一次任务中的重复副作用;集合过大时,应换成带任务编号和过期时间的外部存储。
var cursor uint64
seen := make(map[string]struct{})
for {
keys, nextCursor, err := rdb.Scan(ctx, cursor, "cache:user:*", 100).Result()
if err != nil {
return fmt.Errorf("扫描 Redis keyspace 失败: %w", err) // 中文说明:保留原错误,便于重试和告警。
}
for _, key := range keys {
if _, exists := seen[key]; exists {
continue // 中文说明:同一轮任务中重复出现时,不重复执行副作用。
}
seen[key] = struct{}{}
if err := refreshOne(ctx, key); err != nil {
return fmt.Errorf("刷新键 %q 失败: %w", key, err) // 中文说明:失败立即停止,避免静默漏处理。
}
}
cursor = nextCursor
if cursor == 0 {
break // 中文说明:只有服务端返回 0,完整遍历才结束。
}
}
如果任务是删除过期缓存,重复执行 UNLINK key 通常比“先查再删”更容易做成幂等;如果是写数据库,就用业务唯一键、任务批次号或 upsert 约束重复提交。严格的一次性语义不能由 SCAN 单独提供。

线上排查要看哪些边界
生产任务建议同时记录任务 ID、起始时间、最后游标、扫描总数、唯一键数、重复数、空批次数和失败键。这样可以区分“Redis 返回重复”和“客户端重试整批”两类问题。若 keyspace 持续增长,完整遍历可能迟迟回不到 0;可以限定维护窗口、冻结写入,或接受只覆盖某个时间段的最终一致结果。
Redis Cluster 下,逻辑上的全量扫描还意味着分别遍历各个主节点,应用层需要为节点维护独立游标和去重边界;不要把一个节点的游标直接交给另一个节点。若只是按前缀清理,优先缩小 MATCH 范围并控制并发,避免把“重复键”误判成需要不断增大 COUNT 的性能问题。
常见问题
把 COUNT 调大能彻底消除重复吗?
不能。它只是服务端每次调用的工作量提示,返回数量和重复语义都没有变。
返回空数组是不是已经扫描完了?
不是。只有返回游标为 0 才结束;空数组且游标非零时必须继续。
需要严格一次处理,应该依赖 SCAN 吗?
不应该。把唯一性放到集合、数据库唯一键或任务状态表中,并让副作用支持重试。
把 SCAN 当作“可能重复、可能空批、非快照”的遍历接口,消费端再负责幂等和任务记录,重复键就会从故障信号变成可管理的正常边界。
-
331 收藏
-
302 收藏
-
279 收藏
-
446 收藏
-
345 收藏
-
216 收藏
-
332 收藏
-
390 收藏
-
494 收藏
-
375 收藏
-
394 收藏
-
490 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习