当前位置: 首页 > news >正文

拒绝全表扫描灾难:用 SSCAN 安全遍历 Redis 亿级 Set 集合


案发场景:
你的系统里有一个全网黑名单集合security:blacklisted_users,里面躺着 500 万个违规用户 ID。
某天,安全部门要求把这份名单全量导出来做离线分析。
你的新人同事大手一挥,在 Java 里写下了一行:Set<String> allUsers = redis.opsForSet().members("security:blacklisted_users");
毁灭瞬间:
代码上线的瞬间,Redis 实例 CPU 飙升至 100%。500 万个字符串被一次性打包塞进网卡,Redis 丧失响应长达 8 秒。
8 秒钟,足够让你们公司的核心交易链路触发雪崩报警,网关大面积 502。
破局之道:
面对大 Key 的遍历,永远、绝对不要使用SMEMBERS**
请祭出游标遍历神器:
SSCAN**。
每次只拿 1000 条,处理完了再拿下一批,把一次性 O(N) 的核爆打击,化解为无数次 O(1) 的蒙毛毛雨。


1. 核心解剖:SMEMBERS错在哪了?

Redis 是单线程处理命令的。
SMEMBERS的底层逻辑极其简单粗暴:遍历底层数据结构(哈希表或整数集合),把所有元素塞进回复缓冲区,一次性发给客户端。

致命三连击:

  1. CPU 独占:遍历 500 万数据需要耗费大量的 CPU 周期,期间 Redis 无法处理任何GETSET请求。
  2. 网卡打满:几百 MB 的数据瞬间涌向网卡,极易造成网络拥塞。
  3. 客户端 OOM:你的 Java 服务突然接收到一个包含 500 万个 String 的巨型 List,极易触发 JVM 老年代报警甚至 OutOfMemoryError。

2. 救世主SSCAN:游标 (Cursor) 的艺术

SSCAN的核心思想是分治法。它不是一次性给你所有数据,而是通过一个游标 (Cursor),像翻书一样,一页一页地读取数据。

基础语法:

SSCAN key cursor[MATCH pattern][COUNT count]

工作流:

  1. 第一步:客户端发起SSCAN my_set 0(游标从 0 开始)。
  2. 第二步:Redis 返回两样东西:下一个游标的值(比如14),以及一小批数据(默认 10 条左右)。
  3. 第三步:客户端处理完这批数据后,拿着新游标继续请求:SSCAN my_set 14
  4. 终点:直到 Redis 返回的新游标为0,代表整个集合已经完完整整遍历了一遍。

3. 深水区:SSCAN的三大底层“潜规则”

很多开发者以为SSCAN就像 SQL 里的LIMIT offset, size,其实完全不同!不了解底层的潜规则,必定踩坑。

潜规则一:COUNT 只是个“建议”,不是绝对限制

如果你写了COUNT 1000,Redis 并不保证一定返回 1000 条数据。它可能返回 900 条,也可能返回 1200 条,甚至可能返回空数组(但游标不为 0)

  • 原因:Redis 是基于底层 Hash 表的“槽位(Bucket)”进行遍历的。如果某个槽位碰巧有一长串哈希冲突的链表,Redis 会把这条链表上的所有元素一次性全给你。
  • 极端情况:如果你的 Set 底层使用的是IntSet(整数集合,内存极度紧凑),Redis 会无视 COUNT 参数,在第一次扫描时就把全量数据一次性返回!
潜规则二:逆序高位进位算法 (高深魔法)

在遍历过程中,如果有其他线程在疯狂对这个 Set 进行增删,甚至导致了底层的 Hash 表扩容(Rehash)。普通的数组下标遍历必定会漏掉数据或者大量重复。
Redis 采用了极其天才的反向二进制迭代算法。它保证了即使在扩容期间:

  • 绝对不会漏掉在遍历开始前就已经存在的元素。
  • 但是,可能会返回重复的元素
潜规则三:客户端必须自己去重

基于上一条,SSCAN明确在官方文档中指出:可能会返回重复元素
因此,客户端在接收到数据后,如果业务对重复敏感,必须在本地内存中(或者业务逻辑上)做好幂等去重。


4. 代码落地:Spring Boot 实战演练

在 Java 中,StringRedisTemplate为我们封装了极度优雅的迭代器(Iterator)模式。你根本不需要自己去维护那个抽象的游标数字。

