Redis命令:SMOVE

Redis SMOVE命令用于原子性地将一个成员从源集合移动到目标集合,时间复杂度O(1)。它保证删除与插入的原子性,避免SREM+SADD的中间状态风险。支持自动创建目标集合、去重、源集合清空后自动删除Key,并具备TTL不继承、跨槽限制、类型安全等关键特性。广泛应用于任务抢占、状态流转、资格发放等高并发业务场景。

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 中1source 移除该成员,destination 加入该成员
member 不在 source 中0两边都不变,destination 也不会被创建
source 不存在0两边都不变
source 与 destination 是同一个 Key1(成员存在)/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。

四、目标集合的创建、去重与源集合删除

三个容易忽略的行为:

  1. destination 不存在时自动创建,新 Key 不带过期时间。
  2. destination 已含该成员时不报错、不重复:源集合删除该成员,目标集合保持不变,返回值仍是 1。
  3. 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单命令原子1List 之间移动元素(对应集合场景的 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 自然过期。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值