Redis SMOVE 命令详细教程
SMOVE 把一个成员从一个集合原子地移动到另一个集合:成员在源集合中时,从源集合删除并加入目标集合,返回 1;成员不在源集合中时不做任何操作,返回 0。它从 Redis 1.0.0 起提供,复杂度为 O(1)。可以把它理解为 SREM + SADD 的原子合并版:省一次网络往返,也不会出现"从源删掉了但没加进目标"的中间状态。
资料合集:https://pan.quark.cn/s/10e98d308913、https://pan.quark.cn/s/f56bc69c5338
一、概览与语法
SMOVE source destination member
| 项目 | 说明 |
|---|---|
| 数据类型 | Set(源与目标都必须是 Set) |
| 支持版本 | Redis 1.0.0 起 |
| source | 源集合 Key,成员从这里移出 |
| destination | 目标集合 Key,不存在时自动创建 |
| member | 要移动的成员 |
| 返回值 | 整数 1 表示发生了移动,0 表示未移动 |
| 时间复杂度 | O(1) |
| ACL | @write、@set、@fast |
| 命令标记 | write、fast |
官方说明:把 member 从 source 集合移动到 destination 集合。若 member 不是 source 的成员,则不执行任何操作并返回 0。$TRAE_REF
二、基础示例
以下命令在测试实例的 redis-cli 中执行。文中结果是预期说明,未实际连接 Redis 运行。
DEL tutorial:{smove}:src tutorial:{smove}:dst
SADD tutorial:{smove}:src "one" "two"
SADD tutorial:{smove}:dst "three"
SMOVE tutorial:{smove}:src tutorial:{smove}:dst "two"
SMEMBERS tutorial:{smove}:src
SMEMBERS tutorial:{smove}:dst
预期结果:两条 SADD 分别返回 2、1;SMOVE 返回 1;源集合只剩 "one";目标集合变为 "three"、"two"。
三、返回值与"未移动"的几种情形
| 情形 | 返回 | 数据变化 |
|---|---|---|
| member 在 source 中 | 1 | source 移除该成员,destination 加入该成员 |
| member 不在 source 中 | 0 | 两边都不变,destination 也不会被创建 |
| source 不存在 | 0 | 两边都不变 |
| source 与 destination 是同一个 Key | 1(成员存在)/0(成员不存在) | 集合内容不变,等价于一次 SISMEMBER |
| destination 类型不是 Set | 报 WRONGTYPE | 两边都不变 |
SMOVE tutorial:{smove}:src tutorial:{smove}:dst "nope"
SMEMBERS tutorial:{smove}:dst
SMOVE tutorial:{smove}:nokey tutorial:{smove}:dst "one"
EXISTS tutorial:{smove}:nokey
预期结果:第一条返回 0;SMEMBERS 仍为 three、two,未受影响;第三条返回 0;EXISTS 返回 0,说明不存在的 source 不会因为 SMOVE 被创建。
自移动示例,常用于"只做存在性校验而不修改数据":
DEL tutorial:{smove}:self
SADD tutorial:{smove}:self "a"
SMOVE tutorial:{smove}:self tutorial:{smove}:self "a"
SMOVE tutorial:{smove}:self tutorial:{smove}:self "b"
SCARD tutorial:{smove}:self
预期结果:第一条 SMOVE 返回 1(成员存在),第二条返回 0(成员不存在);SCARD 返回 1,集合内容没有变化。这种写法语义上等价于 SISMEMBER,但可读性差,日常判断存在性应直接用 SISMEMBER。
四、目标集合的创建、去重与源集合删除
三个容易忽略的行为:
- destination 不存在时自动创建,新 Key 不带过期时间。
- destination 已含该成员时不报错、不重复:源集合删除该成员,目标集合保持不变,返回值仍是
1。 - source 移空后 Key 被删除:集合不会以"空集合"形式留在内存里。
DEL tutorial:{smove}:a tutorial:{smove}:b
SADD tutorial:{smove}:a "x"
SADD tutorial:{smove}:b "x" "y"
SMOVE tutorial:{smove}:a tutorial:{smove}:b "x"
EXISTS tutorial:{smove}:a
SCARD tutorial:{smove}:b
预期结果:SMOVE 返回 1;EXISTS 返回 0(源集合已空并被删除);SCARD 返回 2,目标集合仍是 x、y,没有产生重复成员。
第 3 点对业务有实际影响:如果代码用 EXISTS 判断"这批数据是否处理完",源集合移空后的消失可以被直接当作完成信号。但反过来,如果业务依赖"空集合仍然存在"来做状态标记,SMOVE 是无法保留这个空壳的,需要改用额外的标记 Key。
五、TTL 与 Key 生命周期
| 对象 | SMOVE 后的 TTL 行为 |
|---|---|
| source 仍有成员 | 原有 TTL 保持不变 |
| source 被移空 | Key 被删除,TTL 随之消失 |
| destination 原本不存在 | 新建 Key,无 TTL(永久) |
| destination 原本存在 | 原有 TTL 保持不变 |
| 被移动的成员 | 成员本身只是换了容器,值不变 |
DEL tutorial:{smove}:ttl1 tutorial:{smove}:ttl2
SADD tutorial:{smove}:ttl1 "a" "b"
EXPIRE tutorial:{smove}:ttl1 100
SADD tutorial:{smove}:ttl2 "z"
EXPIRE tutorial:{smove}:ttl2 50
SMOVE tutorial:{smove}:ttl1 tutorial:{smove}:ttl2 "a"
TTL tutorial:{smove}:ttl1
TTL tutorial:{smove}:ttl2
SMOVE tutorial:{smove}:ttl1 tutorial:{smove}:ttl2 "b"
EXISTS tutorial:{smove}:ttl1
预期结果:第一次 SMOVE 后两个 TTL 约为 100 与 50(实际略小于设定值);把 b 也移走后 EXISTS 返回 0,源 Key 彻底消失。
这带来一个常见坑:把一个带 TTL 的临时集合逐个 SMOVE 进目标集合,成员会"继承"目标的 TTL(或永久存在),而不是把源集合的过期特性带过来。若希望目标数据也自动过期,必须在写入后显式对 destination 执行 EXPIRE。
六、边界情况与错误处理
| 场景 | 行为 |
|---|---|
| 缺少 member 参数 | 报参数数量错误(命令需要 4 个参数) |
| source 不存在 | 返回 0,destination 不变 |
| source 是其他类型 | 报 WRONGTYPE |
| destination 是其他类型 | 报 WRONGTYPE,source 不修改 |
| member 为空字符串 | 按普通成员处理 |
| 大小写不同 | 视为不同成员 |
| 集群中两 Key 不在同一槽 | 报 CROSSSLOT |
SET tutorial:{smove}:str text
DEL tutorial:{smove}:src2
SADD tutorial:{smove}:src2 "one"
SMOVE tutorial:{smove}:src2 tutorial:{smove}:str "one"
SMEMBERS tutorial:{smove}:src2
SMOVE tutorial:{smove}:src2 tutorial:{smove}:dst2
预期结果:第四条对 destination 是 String 类型报 WRONGTYPE;SMEMBERS 仍返回 "one",证明失败时源集合没有被部分修改;最后一条缺少 member,报 ERR wrong number of arguments for 'smove' command。
"目标类型错误时源集合不受影响"是 SMOVE 重要的安全属性:类型校验发生在删除之前,不会出现"先从源删掉、再因为目标类型错误而丢失成员"的情况。但这也意味着错误必须被捕获,如果客户端吞掉了 WRONGTYPE 异常,业务会误以为移动成功。
七、与相近命令的区别
| 命令 | 是否原子 | 网络往返 | 适用场景 |
|---|---|---|---|
| SMOVE | 单命令原子 | 1 | 集合之间移动单个成员 |
| SREM + SADD | 否 | 2 | 需要附带额外逻辑时,应改用 MULTI/Lua |
| SADD + SREM | 否 | 2 | 顺序不同、失败点不同,风险更高 |
| LMOVE | 单命令原子 | 1 | List 之间移动元素(对应集合场景的 SMOVE) |
| SISMEMBER | 只读 | 1 | 仅判断成员是否存在 |
| SPOP / SRANDMEMBER | 单命令原子 | 1 | 随机移除/随机读取,不指定成员 |
| SINTERSTORE / SUNIONSTORE | 单命令原子 | 1 | 整体集合运算并落地,不是单成员搬移 |
选择要点:
- 只需移动一个指定成员,用 SMOVE,不要自己拼 SREM + SADD。
- 需要"移动失败就回滚"或"移动并记录日志"等复合逻辑,用 Lua 脚本包裹。
- 需要整体搬迁集合内容,用
SUNIONSTORE destination source destination再DEL source,而不是循环 SMOVE N 次。 - SMOVE 只能在 Set 之间移动,不能把成员搬到 List/Hash,跨数据类型要用对应的 LPUSH/HSET 等命令。
八、客户端示例
前提为已安装 redis-py 并准备好本地测试实例。redis-py 的 smove(src, dst, value) 返回布尔值。
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
src = "tutorial:{smove}:pysrc"
dst = "tutorial:{smove}:pydst"
try:
r.delete(src, dst)
r.sadd(src, "one", "two")
r.sadd(dst, "three")
print(r.smove(src, dst, "two")) # True,移动成功
print(r.smove(src, dst, "nope")) # False,成员不在源集合
print(sorted(r.smembers(src))) # ['one'],源集合被移空一个成员
print(sorted(r.smembers(dst))) # ['three', 'two']
finally:
r.delete(src, dst)
r.close()
Java(Jedis)示例,smove(srcKey, dstKey, member) 返回 long:
try (Jedis jedis = new Jedis("localhost", 6379)) {
String src = "tutorial:{smove}:javasrc";
String dst = "tutorial:{smove}:javadst";
jedis.del(src, dst);
jedis.sadd(src, "one", "two");
jedis.sadd(dst, "three");
System.out.println(jedis.smove(src, dst, "two")); // 1
System.out.println(jedis.smove(src, dst, "nope")); // 0
System.out.println(jedis.smembers(src)); // [one]
System.out.println(jedis.smembers(dst)); // [three, two]
jedis.del(src, dst);
}
注意返回类型差异:redis-cli 与 Jedis 返回整数 0/1,redis-py 转换成布尔值。用 if result: 判断在 Python 中可行,在 Java 中要写 if (result == 1),不能用 Boolean 直接接收 long。
九、原子性与并发
单命令原子:删除与加入在同一步完成。其他客户端不会观察到"两边都有"或"两边都没有"的中间状态。这正是它优于 SREM + SADD 的核心原因——后者在两步之间失败会导致成员直接丢失。
并发争抢同一个成员:两个客户端同时对同一成员执行 SMOVE,只有一个返回 1,另一个返回 0(此时成员已不在源集合)。这个特性可以用来实现"抢占式任务领取":待处理集合 → 处理中集合,返回 1 的那一方获得任务。
并发与 SREM 竞争:如果在 SMOVE 执行前该成员被其他客户端 SREM 掉,SMOVE 返回 0,目标集合不受影响,不会"凭空多出一个成员"。
事务与 Pipeline:可在 MULTI/EXEC、Lua、Pipeline 中使用。Pipeline 只是批量发送,不提供跨命令一致性;Lua 内可把 SMOVE 与后续判断、计数组合成原子逻辑。
集群限制:source 与 destination 必须在同一哈希槽,否则报 CROSSSLOT。给两个 Key 加相同哈希标签即可(示例中的 {smove} 标签正是这个作用),可用 SMOVE tutorial:{smove}:src tutorial:{smove}:dst member 这种写法保证同槽。
十、性能与使用注意事项
恒为 O(1):哈希定位一次,做一次删除和一次插入,与集合规模无关。
省一次往返:相比 SREM + SADD,网络成本减半;在高频调度场景(每秒数千次任务转移)差距明显。
避免循环 SMOVE 做批量迁移:N 个成员就是 N 次调用。整集合搬迁用 SUNIONSTORE 一次完成,或用 Lua 脚本批量搬。
内存即时释放:成员移出后源集合立即释放其内存;源集合被移空时 Key 整体删除,不会残留空集合开销。
编码可能在移动时切换:集合在 intset 与 hashtable 之间按内容自动转换,SMOVE 触发的正常增删可能引起编码变化,但不改变命令复杂度。
destination 不会被动覆盖:与 SINTERSTORE 不同,SMOVE 是"合并一个成员"而非"整体替换",不会清空目标集合已有成员。但反过来,如果目标 Key 被误当成临时集合,脏数据会持续累积,需要配合 SREM 或 TTL 管理。
TTL 不会自动继承:目标集合的存活时间与源集合无关,跨集合迁移时务必显式设置过期时间。
十一、典型业务场景
任务状态流转:queue:pending → queue:processing,返回 1 表示领取成功,天然实现无锁抢占;完成后从 processing 移入 queue:done。
审核状态迁移:用户提交后在 audit:waiting,审核通过时 SMOVE 到 audit:passed,拒绝时 SMOVE 到 audit:rejected。
名额与资格发放:中签用户从 lottery:candidates 移入 lottery:winners,返回 1 计为一次成功发放,避免重复发放。
在线状态迁移:会话超时时把用户 ID 从 online 移到 offline,两者都是集合,便于批量统计。
灰度分组调整:把用户从一个实验组集合转入另一个,保证同一用户不会同时出现在两组(前提是先从源移出)。
临时集合归档:带 TTL 的临时去重集合在处理完成后,把需要保留的成员逐个 SMOVE 到持久集合,再让临时 Key 自然过期。
Redis SMOVE命令用于原子性地将一个成员从源集合移动到目标集合,时间复杂度O(1)。它保证删除与插入的原子性,避免SREM+SADD的中间状态风险。支持自动创建目标集合、去重、源集合清空后自动删除Key,并具备TTL不继承、跨槽限制、类型安全等关键特性。广泛应用于任务抢占、状态流转、资格发放等高并发业务场景。

197

被折叠的 条评论
为什么被折叠?