场景:全量导出并清洗千万级黑名单
importorg.springframework.data.redis.core.Cursor;importorg.springframework.data.redis.core.ScanOptions;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjava.io.IOException;@ServicepublicclassBlacklistService{privatefinalStringRedisTemplateredisTemplate;publicBlacklistService(StringRedisTemplateredisTemplate){this.redisTemplate=redisTemplate;}/** * 安全地遍历超大 Set */publicvoidexportBlacklistSafely(StringsetKey){// 配置扫描选项:每次建议捞取 1000 条,匹配所有元素ScanOptionsoptions=ScanOptions.scanOptions().count(1000).match("*").build();// 这里的 scan 方法底层已经处理了游标循环,返回的是一个游标迭代器try(Cursor<String>cursor=redisTemplate.opsForSet().scan(setKey,options)){intprocessCount=0;while(cursor.hasNext()){StringuserId=cursor.next();// 自动带着新游标去 Redis 拉取下一批// --- 业务处理区 ---// 注意:这里需要考虑 SSCAN 可能返回重复元素的特性,如果是写文件或发 MQ,需做好幂等processUser(userId);processCount++;if(processCount%5000==0){System.out.println("已安全处理 "+processCount+" 条数据...");}}}catch(IOExceptione){thrownewRuntimeException("游标关闭异常",e);}}privatevoidprocessUser(StringuserId){// 比如将数据写入本地文件、推送到离线数仓等}}

避坑细节:Cursor实现了Closeable接口。虽然 Redis 的游标是无状态的(服务器不保存游标状态),但在 Spring 底层,它可能持有连接资源。务必使用try-with-resources确保cursor.close()被调用,防止连接泄漏!


5. 三大高频实战场景

场景一:无感知的热数据迁移
  • 业务:需要把一个几 GB 的 Set 从老集群迁移到新集群。
  • 实战:绝对不能SMEMBERS。写一个常驻脚本,一边用SSCAN慢慢扫描老 Key,一边分批SADD到新 Key。既不影响老集群的在线业务,又能平滑完成迁移。
场景二:僵尸粉/过期数据的后台静默清理
  • 业务:找出集合中已经注销的账号,并把它们踢出集合。
  • 实战:开一个定时任务,用SSCAN每秒扫 500 个用户,拿到业务库里对比。如果是僵尸粉,就对这个 Key 执行SREM删除。细水长流,完全不占用 Redis 宝贵的峰值性能。
场景三:配合 MATCH 实现模糊匹配过滤
  • 业务:找出一个包含千万个订单号的 Set 中,所有以REFUND_开头的退款单号。
  • 实战:使用SSCAN order_set 0 MATCH REFUND_* COUNT 2000。让 Redis 在底层帮你做初步过滤,极大减少网络传输的数据量。

总结

在架构的演进道路上,敬畏生产环境是第一准则。

SMEMBERS就像是拿着大网去海里捞鱼,很爽,但网太大容易把船拖翻。
SSCAN则是一根精致的鱼竿,虽然需要一竿一竿地钓,但它保证了你的航船永远平稳前行。

下次再看到代码里出现SMEMBERSHGETALLKEYS *,请毫不犹豫地给它打回重做,换成SSCANHSCANSCAN

http://www.jsqmd.com/news/452758/

相关文章:

  • 2603,禁止微软更新工具
  • 2603C++,简单实现协程
  • 如何快速搭建简单SpringBoot项目网页
  • 如何使用 Python 连接 MySQL 数据库?
  • 如何在docker中的mysql容器内执行命令与执行SQL文件
  • Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案
  • 基于Java+SSM+Django大学生成果登记系统(源码+LW+调试文档+讲解等)/大学生成果登记系统使用教程/大学生成果登记平台/大学生成果管理系统/大学生成果申报系统/大学生成果展示系统
  • 基于Java+SSM+Django健身中心管理系统(源码+LW+调试文档+讲解等)/健身中心管理软件/健身房管理系统/健身中心会员管理系统/健身房会员软件/健身房管理软件/健身俱乐部管理系统
  • Flutter 组件 t_stats 的适配 鸿蒙Harmony 实战 - 驾驭高性能统计学运算、实现鸿蒙端海量数据实时态势感知与工业级描述性统计方案
  • 在SpringBoot项目中集成MongoDB
  • 地址转坐标:利用高德API进行批量地理编码
  • 基于Java+SSM+Flask网页商城系统(源码+LW+调试文档+讲解等)/网页商城系统使用教程/网页商城系统开发/网页商城系统模板/网页商城系统源码/网页商城系统搭建/网页商城系统优势
  • Flutter 组件 http_retry 的适配 鸿蒙Harmony 深度进阶 - 驾驭分布式负载感知重试、实现鸿蒙端高可靠通讯与协议幂等性审计方案
  • Flutter 组件 mock_client 的适配 鸿蒙Harmony 实战 - 驾驭 HTTP 协议级测试模拟、实现鸿蒙端离线环境下的接口断言与质量门禁方案
  • 某外包全员降薪,AI编程概念还没落地,外包程序员先背了锅,说句扎心的,这只是开始
  • Flutter 组件 shared_aws_api 的适配 鸿蒙Harmony 实战 - 驾驭跨平台 AWS 云服务通讯、实现鸿蒙端签名版本 4 (SigV4) 自动审计与高性能 API 鉴权方案
  • Flutter 组件 simplify 的适配 鸿蒙Harmony 实战 - 驾驭路径精简算法、实现鸿蒙端高性能地理足迹渲染与矢量图形优化方案
  • Flutter 组件 lcov_parser 的适配 鸿蒙Harmony 实战 - 驾驭 0307 批次代码质量审计、实现鸿蒙端测试覆盖率分析与自动化治理看板方案
  • Edge浏览器STATUS_ACCESS_DENIED错误终极指南:从原因分析到一键修复
  • ChatGPT指令实战指南:从基础到高级应用的最佳实践
  • 如何安全留存社交记忆?GetQzonehistory全攻略
  • 内容访问技术解析:开源工具辅助资源获取系统指南
  • x64dbg调试器实战:从零开始分析32位程序的完整流程(附常见问题解答)
  • 4步掌握开源内容解锁工具:突破付费访问限制的完整方案
  • 数字记忆备份完整指南:3个维度构建个人数据安全防线
  • 网页内容访问优化指南:合法获取付费内容的技术策略
  • 3步搞定音乐获取:让Spotify音乐实现永久离线自由
  • AI 辅助开发实战:基于 Spring Boot + Vue 的毕业设计高效构建与参考文献整合指南
  • PADS差分对设置避坑指南:为什么你的高速信号总是不稳定?
  • 【多目全景】基于海思3403平台的实时4路视频拼接与畸变校正技术解析