Set 交并差与随机取样

举报
yd_227606267 发表于 2026/11/06 00:34:17 2026/11/06
【摘要】 用 Set 的交并差与随机取样命令实现共同好友、抽奖与标签聚合

Set 的价值有两条:成员天然唯一,以及服务端原生的交并差集合运算。共同关注、可能认识的人、标签聚合、无放回抽奖这些功能,用 Set 一两条命令就能完成,但大集合上的交并差是 O(N) 操作,用不好会直接阻塞主线程。本文以 Redis 7 为基准讲清每个命令的行为边界。

一、成员操作与判存

SADD 向集合添加成员,返回实际新增的数量,重复成员被忽略:

127.0.0.1:6379> SADD tags:1001 java redis
(integer) 2
127.0.0.1:6379> SADD tags:1001 java
(integer) 0

SISMEMBER 判断单个成员存在性,O(1);SMISMEMBER(Redis 6.2+)一次判断多个成员,结果按顺序返回 0/1。SREM 删除成员并返回删除数;SCARD 返回基数,O(1) 直接读元数据;SMOVE 把成员从一个集合原子地移到另一个集合,跨集合的"转移"不需要先删后加。SMEMBERS 返回全部成员,O(N),大集合上慎用,遍历一律换 SSCAN。

SSCAN 与 Hash 的 HSCAN 同款语义:游标为 0 表示结束,COUNT 只是提示,返回元素可能重复、需调用方去重。它保证不阻塞主线程,是线上巡检集合内容的标准姿势。顺带一提,Set 的成员唯一但无序,需要按插入顺序或分数顺序时分别换 List 与 ZSet,不要用 Set 硬凑顺序。

二、交并差:即时返回还是写回

三类运算各有即时版与 STORE 版:

| 运算 | 即时版 | 写回版 | 典型场景 |

| --- | --- | --- | --- |

| 交集 | SINTER | SINTERSTORE | 共同关注、共同标签 |

| 并集 | SUNION | SUNIONSTORE | 合并标签、合并权限 |

| 差集 | SDIFF | SDIFFSTORE | 可能认识的人、差量同步 |

三个要点。第一,SDIFF 有方向性,SDIFF a b 返回在 a 不在 b 的成员,参数顺序写反结果完全不同。第二,SINTER 的成本与最小的那个集合规模成正比,Redis 会自动从最小集合开始探测,但结果集很大时响应体与网络传输仍是负担,把结果写回再用 SSCAN 分批读更稳。第三,"是否存在共同关注"这类布尔判断不需要拉全量交集,用 SINTERCARD(Redis 7.0)只返回交集个数,并用 LIMIT 提前截断:

127.0.0.1:6379> SINTERCARD 2 follow:1001 follow:1002 LIMIT 10
(integer) 3

只要判断"交集是否非空",LIMIT 传 1 即可,成本被牢牢钉住。

共同关注这类高频展示的结果适合用 SINTERSTORE 预计算:SINTERSTORE common:1001:1002 follow:1001 follow:1002,结果 key 设几分钟 TTL,缓存期内直接 SMEMBERS 或 SCARD 读取,失效再算。写回加过期把 O(N) 计算从请求路径挪到了低频路径,是集合运算上线前的标准改造。再举个差集的实例:给用户 1001 推荐"好友的好友",对每个好友 f 执行 SDIFF follow:f follow:1001 得到 f 关注而 1001 未关注的候选,应用层按出现次数排序取头部,整套操作全部用 pipeline 打包,RTT 只有一次。Cluster 下多 key 运算要求所有 key 同槽,key 设计要带 hash 标签。

三、随机取样与抽奖

SRANDMEMBER 与 SPOP 都能随机取成员,区别在是否删除,以及 count 的正负语义:

  • SRANDMEMBER key count:count 为正返回最多 count 个不重复成员(超过集合基数则返回全部),为负返回 |count| 个允许重复的成员,成员保留在集合中。
  • SPOP key count:随机弹出并删除,适合"抽中即走"的无放回抽奖。
