Redis Set(集合)
本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。
1. 定位:解决什么问题
Set 是没有顺序保证的唯一字符串成员集合,适合好友、黑名单、标签和精确去重。需要先后顺序用 List,需要分数或排名用 ZSet,只需要近似人数且不查询成员时可选 HyperLogLog。
基本操作示例
SADD learn:types:tags redis cache redis
SCARD learn:types:tags
SISMEMBER learn:types:tags redis预期成员数为 2,成员判断为 1。需要排名时不能依赖 SMEMBERS 的顺序;大集合的全量读取和集合运算应评估成本。
参考:Set。
游戏与后端场景
| 场景 | 建模与操作 | 需要补齐的业务规则 |
|---|---|---|
| 好友与黑名单 | SADD friends:1001 1002,共同好友用 SINTER | 双向好友变更需要协调两边,Cluster 多键操作需同槽 |
| 标签、功能名单 | SISMEMBER feature:beta uid | 明确名单更新传播和缓存有效期 |
| 一次性兑换码 | SREM codes:unused X 返回 1 才取得该码 | 移除成功后发奖失败会丢失领取机会,需持久化兑换记录与补偿 |
| 精确去重人数 | 用 uid 作成员,SCARD 计数 | 内存随人数增长;只需近似数量可选 HyperLogLog |
2. 命令速查与示例
按用途分组,共 17 项,覆盖 Redis 8.2.10 的本类型命令及子命令。点击命令名查看官方参数。示例在 redis-cli 中逐行输入,每次换例子前重置下面的教学数据。
准备与重置示例数据
在独立实验实例的 redis-cli 交互模式逐行输入,避免 shell 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。
UNLINK learn:{set}:a learn:{set}:b learn:{set}:out
SADD learn:{set}:a a b c
SADD learn:{set}:b b c d示例的返回说明以刚完成准备段为前提,随机结果与剩余 TTL 不固定。带同一 {...} 的教学键便于在 Cluster 中满足相关多键操作的同槽要求。
成员操作
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| SADD 加入唯一成员 | SADD learn:{set}:a c dc 已存在,新增 d,返回 1。 | O(K) |
| SREM 移除指定成员 | SREM learn:{set}:a b missing仅 b 被删除,返回 1。 | O(K) |
| SISMEMBER 检查单成员 | SISMEMBER learn:{set}:a b返回 1。 | O(1) |
| SMISMEMBER 检查多个成员 | SMISMEMBER learn:{set}:a b z返回 1、0。 | O(K) |
| SCARD 集合大小 | SCARD learn:{set}:a返回 3。 | O(1) |
| SMEMBERS 读取全部成员 | SMEMBERS learn:{set}:a返回 a、b、c,顺序不保证。 | O(N) |
| SSCAN 游标遍历 | SSCAN learn:{set}:a 0 COUNT 10用返回游标继续到 0,可能出现重复。 | 单次 O(1) 完整 O(N) |
| SRANDMEMBER 随机读取 | SRANDMEMBER learn:{set}:a 2读取两个不同成员,不删除。 | 单个 O(1) 批量 O(M) |
| SPOP 随机弹出 | SPOP learn:{set}:a 1随机移除并返回一个成员。 | 单个 O(1) 批量 O(M) |
| SMOVE 跨集合移动成员 | SMOVE learn:{set}:a learn:{set}:b a返回 1;a 从源集合移到目标集合。 | O(1) |
集合运算
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| SINTER 交集 | SINTER learn:{set}:a learn:{set}:b返回 b、c。 | 最坏 O(Nmin×K) |
| SINTERCARD 只统计交集数量 | SINTERCARD 2 learn:{set}:a learn:{set}:b返回 2;LIMIT 可在达到阈值后提前结束。 | 最坏 O(Nmin×K) |
| SINTERSTORE 保存交集 | SINTERSTORE learn:{set}:out learn:{set}:a learn:{set}:b覆盖 out,保存 b、c 并返回 2。 | 最坏 O(Nmin×K) |
| SUNION 并集 | SUNION learn:{set}:a learn:{set}:b返回 a、b、c、d,顺序不保证。 | O(T) |
| SUNIONSTORE 保存并集 | SUNIONSTORE learn:{set}:out learn:{set}:a learn:{set}:b覆盖 out,返回成员数 4。 | O(T) |
| SDIFF 差集 | SDIFF learn:{set}:a learn:{set}:b返回 a,操作有方向。 | O(T) |
| SDIFFSTORE 保存差集 | SDIFFSTORE learn:{set}:out learn:{set}:a learn:{set}:b覆盖 out,返回 1。 | O(T) |
版本与复杂度补充
下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。
| 命令 | 引入版本与状态 | 复杂度说明 |
|---|---|---|
| SADD | 1.0.0 | O(K),K 为本次指定元素数。 |
| SREM | 1.0.0 | O(K),K 为本次指定元素数。 |
| SISMEMBER | 1.0.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| SMISMEMBER | 6.2.0 | O(K),K 为本次指定元素数。 |
| SCARD | 1.0.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| SMEMBERS | 1.0.0 | O(N),N 为集合大小。 |
| SSCAN | 2.8.0 | 官方渐进遍历口径:单次 O(1),完整迭代 O(N);COUNT 只是提示,编码和结果数量影响耗时。 |
| SRANDMEMBER | 1.0.0 | 单元素 O(1);带 COUNT 时 O(M),M 为请求返回数。 |
| SPOP | 1.0.0 | 单元素 O(1);带 COUNT 时 O(M),M 为请求返回数。 |
| SMOVE | 1.0.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| SINTER | 1.0.0 | 最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。 |
| SINTERCARD | 7.0.0 | 最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。 |
| SINTERSTORE | 1.0.0 | 最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。 |
| SUNION | 1.0.0 | O(T),T 为所有输入集合的成员数之和。 |
| SUNIONSTORE | 1.0.0 | O(T),T 为所有输入集合的成员数之和。 |
| SDIFF | 1.0.0 | O(T),T 为所有输入集合的成员数之和。 |
| SDIFFSTORE | 1.0.0 | O(T),T 为所有输入集合的成员数之和。 |
通用键操作见键与过期时间,这里集中列出类型自身的命令。
3. 底层编码与内存
整数成员较少时可用 intset;8.2.10 的非整数小集合也可能用 listpack,大集合通常为 hashtable。成员内容、字节长度、数量都会影响选择和转换;不是“非整数必然 hashtable”。
默认 set-max-intset-entries 512;紧凑字符串集合的 set-max-listpack-entries 128、set-max-listpack-value 64。这是该版本的编码条件,不能靠改变阈值代替容量治理。
编码与内存怎么测
在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:
SADD learn:types:tags redis cache
OBJECT ENCODING learn:types:tags
MEMORY USAGE learn:types:tags固定键名、元素字节长度、Redis 版本和分配器,分别记录 1、10、100 个元素或不同值长度时的内存,再跨越编码阈值观察。单元素的增量开销可用相邻两次内存差估计,编码转换时不能套用线性平均;String/Bitmap 的横轴应使用字节数或最高位偏移。比较“一键多元素”和“多个独立键”时,把键名、对象、字典项和 TTL 管理开销一起计入。
MEMORY USAGE 默认对嵌套值采样,SAMPLES 0 会遍历所有嵌套值,仅在小型隔离实验中使用。其结果不等同于进程 RSS;大量键还会增加全局字典、分配器碎片和复制缓冲等成本,需结合 INFO memory。没有适用于所有版本的固定“每 key 几字节”。
依据:8.2.10 配置、对应类型实现、MEMORY USAGE。
4. 性能与阻塞风险
单成员操作与大集合交并差的成本不同。SSCAN 可分摊遍历,但不是有序分页,不提供快照,也可能重复。COUNT 为工作量提示,小型紧凑编码集合可能一次返回全部。大规模共同好友查询应限制候选规模,避免在热路径反复全量交集。
常规命令执行主要占用主执行路径,耗时计算会拖慢同实例其他请求;网络 I/O、后台持久化和释放内存有其他线程/进程参与。命令复杂度不包含所有网络、分配、编码转换与客户端成本。
5. 原子性与并发
SREM 本身可让一个兑换码只被某次调用移除,但并不原子完成数据库发奖。先 SISMEMBER 再 SREM 没有必要,直接使用删除返回值;发奖必须配合兑换记录、请求幂等和补偿。双向好友建立涉及两个键和权限校验时需统一处理。
MULTI/EXEC 保证排队命令连续执行,但执行时单条报错不会回滚其他已成功命令;WATCH 是乐观冲突检测,需要有界重试。Lua 也不会自动回滚运行错误前的写入,应先校验参数与类型,再修改状态。
6. 过期、持久化与生命周期
Set 无成员 TTL。按日去重可用每天独立键并设置整键过期,个人好友则通常按明确事件删除。需要按成员时间过期可另选 ZSet 记录时间并清理。最后一个成员删除后键消失;长期累计成员应定义归档或上限。
大键会增加 RDB/AOF 重写、复制全量同步和恢复时的处理量,fork 写时复制还可能增加峰值内存。单次同步删除聚合对象可能阻塞;UNLINK 先摘除键,把主要回收工作移到后台,但总回收工作和内存压力仍存在,不能替代限长和拆分。
完成教学实验后,只清理自己创建的键,例如 UNLINK learn:types:tags。
7. 集群与扩展
SINTER/SUNION/SDIFF 等相关多键服务端操作要求同槽;把所有玩家好友集放在同一 tag 会导致热点。跨分片求交可在应用侧读取必要数据并计算,但失去单次一致观察。从副本读黑名单可能有短暂漏拦截,授权要求应决定读取位置。
一个键只在一个分片中,hash tag 控制相关键进入同一槽,不会把一个大键切开。副本读取通常是最终一致观察;操作后立即读到本次写入的需求应选主节点或明确的协调策略。
8. Go 客户端与最佳实践
下面函数使用 github.com/redis/go-redis/v9,调用方提供 context.Context 和已配置认证、超时的 *redis.Client。
func SetExample(ctx context.Context, rdb *redis.Client, uid string) (bool, error) {
if err := rdb.SAdd(ctx, "learn:go:beta", uid).Err(); err != nil {
return false, err
}
return rdb.SIsMember(ctx, "learn:go:beta", uid).Result()
}客户端可用 []string 承载成员列表,但不要依赖列表顺序。UID 统一用同一字符串表示,避免 001 和 1 被当成两个成员。Pipeline 可批量发独立查询;需要“比较关系再修改”时使用脚本或 WATCH。
普通 Pipeline 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。
继续阅读
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践