Set:去重、标签与随机抽取
Set 是无序、去重的字符串集合。它的两大杀手锏:O(1) 判断"存在不存在",以及服务端直接完成交集、并集、差集运算——共同好友、共同关注这类需求一条命令搞定。
1. 核心命令实操
1.1 增删查
SADD tags:article:1 "redis" "cache" "database"
# 已存在,自动去重,返回实际新增数 0
SADD tags:article:1 "redis"
SISMEMBER tags:article:1 "redis"
# 6.2+ 批量判断
SMISMEMBER tags:article:1 "redis" "mysql"
# 集合大小
SCARD tags:article:1
SREM tags:article:1 "database"
SMEMBERS tags:article:1SMEMBERS 返回全部成员,大集合上等同于 KEYS 的危险性,改用 SSCAN:
127.0.0.1:6379> SSCAN tags:article:1 0 COUNT 100
1) "0"
2) 1) "cache"
2) "redis"1.2 随机抽取:SPOP 与 SRANDMEMBER
127.0.0.1:6379> SADD lottery:users u1 u2 u3 u4 u5
(integer) 5
127.0.0.1:6379> SRANDMEMBER lottery:users 2
1) "u3"
2) "u1" # 只随机读取,不删除
127.0.0.1:6379> SPOP lottery:users 2
1) "u5"
2) "u2" # 随机弹出并删除
127.0.0.1:6379> SCARD lottery:users
(integer) 3两者的区别决定了抽奖用法:
- 一等奖不能重复中:用
SPOP,中奖者移出奖池; - 每轮独立、可重复参与:用
SRANDMEMBER。
SRANDMEMBER 的 count 为负数时允许返回重复成员(如 -5 可能返回同一人多次)。
1.3 集合运算:交、并、差
SADD follow:ada u2 u3 u4
SADD follow:bob u3 u4 u5
# 交集:共同关注
SINTER follow:ada follow:bob
# 并集
SUNION follow:ada follow:bob
# 差集:ada 关注而 bob 没关注的
SDIFF follow:ada follow:bob
# STORE 版本把结果存成新 key,便于缓存复用
SINTERSTORE common:ada:bob follow:ada follow:bob
EXPIRE common:ada:bob 300
SMEMBERS common:ada:bob
# 7.0+ 只要交集数量,不要成员,更省
SINTERCARD 2 follow:ada follow:bob带 STORE 后缀的版本把结果存进新 key,配合 EXPIRE 就是一个"交集结果缓存 5 分钟"的实现;SINTERCARD(7.0+)只返回交集大小,省掉传输成员的开销。
复杂度注意:SINTER 是 O(N*M)(最小集合大小 × 集合个数),SUNION/SDIFF 是 O(N)(全部元素之和)。对两个百万级集合求并集是重操作,必要时放到从库或离线做。
2. 底层编码:intset 与 listpack 与 hashtable
SADD nums 1 2 3
OBJECT ENCODING nums
SADD strs "a" "b"
OBJECT ENCODING strs
SADD strs "this-is-a-long-member-over-sixty-four-bytes-xxxxxxxxxxxxxxxxxxxxxxx"
OBJECT ENCODING strs| 编码 | 条件 | 结构 |
|---|---|---|
intset | 全部成员是整数,且个数 ≤ 512 | 有序整数数组,二分查找 |
listpack | 成员含字符串,个数 ≤ 128 且都 ≤ 64 字节(7.2+) | 紧凑连续内存 |
hashtable | 超出上述条件 | 哈希表,value 为空 |
intset(升级机制):
[1, 2, 3] 全是 int16 -> 每个成员 2 字节
SADD nums 100000 需要 int32 -> 整个数组升级为 4 字节/成员
SADD nums 10^12 需要 int64 -> 再升级为 8 字节/成员
(只升不降)相关配置:
set-max-intset-entries 512
set-max-listpack-entries 128
set-max-listpack-value 64存纯数字 ID 的集合(用户 ID、商品 ID)时 intset 极其省内存:512 个 int32 只占约 2 KB。
3. 典型场景
3.1 标签系统
# 双向存储: 文章的标签 + 标签下的文章
127.0.0.1:6379> SADD article:1:tags "redis" "cache"
(integer) 2
127.0.0.1:6379> SADD tag:redis:articles 1 5 9
(integer) 3
127.0.0.1:6379> SADD tag:cache:articles 1 7
(integer) 2
# 同时含 redis 和 cache 标签的文章:
127.0.0.1:6379> SINTER tag:redis:articles tag:cache:articles
1) "1"3.2 抽奖去重
# 报名: SADD 天然防重复报名
127.0.0.1:6379> SADD raffle:20260801 u1001
(integer) 1
127.0.0.1:6379> SADD raffle:20260801 u1001
(integer) 0 # 重复报名无效
# 开奖: 抽 3 个三等奖(不放回)
127.0.0.1:6379> SPOP raffle:20260801 3
1) "u1017"
2) "u1001"
3) "u1093"3.3 点赞与已读去重
# 返回 1 = 点赞成功
SADD like:post:42 u1
# 返回 0 = 已点过(可提示或执行取消逻辑)
SADD like:post:42 u1
SADD like:post:42 u2 u3
# 点赞数
SCARD like:post:42
SISMEMBER like:post:42 u9💡用返回值代替先查后写
判断"是否已点赞"不需要先 SISMEMBER 再 SADD——直接 SADD,看返回值是 1 还是 0。一次网络往返,而且原子,不存在并发下的先查后写竞态。
⚠️大集合的三个雷区
1)SMEMBERS 百万成员集合会阻塞并产生巨大响应,遍历用 SSCAN;2)SINTERSTORE 大集合会在主线程做大量计算;3)海量 UV 去重场景(千万级)用 Set 太耗内存——每天几千万 ID 可能吃掉数 GB,这种"只要数量不要成员"的需求应该用 HyperLogLog(第 8 章),12 KB 搞定。
小结
- Set 提供 O(1) 存在性判断与自动去重,SADD 返回值可直接当"是否首次"用
- SPOP 不放回抽取(抽奖),SRANDMEMBER 只读随机(可重复)
- SINTER/SUNION/SDIFF 服务端集合运算,STORE 版本可缓存结果
- 编码三态:纯整数 intset,小字符串集合 listpack,大集合 hashtable
- 需要"带权重的有序集合"时,进入下一章 ZSet →
🎯练习
- 构造两个各含 10 个成员的关注集合,分别求共同关注、全部关注、独有关注,并用 SINTERSTORE 把结果缓存 60 秒。
- 实现一个抽奖流程:100 人报名(防重复),先抽 1 个一等奖(不放回),再从剩余人中抽 5 个参与奖。
- 向一个纯数字 Set 依次加入 500 个小整数和 1 个字符串,观察 OBJECT ENCODING 的两次状态。