Redis命令:SMOVE

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

一、概览与语法

redis 复制代码
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 运行。

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 两边都不变
redis 复制代码
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 被创建。

自移动示例,常用于"只做存在性校验而不修改数据":

redis 复制代码
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 被删除:集合不会以"空集合"形式留在内存里。
redis 复制代码
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 保持不变
被移动的成员 成员本身只是换了容器,值不变
redis 复制代码
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
redis 复制代码
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) 返回布尔值。

python 复制代码
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:

java 复制代码
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 自然过期。

相关推荐
MetaLite4 小时前
Spring 同一个类里调用 @CacheEvict,为什么缓存没清掉?
java·spring·缓存
可乐ea4 小时前
Agent 隔离为什么失效:CPU 缓存成了跨机器信道
缓存·ai智能体·红队测试·沙箱安全·agent隔离·侧信道
wifi___6 小时前
Redis分布式锁
数据库·redis·分布式
谢白羽6 小时前
从传统运维到 AI Infra: K8s 上调度 100 张 GPU踩坑笔记
运维·人工智能·缓存·kubernetes·vllm·sglang·vllm-omni
萧瑟余晖7 小时前
Spring缓存抽象详解
java·spring·缓存
李纲明7 小时前
WordPress 前台改完还是旧页:浏览器、CDN、页面缓存、对象缓存怎么分层清
缓存·cdn·wordpress·devdiy
夕除8 小时前
redis--IO复用(epoll)
redis
乐观的Terry9 小时前
AI翻译助手-JWT与redis无状态认证
数据库·redis·缓存