127.0.0.1:6379> SRANDMEMBER lottery:pool 3
1) "u:88"
2) "u:12"
3) "u:203"
127.0.0.1:6379> SPOP lottery:pool 1
"u:12"

展示"今日幸运用户"但不消耗奖池,用 SRANDMEMBER 正数;一人一奖的抽签,用 SPOP;需要"重复展示且可能重复"的轮播,可以用负数 count。注意 SPOP 弹出的成员不可恢复,删除性操作前确认业务语义。

并发一致性上,两者都是单命令原子操作,万人同时抽不会发出重复的奖,但中奖结果要立刻 SADD 到 lottery:winners 之类的集合留底,抽奖与留底两条命令之间进程崩溃会漏记,要求严格时用 Lua 把 SPOP 与 SADD 打包执行。奖池本身也要有界:活动结束 SPOP 清空或整 key 设 TTL,别让活动集合变成常驻大 key。

四、去重、标签与小整数集合

Set 还擅长"第一次"类判断:报警去重(SADD 返回 0 说明今天已经报过)、接口防重放(把 nonce 放进带 TTL 的 Set)。标签体系双向建模:文章带标签用 SADD art:tags:1001 java redis,标签下的文章列表用 art:tag:java,多标签求并集取文章列表用 SUNION,"同时包含 java 与 redis"的筛选用 SINTER。两个方向的一致性靠同一事务链路双写:文章打标时同时 SADD 到文章维度与标签维度,漏写一边就会出现"标签页有文章、文章页点不进去"的脏数据。

一个容易忽略的底层细节:成员全部是整数且个数不超过 set-max-intset-entries(默认 512)时,Set 编码为 intset,有序紧凑存储,判存是二分查找。利用这一点,小的纯整数 ID 集合(如权限点位、白名单)可以用 Set 存得非常省,但只要混入一个非整数成员就会退出 intset 编码。

多层关系也能用 Set 拼:二度人脉推荐时,先取我的关注列表,再对每个好友用 SISMEMBER 判断"对方是否关注了候选推荐人",全部命令经 pipeline 一次打包发送,网络往返只有一次。同时要给单个集合设规模上限——关注列表上限 5000 这类产品约束,本质上也是在保护 SINTER/SDIFF 的计算成本不失控。

常见坑 / 最佳实践

  1. 大集合禁用 SMEMBERS 与大结果集的 SINTER/SUNION/SDIFF。 这些都是 O(N) 即时计算,会阻塞主线程;改用 STORE 版写回加 SSCAN 分批读,或用 SINTERCARD 只要计数。
  2. SRANDMEMBER 负数 count 允许重复。 抽奖去重逻辑如果按"返回个数等于人数"校验会失真,需要不重复结果时一律用正数。
  3. SPOP 是删除性操作,不可恢复。 需要"只展示不消耗"时用 SRANDMEMBER,两者不要混用。
  4. SDIFF 的方向别写反。 上线前用小数据集验证参数顺序,差集方向错误往往测不出来,到线上才暴露成数据错误。
  5. Cluster 下集合运算要求同槽。 多个 key 带 hash 标签设计,否则 CROSSSLOT 报错;单机版不报错但拆到 Cluster 后会炸,key 设计要前置。

小结

  • Set 的成员判存是 O(1),SADD 返回新增数,SMISMEMBER 支持批量判存。
  • 交并差优先考虑 STORE 版与 SINTERCARD(Redis 7.0),把 O(N) 计算与大响应挡在服务端可控范围内。
  • SRANDMEMBER 取样不删除、正数去重负数可重复,SPOP 取样即删除,抽奖场景按业务语义选择。
  • 纯整数小集合享受 intset 编码的紧凑与二分判存,混入字符串成员即失效。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。