Redis SUNIONSTORE 命令详细教程
SUNIONSTORE 计算多个集合的并集,并把结果写入指定的目标 Key,返回写入的成员数量。它从 Redis 1.0.0 起提供,复杂度为 O(N)(N 为所有参与运算集合的元素总数)。它是 SUNION 的写版本:SUNION 把并集返回给客户端,SUNIONSTORE 把并集留在 Redis 里,便于缓存、二次运算或分批读取。目标 Key 会被完全覆盖,结果为空时目标 Key 会被删除。
资料合集:https://pan.quark.cn/s/10e98d308913、https://pan.quark.cn/s/f56bc69c5338
一、概览与语法
redis
SUNIONSTORE destination key [key ...]
| 项目 | 说明 |
|---|---|
| 数据类型 | Set |
| 支持版本 | Redis 1.0.0 起 |
| destination | 结果写入的目标 Key,已存在时被覆盖 |
| key | 一个或多个参与运算的集合,全部参与求并集 |
| 返回值 | 整数,写入目标集合的成员数量;结果为空时返回 0 |
| 时间复杂度 | O(N),N 为所有给定集合的元素总数 |
| ACL | @write、@set、@slow |
| 命令标记 | write、denyoom |
官方说明:计算所有指定集合的并集并存储到 destination 中。若 destination 已存在则被覆盖。$TRAE_REF
二、基础示例
以下命令在测试实例的 redis-cli 中执行。文中结果是预期说明,未实际连接 Redis 运行。
redis
DEL tutorial:{sunionstore}:key1 tutorial:{sunionstore}:key2 tutorial:{sunionstore}:key
SADD tutorial:{sunionstore}:key1 "a" "b" "c"
SADD tutorial:{sunionstore}:key2 "c" "d" "e"
SUNIONSTORE tutorial:{sunionstore}:key tutorial:{sunionstore}:key1 tutorial:{sunionstore}:key2
SCARD tutorial:{sunionstore}:key
SMEMBERS tutorial:{sunionstore}:key
预期结果:两条 SADD 各返回 3;SUNIONSTORE 返回 5;SCARD 返回 5;SMEMBERS 返回 a、b、c、d、e 共 5 个成员(顺序不保证)。重复成员 c 只出现一次。
三、destination 的覆盖与 TTL 重置
这是本命令最容易出问题的行为:
| destination 原状态 | 运算后 |
|---|---|
| 不存在 | 创建并写入结果 |
| 已存在且结果非空 | 旧内容被整体覆盖,旧成员全部丢失 |
| 已存在且结果为空 | destination 被删除,Key 不再存在 |
| 是其他数据类型 | 被覆盖为 Set,不报错 |
| 有 TTL | TTL 被清除,新 Key 永久存在 |
redis
DEL tutorial:{sunionstore}:overwrite tutorial:{sunionstore}:s1 tutorial:{sunionstore}:s2
SADD tutorial:{sunionstore}:overwrite "old1" "old2"
EXPIRE tutorial:{sunionstore}:overwrite 1000
SADD tutorial:{sunionstore}:s1 "x" "y"
SADD tutorial:{sunionstore}:s2 "y" "z"
SUNIONSTORE tutorial:{sunionstore}:overwrite tutorial:{sunionstore}:s1 tutorial:{sunionstore}:s2
SMEMBERS tutorial:{sunionstore}:overwrite
TTL tutorial:{sunionstore}:overwrite
预期结果:SUNIONSTORE 返回 3;SMEMBERS 返回 x、y、z,旧的 old1、old2 已被清除;TTL 返回 -1,原来设置的 1000 秒过期时间已丢失。
这两点经常成对踩坑:覆盖是 replace 而不是 merge(旧数据全丢),覆盖后 TTL 也一起丢(缓存的 Key 从此永不过期,悄悄变成常驻内存)。把 SUNIONSTORE 用作"预计算缓存"时,必须在写入后立刻重新 EXPIRE。
四、destination 参与运算
destination 可以与源 Key 同名,Redis 会先完整计算再写入,不会出现边算边改的问题:
redis
DEL tutorial:{sunionstore}:self
SADD tutorial:{sunionstore}:self "a" "b"
SUNIONSTORE tutorial:{sunionstore}:self tutorial:{sunionstore}:self
SMEMBERS tutorial:{sunionstore}:self
预期结果:SUNIONSTORE 返回 2;SMEMBERS 返回 a、b。只传一个 Key 时并集就是它自己,因此这是一种"原地去重/重建"的写法。
常见用法是增量累加:
redis
DEL tutorial:{sunionstore}:acc tutorial:{sunionstore}:daily
SUNIONSTORE tutorial:{sunionstore}:acc tutorial:{sunionstore}:acc tutorial:{sunionstore}:daily
预期结果:读起来是"把今天的集合并入累计集合",但注意 destination 同时作为源出现时,可读性差且容易让维护者误以为写错了。更清晰的写法是把结果写到独立 Key(如 acc:result),再用 RENAME 替换,或者直接用 SADD acc member... 累加。
五、空结果的删除语义
并集为空只发生在所有源 Key 都不存在(或都是空集合)时:
redis
DEL tutorial:{sunionstore}:empty
SUNIONSTORE tutorial:{sunionstore}:empty tutorial:{sunionstore}:nokey1 tutorial:{sunionstore}:nokey2
EXISTS tutorial:{sunionstore}:empty
SCARD tutorial:{sunionstore}:empty
预期结果:SUNIONSTORE 返回 0;EXISTS 返回 0;SCARD 返回 0。三者一致说明 destination 已被删除,而不是留下一个空集合。
对缓存场景的启示:结果集合消失后,读取方看到的是"Key 不存在",而不是"空集合"。用 SCARD 判断时都会得到 0,但用 EXISTS 或 TYPE(返回 none)可以确认它确实不存在,需要在监控里区分"未生成"和"已生成但为空"这两种状态。
六、边界情况与错误处理
| 场景 | 行为 |
|---|---|
| 只传 destination 不传 key | 报参数数量错误(至少需要 3 个参数) |
| 只传一个 key | 结果等于该集合本身,返回其成员数量 |
| 任一源 Key 不存在 | 视为空集合;全部不存在则结果为空 |
| 任一源 Key 类型不是 Set | 报 WRONGTYPE,destination 不被修改 |
| destination 是其他类型 | 被覆盖为 Set,不报错 |
| 结果为空 | destination 被删除,返回 0 |
| 写满内存 | 报 OOM 错误(命令带 denyoom 标记) |
redis
SET tutorial:{sunionstore}:str text
SADD tutorial:{sunionstore}:src "one"
SUNIONSTORE tutorial:{sunionstore}:dst tutorial:{sunionstore}:src tutorial:{sunionstore}:str
EXISTS tutorial:{sunionstore}:dst
SUNIONSTORE tutorial:{sunionstore}:dst2
预期结果:第一条因源 Key str 是 String 类型报 WRONGTYPE;EXISTS 返回 0,证明失败时 destination 没有被创建或修改(失败是原子的);最后一条缺少参与运算的 key,报 ERR wrong number of arguments for 'sunionstore' command。
"destination 是其他类型也能被覆盖"这一点与源 Key 的严格校验形成对比:源 Key 类型不对会整体失败,destination 类型不对则直接覆盖。这意味着误把业务数据 Key 当作 destination 时会静默毁掉原数据,而且不会报错。
七、与相近命令的区别
| 命令 | 结果去向 | 是否覆盖目标 | 是否传输成员 | 返回 |
|---|---|---|---|---|
| SUNIONSTORE | 写入 destination | 是(含 TTL 丢失) | 否 | 写入数量 |
| SUNION | 只返回客户端 | 无 | 是 | 成员数组 |
| SINTERSTORE | 交集写入 destination | 是 | 否 | 写入数量 |
| SDIFFSTORE | 差集写入 destination | 是 | 否 | 写入数量 |
| SMEMBERS | 读取单个集合 | 无 | 是 | 成员数组 |
| SADD | 向集合追加成员 | 否(累加) | 否 | 新增数量 |
选择要点:
- 只想本次用一下并集:SUNION,不要多写一次磁盘/内存。
- 结果会被多次读取、或结果很大不想反复传输 :SUNIONSTORE +
EXPIRE。 - 想累加到已有集合 :SADD(
SADD dest member...),或用 SUNIONSTORE 的"自引用"写法,但后者会重建整个集合(O(N))。 - 只关心数量 :SUNION 拿不到数量之外的省事方案,但可以用 SUNIONSTORE 后
SCARD(有写入成本),或在客户端对 SUNION 结果取长度。
八、客户端示例
前提为已安装 redis-py 并准备好本地测试实例。redis-py 的 sunionstore(dest, keys) 返回写入数量。
python
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
k1 = "tutorial:{sunionstore}:py1"
k2 = "tutorial:{sunionstore}:py2"
dest = "tutorial:{sunionstore}:pydest"
try:
r.delete(k1, k2, dest)
r.sadd(k1, "a", "b", "c")
r.sadd(k2, "c", "d")
print(r.sunionstore(dest, [k1, k2])) # 4
print(sorted(r.smembers(dest))) # ['a', 'b', 'c', 'd']
# 覆盖 + TTL 重置:写入后必须重新设置过期时间
r.expire(dest, 60)
print(r.ttl(dest)) # 约 60
r.delete(k1, k2) # 源集合都消失
print(r.sunionstore(dest, [k1, k2])) # 0,结果为空
print(r.exists(dest)) # 0,destination 被删除
finally:
r.delete(k1, k2, dest)
r.close()
Java(Jedis)示例,sunionstore(dstkey, keys...) 返回 long:
java
try (Jedis jedis = new Jedis("localhost", 6379)) {
String k1 = "tutorial:{sunionstore}:java1";
String k2 = "tutorial:{sunionstore}:java2";
String dest = "tutorial:{sunionstore}:javadest";
jedis.del(k1, k2, dest);
jedis.sadd(k1, "a", "b", "c");
jedis.sadd(k2, "c", "d");
System.out.println(jedis.sunionstore(dest, k1, k2)); // 4
System.out.println(jedis.smembers(dest)); // [a, b, c, d]
jedis.expire(dest, 60); // 覆盖后必须重设 TTL
jedis.del(k1, k2, dest);
}
九、原子性与并发
单命令原子:计算与写入在同一条命令内完成,其他客户端看不到"算完但未写"的中间状态。要么看到旧内容,要么看到完整的新内容。
并发覆盖 :多个生产者向同一 destination 执行 SUNIONSTORE 时,最终内容取决于执行顺序,后执行者完全覆盖先执行者。不要把同一个 destination 当作多方共享的累加器------那样会丢数据,应改用 SADD 累加。
结果不是快照锁定:命令结束后 destination 是普通集合,可以被后续 SADD/SREM 修改,也会因 TTL 到期而消失。
事务与 Lua:可在 MULTI/EXEC、Lua、Pipeline 中使用。需要在"生成结果 + 设置 TTL + 通知下游"之间保持原子性时,用 Lua 把这几步包在一起最稳。
集群限制 :destination 与所有源 Key 必须落在同一哈希槽,否则报 CROSSSLOT。给所有 Key 加相同哈希标签即可(例如 {cart}),但要注意这会把这些 Key 绑到同一节点。
十、性能与使用注意事项
O(N) 且带 @slow 标记:N 是所有源集合的元素总数。它是同步命令,大集合上会阻塞主线程,必要时在副本(不能写)之外的低峰时段执行,或拆小批量。
写入成本真实存在:结果集合要实际占用内存。把 SUNIONSTORE 当作"每次请求都跑一遍"的实时计算是误用,它适合"定期预计算 + 多次读取"。
覆盖会导致内存抖动 :旧结果集合被整体释放、新集合被整体构建,内存峰值约为新旧两份之和。超大集合上要留意 maxmemory 触发淘汰的可能。
TTL 必须显式设置 :覆盖之后 TTL 归零,这在缓存场景里是"内存缓慢泄漏"的常见来源。写完后立刻 EXPIRE,或把"生成 + 过期"收进 Lua。
避免用自引用反复重建 :SUNIONSTORE dest dest src 每次都重建整个集合,成本 O(总元素数)。持续累加应使用 SADD dest member...(O(每次新增数量))。
与 SSCAN 配合读取 :结果集合可能很大,读取时不要用 SMEMBERS 拉全量,用 SSCAN 分批遍历。
十一、典型业务场景
权限并集缓存 :用户多角色权限集合求并集写入 perm:{uid},并设置 30 分钟 TTL,登录后的鉴权直接读缓存集合。
多来源名单合并 :把站内关注、导入名单、活动参与三个集合合并为 audience:{campaign},投放系统读取结果集合。
标签画像预计算:多个标签集合定期求并集写入画像 Key,供离线任务与实时查询共用。
季度/年度活跃汇总 :按月活跃用户集合求并集写入季度活跃集合,报表直接 SCARD。
多仓库库存合并:各仓可售 ID 集合求并集写入全局可售集合,前端查询只读一份数据。
灰度名单合并:多批次灰度名单求并集写入最终名单,避免每次请求都做多集合运算。