对一个变化的 Set 使用 SSCAN,元素被扫描的情况:
扫描开始前添加的元素:一定会被扫描到,但可能会重复出现。
扫描开始后添加的元素:可能会被扫描到,也可能会被遗漏;一旦被扫描到,也有可能出现重复
因为 SSCAN 是无锁、增量的,Redis 不保证一致性,只保证最终尽量扫描全量。
官方建议总是使用代码层过滤重复项,例如用 Set<String> seen = new HashSet<>()
来去重。
1. 扫描开始前添加的元素:
一定会被扫描到 ,但可能会重复出现
原因:
- 这些元素已经存在于集合中,Redis 会从哈希桶 0 开始遍历所有元素;
- 如果中间发生
rehash
或元素移动,Redis 可能会回扫,导致部分元素重复被返回。
结论: 保证不漏; 不保证不重复
2. 扫描开始后添加的元素:
可能会被扫描到,也可能会被遗漏;一旦被扫描到,也有可能出现重复
原因:
- 如果新增元素正好落在 Redis 尚未扫描到的桶(slot) ,那么它就可能被扫到;
- 如果新增元素插入到已经扫描过的桶 ,Redis 不会回头扫描 → 被永久漏掉。
结论: 不保证能扫到(取决于插入时机和哈希槽位置) 扫到也可能重复(尤其发生重排/rehash)
为什么会这样?
Redis 的 SSCAN
是一种 渐进式游标遍历,它的工作机制决定了以下特性:
1. 游标遍历,不阻塞
SSCAN
不是一次性返回所有元素,而是通过一个游标cursor
分批返回;- 每次调用
SSCAN
返回一部分数据 + 下一轮的cursor
。
2. 遍历的是快照视图(基本稳定)
-
Redis 使用哈希表 + rehash 机制管理 Set 元素;
-
扫描视图的哈希表结构相对稳定(除非发生扩容);
-
所以,在SSCAN 遍历期间:
- 已经存在的元素不会被漏掉(只要你扫描完整);
- 新增的元素可能被忽略(因为游标已经过对应桶);
- 被删除的元素可能仍被返回(因为已在缓冲区或桶中,还没同步清理)。
背景知识:Redis 的 SSCAN 如何实现?
Redis 中的集合(Set)底层结构是哈希表(dict)或整数集合。SSCAN
遍历 Set 本质上是:
- 每次调用扫描哈希表的一部分(不是一个元素一个元素地遍历);
- 基于游标(cursor) 和rehash-safe 迭代器实现;
- 在迭代过程中,集合内容不能保证不变。
情况一:重复(返回过的元素再次返回)
具体情况一:扫描过程中删除元素导致游标错位
- SSCAN 使用游标记录上一次遍历的位置(某个哈希桶);
- 若中途有元素被删除或 rehash 后移,Redis 会重复回到之前的哈希槽;
- 结果:已返回的元素可能再次返回。
举个例子:
假设 Set
中有 a b c d e f
,初始结构分布在 3 个槽:
槽位 | 元素 |
---|---|
0 | a b |
1 | c d |
2 | e f |
你第一次调用:
css
SSCAN key 0 COUNT 2
# 返回 a, b;cursor=1
但这时,b
被删掉、e
被加进来,此时槽结构变化了。
你第二次调用:
shell
SSCAN key 1 COUNT 2
# 有可能又返回 a(因为 rehash 了) → 重复
情况二:漏扫(新增元素从未被返回)
具体情况一:新元素插入到已扫描过的槽
- 如果
SSCAN
已扫描了某些槽,新元素刚好插入到这些槽中; - 因为扫描是线性前进的,Redis 不会回头;
- 所以这些新元素永远不会被扫描到 ,导致漏扫。
举个例子:
还是上面那个结构:
- 你第一次调用
SSCAN key 0 COUNT 6
→ 扫完全部槽; - 然后你执行:
SADD key g
→ 假设 g 落到槽 1(已扫描过); - 你继续执行
SSCAN key 0
→ 迭代结束,g 永远没返回。
小结:重复和漏扫的根本原因
问题 | 触发原因 | 说明 |
---|---|---|
重复 | 删除元素 / rehash 导致游标回退 | 游标混乱,可能扫回已扫描区域 |
漏扫 | 元素新增在已扫描槽位 | Redis 不回扫,直接跳过新元素 |
避免建议(结合实际开发)
如果数据变动不可避免:
- 接受重复结果,客户端用 Set 去重;
- 适合日志采集、订阅推送等容错性强的场景。
如果必须精确遍历:
- 锁定集合期间禁止写操作(加锁、用快照集合);
- 或使用
SMEMBERS
全量获取(适合小集合)。
一个业务中真实示例
假设你在做直播间订阅推送:
vbnet
sub:liveRoom:001 → Set("u123", "u456", "u789", ..., "u999")
你定时用 SSCAN
扫出所有用户发消息。
风险:
- 有新用户进入时:他插入到了已扫描的槽 → 你永远漏掉这个人;
- 用户断开后被移除,rehash 后 Redis 扫回原位置 → 某些用户被"重复发消息";
最终建议(实战推荐)
方案 | 能力 | 场景 |
---|---|---|
SSCAN + 客户端 Set 去重 | 防重复,但不能防扫描事插入的漏扫 | 推送、缓存清理 |
临时复制 Set + SSCAN | 防漏 + 防重复 | 中等数据量、低频任务 |