对一个变化的 Set 使用 SSCAN,元素被扫描的情况:

对一个变化的 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 不会回头;
  • 所以这些新元素永远不会被扫描到 ,导致漏扫。

举个例子:

还是上面那个结构:

  1. 你第一次调用 SSCAN key 0 COUNT 6 → 扫完全部槽;
  2. 然后你执行:SADD key g → 假设 g 落到槽 1(已扫描过);
  3. 你继续执行 SSCAN key 0 → 迭代结束,g 永远没返回。

小结:重复和漏扫的根本原因

问题 触发原因 说明
重复 删除元素 / rehash 导致游标回退 游标混乱,可能扫回已扫描区域
漏扫 元素新增在已扫描槽位 Redis 不回扫,直接跳过新元素

避免建议(结合实际开发)

如果数据变动不可避免:

  • 接受重复结果,客户端用 Set 去重;
  • 适合日志采集、订阅推送等容错性强的场景。

如果必须精确遍历:

  • 锁定集合期间禁止写操作(加锁、用快照集合);
  • 或使用 SMEMBERS 全量获取(适合小集合)。

一个业务中真实示例

假设你在做直播间订阅推送:

vbnet 复制代码
 sub:liveRoom:001 → Set("u123", "u456", "u789", ..., "u999")

你定时用 SSCAN 扫出所有用户发消息。

风险:

  • 有新用户进入时:他插入到了已扫描的槽 → 你永远漏掉这个人;
  • 用户断开后被移除,rehash 后 Redis 扫回原位置 → 某些用户被"重复发消息";

最终建议(实战推荐)

方案 能力 场景
SSCAN + 客户端 Set 去重 防重复,但不能防扫描事插入的漏扫 推送、缓存清理
临时复制 Set + SSCAN 防漏 + 防重复 中等数据量、低频任务
相关推荐
IT大白鼠4 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz4 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙4 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除4 天前
redis--018
redis
两锭子4 天前
Redis基础
redis
烟沙九洲4 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索4 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞4 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路5 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog5 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis