Skip to content
← 返回Redis列表
数据与原子性

Redis Set(集合) ​

本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。

1. 定位:解决什么问题 ​

Set 是没有顺序保证的唯一字符串成员集合,适合好友、黑名单、标签和精确去重。需要先后顺序用 List,需要分数或排名用 ZSet,只需要近似人数且不查询成员时可选 HyperLogLog。

基本操作示例 ​

text
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 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。

text
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 d
c 已存在,新增 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)
版本与复杂度补充

下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。

命令引入版本与状态复杂度说明
SADD1.0.0O(K),K 为本次指定元素数。
SREM1.0.0O(K),K 为本次指定元素数。
SISMEMBER1.0.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
SMISMEMBER6.2.0O(K),K 为本次指定元素数。
SCARD1.0.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
SMEMBERS1.0.0O(N),N 为集合大小。
SSCAN2.8.0官方渐进遍历口径:单次 O(1),完整迭代 O(N);COUNT 只是提示,编码和结果数量影响耗时。
SRANDMEMBER1.0.0单元素 O(1);带 COUNT 时 O(M),M 为请求返回数。
SPOP1.0.0单元素 O(1);带 COUNT 时 O(M),M 为请求返回数。
SMOVE1.0.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
SINTER1.0.0最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。
SINTERCARD7.0.0最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。
SINTERSTORE1.0.0最坏 O(Nmin×K),Nmin 为最小集合大小,K 为输入集合数。
SUNION1.0.0O(T),T 为所有输入集合的成员数之和。
SUNIONSTORE1.0.0O(T),T 为所有输入集合的成员数之和。
SDIFF1.0.0O(T),T 为所有输入集合的成员数之和。
SDIFFSTORE1.0.0O(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。这是该版本的编码条件,不能靠改变阈值代替容量治理。

编码与内存怎么测 ​

在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:

text
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。

go
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 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。

继续阅读 ​

数据类型总览 · 游戏与通用后端实践 · 原子性基础。

参考:Redis 8.2.10 命令定义、go-redis、集群规格。

继续阅读