SDIFF 计算多个集合的差集:返回第一个集合中存在、且不在任何后续集合中出现的成员。它从 Redis 1.0.0 起提供,是集合三大运算(差集 SDIFF、交集 SINTER、并集 SUNION)之一。差集运算是有方向的,SDIFF A B 与 SDIFF B A 结果通常不同,这是使用时最容易出错的地方。
一、概览与语法
| 项目 |
说明 |
| 数据类型 |
Set |
| 支持版本 |
Redis 1.0.0 起 |
| key |
至少一个集合 Key;第一个是被减数,其余是要排除的集合 |
| 返回值 |
数组,差集结果中的成员;无结果时返回空数组 |
| 时间复杂度 |
O(N),N 为所有参与集合的成员总数 |
| ACL |
@read、@set、@slow |
| 命令标记 |
readonly、sort_for_script |
官方说明:返回第一个集合与所有后续集合之间的差集,即第一个集合中不存在于任何后续集合的成员。不存在的 Key 被视为空集合。
差集的数学表达:SDIFF A B C = A − (B ∪ C)。注意是减去后续集合的并集,不是逐个依次相减。
二、基础示例
以下命令在测试实例的 redis-cli 中执行。文中结果是预期说明,未实际连接 Redis 运行。示例沿用官方文档的数据与结果。
|
1
2
3
4
5
|
DEL tutorial:{sdiff}:key1 tutorial:{sdiff}:key2 tutorial:{sdiff}:key3
SADD tutorial:{sdiff}:key1 "a" "b" "c" "d"
SADD tutorial:{sdiff}:key2 "c"
SADD tutorial:{sdiff}:key3 "a" "c" "e"
SDIFF tutorial:{sdiff}:key1 tutorial:{sdiff}:key2 tutorial:{sdiff}:key3
|
预期结果:三条 SADD 分别返回 4、1、3;SDIFF 返回 "b"、"d"。key1 中的 a 被 key3 排除,c 被 key2 与 key3 排除,只剩 b、d。
三、差集的方向性
差集不满足交换律,参数的书写顺序直接决定结果:
|
1
2
|
SDIFF tutorial:{sdiff}:key1 tutorial:{sdiff}:key2
SDIFF tutorial:{sdiff}:key2 tutorial:{sdiff}:key1
|
预期结果:第一条返回 "a"、"b"、"d"(key1 去掉 c);第二条返回空数组(key2 只有 c,而 c 在 key1 中)。
这组对比说明:第一个 Key 是基准集合,后面的 Key 只用于排除。业务上想表达"我有而你没有",必须是"我"在前。
四、多集合语义与不存在的 Key
| 场景 |
结果 |
| 第一个 Key 不存在 |
空数组(空集减任何集合仍为空) |
| 某个后续 Key 不存在 |
视为空集合,不影响结果 |
| 所有 Key 都存在但无差异 |
空数组 |
| 参与运算的 Key 中有非集合类型 |
报 WRONGTYPE |
|
1
2
3
4
|
DEL tutorial:{sdiff}:empty tutorial:{sdiff}:exist
SADD tutorial:{sdiff}:exist "x"
SDIFF tutorial:{sdiff}:empty tutorial:{sdiff}:exist
SDIFF tutorial:{sdiff}:exist tutorial:{sdiff}:empty
|
预期结果:第一条 SDIFF 返回空数组(基准为空集);第二条返回 "x"(后续 Key 不存在视为空集合,不影响结果)。
不存在的 Key 不报错这一点很实用:用差集做"未读列表"时,即使用户从未有过已读记录,也不必先创建空集合。
五、返回值与空结果的区分
SDIFF 只返回数组,不返回计数:
| 情形 |
返回 |
| 存在差集成员 |
成员数组,顺序不保证 |
| 差集为空 |
空数组,不返回空值 |
| Key 类型错误 |
报 WRONGTYPE |
空数组是正常结果,表示"没有差异",不是错误。返回值没有顺序保证:集合本身无序,SDIFF 的输出顺序也不代表任何业务含义。需要稳定顺序时,应在客户端自行排序,或把结果用 SDIFFSTORE 落地后借助 SORT 处理。
需要结果数量时,客户端取数组长度即可;若只关心数量而不需要成员,Redis 7.0 起可用 SINTERCARD(仅交集),差集没有对应的 count 版本,仍需取回结果或改用 SDIFFSTORE。
六、边界情况与错误处理
| 场景 |
行为 |
| 只传一个 Key |
返回该集合的全部成员(相当于复制) |
| 未传任何 Key |
报语法错误 |
| 任一 Key 类型不是 Set |
报 WRONGTYPE |
| 成员为空字符串 |
正常参与运算 |
| 结果成员数超过 2^32 - 1 |
不可能,受集合上限约束 |
|
1
2
3
4
|
SDIFF tutorial:{sdiff}:key1
SET tutorial:{sdiff}:str text
SDIFF tutorial:{sdiff}:key1 tutorial:{sdiff}:str
SDIFF
|
预期结果:第一条返回 key1 的全部成员 a、b、c、d;第二条因包含 String 类型报 WRONGTYPE;第三条报 ERR wrong number of arguments for 'sdiff' command。
七、与相近命令的区别
| 命令 |
语义 |
结果写入 |
复杂度 |
| SDIFF |
差集 A − (B ∪ C) |
只返回 |
O(N) |
| SDIFFSTORE |
差集并存入 destination |
写入目标 Key |
O(N) |
| SINTER |
交集 A ∩ B ∩ C |
只返回 |
O(N×M) |
| SUNION |
并集 A ∪ B ∪ C |
只返回 |
O(N) |
| SINTERCARD |
交集基数(可带 LIMIT) |
只返回数量 |
O(N×M) |
| SMEMBERS |
单个集合全部成员 |
只返回 |
O(N) |
SDIFFSTORE 是 SDIFF 的写版本,形式为 SDIFFSTORE destination key [key ...],返回写入结果的成员数量。它会把结果覆盖写入 destination:如果 destination 恰好是参与运算的某个 Key,结果会先计算再覆盖,语义上是安全的,但容易被误读,建议使用独立的 Key。
差集与交集、并集的组合可以表达复杂的集合逻辑,例如"关注了我但我不关注的人"(差集)、“互相关注”(交集)、“所有相关用户”(并集)。
八、客户端示例
前提为已安装 redis-py 并准备好本地测试实例。redis-py 的 sdiff(keys, *args) 返回 set,参数可传列表或逐个展开。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
k1 = "tutorial:{sdiff}:py1"
k2 = "tutorial:{sdiff}:py2"
k3 = "tutorial:{sdiff}:py3"
try:
r.delete(k1, k2, k3)
r.sadd(k1, "a", "b", "c", "d")
r.sadd(k2, "c")
r.sadd(k3, "a", "c", "e")
print(r.sdiff([k1, k2, k3])) # {'b', 'd'},返回 set
print(r.sdiff(k1, k2)) # {'a', 'b', 'd'}
print(r.sdiff("no-such-1", "no-such-2")) # set(),基准不存在
finally:
r.delete(k1, k2, k3)
r.close()
|
Java(Jedis)示例,sdiff(keys...) 返回 Set<String>:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
try (Jedis jedis = new Jedis("localhost", 6379)) {
String k1 = "tutorial:{sdiff}:java1";
String k2 = "tutorial:{sdiff}:java2";
String k3 = "tutorial:{sdiff}:java3";
jedis.del(k1, k2, k3);
jedis.sadd(k1, "a", "b", "c", "d");
jedis.sadd(k2, "c");
jedis.sadd(k3, "a", "c", "e");
System.out.println(jedis.sdiff(k1, k2, k3)); // [b, d],顺序不保证
System.out.println(jedis.sdiff(k1, k2)); // [a, b, d]
jedis.del(k1, k2, k3);
}
|
九、原子性与并发
只读且原子:SDIFF 不修改任何数据,单命令原子,返回执行瞬间的差集快照,不会读取到"部分更新"的中间状态。
结果可能立即过期:返回的是快照,命令结束后集合仍可被其他客户端修改。若业务需要"读到之后持续使用这份差异",应在客户端保存结果,或用 SDIFFSTORE 落库到独立 Key。
事务与 Pipeline:可在 MULTI/EXEC、Lua 脚本和管道中正常使用。在 Lua 脚本中结果会被排序(命令标记含 sort_for_script),便于脚本逻辑稳定判断。
集群限制:Redis Cluster 中所有参与运算的 Key 必须落在同一哈希槽,否则报 CROSSSLOT。给多个 Key 加相同哈希标签(如 tutorial:{sdiff}:key1、tutorial:{sdiff}:key2)即可保证同槽。
十、性能与使用注意事项
复杂度 O(N):N 是所有参与集合的成员总数。Redis 会先选一个集合作为基准逐成员检查,实际成本与最坏情况下的成员总数相关。参与运算的集合越大、Key 越多,耗时越长,且带 @slow 标记。
大集合慎用:百万级集合做差集会长时间占用主线程,阻塞其他命令。可考虑:
- 在只读副本上执行;
- 用 SSCAN 分批在客户端计算;
- 用 SDIFFSTORE 异步落地后再消费(仍会阻塞);
- 从数据模型上避免超大集合,按时间或用户分片。
结果传输开销:SDIFF 会把全部结果成员传回客户端,结果很大时网络开销显著。只关心数量时,可用 SDIFFSTORE 到一个临时 Key 再 SCARD,或用 Lua 脚本在服务端求长度(注意脚本同样阻塞)。
内存:差集运算在服务端不分配持久内存,但计算过程可能用到临时集合结构,超大集合时需留意内存峰值。
十一、典型业务场景
未读消息列表:全部消息集合减去已读集合,得到未读消息 ID;即使用户从未读过,已读集合不存在也不影响结果。
推荐候选过滤:候选内容集合减去已曝光/已购买集合,得到本次可推荐的集合。
权限差异对比:用户当前权限集合减去应有权限集合,找出越权项;反向相减则找出缺失权限。
好友关系分析:关注集合减去互关集合,得到"单向关注"的人。
数据同步差异:两个版本的数据 Key 集合做差,找出新增或缺失项。
标签补全:目标标签集合减去用户已有标签集合,得到还需要打的标签。
十二、练习、排错与总结
练习:新建 tutorial:{sdiff}:exa、tutorial:{sdiff}:exb、tutorial:{sdiff}:exc;向 exa 写入 1 2 3 4 5,向 exb 写入 2 4,向 exc 写入 5 6;执行 SDIFF exa exb exc 期望得到 1、3;执行 SDIFF exb exa 期望空数组;执行 SDIFF exa not-exist 期望返回 exa 的全部成员;用 SDIFFSTORE 把结果写入 tutorial:{sdiff}:exout 并用 SCARD 验证数量为 2。
排错要点:结果与预期相反时先检查参数顺序,第一个 Key 才是基准;结果比预期多出成员时,检查后续 Key 是否拼写错误或写成了不存在的 Key(不存在的 Key 视为空集,不会排除任何成员);报 WRONGTYPE 时用 TYPE 逐个检查参与运算的 Key;报 CROSSSLOT 时给所有 Key 加相同哈希标签;大集合上命令阻塞时考虑副本执行或客户端分批;结果顺序与预期不符属正常,集合无序。清理使用 DEL tutorial:{sdiff}:key1 tutorial:{sdiff}:key2 tutorial:{sdiff}:key3 tutorial:{sdiff}:empty tutorial:{sdiff}:exist tutorial:{sdiff}:str tutorial:{sdiff}:py1 tutorial:{sdiff}:py2 tutorial:{sdiff}:py3 tutorial:{sdiff}:java1 tutorial:{sdiff}:java2 tutorial:{sdiff}:java3 tutorial:{sdiff}:exa tutorial:{sdiff}:exb tutorial:{sdiff}:exc tutorial:{sdiff}:exout。速记:1.0 起支持、第一个集合减去后续集合的并集、有方向不可交换、不存在的 Key 视为空集、无结果返回空数组、结果为快照无序、复杂度 O(N) 大集合慎用、SDIFFSTORE 可落库、集群中必须同槽、与 SINTER/SUNION 构成集合三运算。