对一个变化的 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 防漏 + 防重复 中等数据量、低频任务
相关推荐
程序员Sunday2 小时前
Redis 缓存穿透、击穿、雪崩怎么区分?从回源压力找原因
数据库·redis·缓存
guo_wen_qiang2 小时前
redis实现秒杀功能-超卖&库存遗留问题的解决
数据库·redis·缓存
逐流人2 小时前
Redis内存数据库:从数据类型到集群部署
linux·运维·数据库·redis·缓存·云计算
.Bryce.4 小时前
三协议合一实战:用MCP+A2A+ANP搭建一个智能研究助手
网络·redis·agent·codex·mcp·a2a·anp
大侠归来5 小时前
Redis Cluster 的 16384 个槽(Slot)是什么?
数据库·redis·缓存
杨运交5 小时前
[079][消息模块]基于 Redis Stream 的高可靠消息队列实现——Spring Data Redis 实战解析
数据库·redis·spring
xixingzhe26 小时前
秒杀库存三种缓存实现方案
数据库·redis·缓存
思考着亮7 小时前
1.Redis 基础知识
redis
牛战士coding7 小时前
Java 对象写入 Redis 后类型变了?Jackson 多态序列化问题复盘
java·开发语言·redis
xiaoqiMikko8 小时前
Redis 实战(基础篇):把一个老 RedisUtil 搬到 Spring Boot 4,一路踩出来的坑(真机实测)
java·spring boot·redis