对一个变化的 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 防漏 + 防重复 中等数据量、低频任务
相关推荐
陈阿土i2 小时前
SpringAI 1.0.0 正式版——利用Redis存储会话(ChatMemory)
java·redis·ai·springai
bing_1583 小时前
跨多个微服务使用 Redis 共享数据时,如何管理数据一致性?
redis·微服务·mybatis
多多*4 小时前
微服务网关SpringCloudGateway+SaToken鉴权
linux·开发语言·redis·python·sql·log4j·bootstrap
HAPPY酷5 小时前
Kafka 和Redis 在系统架构中的位置
redis·kafka·系统架构
gaoliheng0066 小时前
Redis看门狗机制
java·数据库·redis
潘yi.7 小时前
Redis哨兵模式
数据库·redis·缓存
瀚海澜生8 小时前
redis系列(1)——redis高效的本质:基础键值对的组织和基础数据结构
redis
努力学习的小廉8 小时前
我爱学算法之—— 前缀和(中)
开发语言·redis·算法
多多*9 小时前
基于rpc框架Dubbo实现的微服务转发实战
java·开发语言·前端·redis·职场和发展·蓝桥杯·safari
MuYiLuck13 小时前
【redis实战篇】第八天
数据库·redis·缓存