Redis命令:SUNIONSTORE

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 集合求并集写入全局可售集合,前端查询只读一份数据。

灰度名单合并:多批次灰度名单求并集写入最终名单,避免每次请求都做多集合运算。

相关推荐
麻瓜code2 小时前
秒杀优化三连:Lua + 阻塞队列 + Redis Stream,响应从 3.4 秒到毫秒级
redis·junit·lua
没有气的汽水5 小时前
一次生产 Redis 内存打满的排查、清理和复盘
redis
hey you~6 小时前
云呼叫中心坐席自动分配实战:ACD 策略、Redis+Lua 实现与排查清单
数据库·redis·lua·云呼叫中心·acd·坐席分配·坐席状态机
我不会起名字3227 小时前
Redis pipeline 用了反而更慢:3 类误用与正确的批量姿势
redis·pipeline·lua·rtt·批量
Tsuki_tl7 小时前
【总结】Redis五大数据类型应用场景及基本命令
数据库·redis·后端·缓存·缓存中间件·redis命令·redis 五大数据类型
代码丰1 天前
大 JSON 放进 Redis 之后,如何用 Stream 解耦耗时任务?
redis·后端·系统架构
一枕槐安4871 天前
RAG 服务扛住高并发:缓存、队列、限流三件套
java·spring·缓存·异步任务·rag·断路器
枫子有风1 天前
卡码网盘项目深挖
java·redis·jwt·es·pgvector
福大大架构师每日一题1 天前
redis 8.10.3正式发布:聚焦集群总线超大消息传播场景的安全修复,8.10.3 Latest更新解读
数据库·redis·安全