Redis MSETNX 命令详细教程
MSETNX 仅当指定的全部 Key 都不存在时,一次性创建所有 String 键值对。任意一个 Key 已存在,就全部不写;不是"跳过已有 Key,补齐其他 Key"。
资料合集:https://pan.quark.cn/s/10e98d308913、https://pan.quark.cn/s/f56bc69c5338
一、语法与返回值
redis
MSETNX key value [key value ...]
| 项目 | 说明 |
|---|---|
| 可用版本 | Redis 1.0.1 起 |
| 参数 | 至少一组 Key/Value,成对提供 |
| 成功返回 | 整数 1,全部写入;不是写入数量 |
| 条件不满足 | 整数 0,整批不写 |
| 官方复杂度 | O(N),N 为 Key 数量 |
| ACL 类别 | @write、@string、@slow |
全部不存在的检查和批量写入是同一原子操作,可用于创建分散在多个 Key 的逻辑对象;RESP2 和 RESP3 的成功/条件失败回复均为整数。参数、权限、内存、部署路由等错误仍可能产生错误回复,不应与整数 0 混为一谈。$TRAE_REF
二、从成功到条件失败
在测试 redis-cli 交互会话执行。以下是预期结果,未连接实例实测;删除前确认专用 Key 可清理。
redis
DEL tutorial:{msetnx}:a tutorial:{msetnx}:b tutorial:{msetnx}:c
MSETNX tutorial:{msetnx}:a "Hello" tutorial:{msetnx}:b "there"
MSETNX tutorial:{msetnx}:b "new" tutorial:{msetnx}:c "world"
MGET tutorial:{msetnx}:a tutorial:{msetnx}:b tutorial:{msetnx}:c
第一次写入返回 1。第二次因 b 已存在而返回 0:b 保持 "there",c 根本没有创建;最终结果依次为 "Hello"、"there"、nil。
三、存在性的准确含义
| 状态 | 结果 |
|---|---|
| 所有 Key 都不存在 | 返回 1,创建全部 String |
| 任意 Key 已存在且为 String | 返回 0,整批不写 |
| 任意 Key 存在但为 Hash/List 等 | 返回 0,不因该类型而报 WRONGTYPE |
任意 Key 的值为 "" |
它仍然存在,返回 0 |
| 已有值与拟写入值相同 | 仍然返回 0,不比较 Value |
| Key 已过期且执行时判定不存在 | 按不存在处理;仍须其他 Key 也不存在 |
新写 Value 为 "" |
合法,成功后该 Key 存在 |
| 没有键值对或参数不成对 | 返回参数数量错误 |
不存在与"存在但为空"不能混淆,先读 GET 再判断空值会引入错误逻辑和竞态。重复 Key 也不应当作防重复机制:在常规实现中,全部预检查通过后依次写入,同名 Key 后值覆盖前值;建议发送前按业务规则去重,不依赖重复参数行为。
四、TTL 与不支持的选项
命令只接受键值对,没有 EX、PX、KEEPTTL 等选项。成功创建的 Key 无过期时间,TTL 为 -1;返回 0 时不修改已有 Key 的值与到期时刻,但剩余 TTL 会随真实时间自然减少。它与 MSET 的区别是根本不覆盖已存在的数据。$TRAE_REF
特别注意 MSETNX a A EX 60 会把 EX 当作另一个 Key,要求 a 和 EX 都不存在,成功时写入 a=A、EX=60,而不是为 a 设置 TTL。不要直接拼接 SET 风格的选项。
先 MSETNX 再由客户端逐个 EXPIRE 会留出进程中断窗口。把 MSETNX 与无条件 EXPIRE 简单放进事务也不安全:即使前者返回 0,后面的过期命令仍会执行,可能改变他人的 Key。条件设置过期必须检查写入结果。
五、带过期的条件创建
下面是固定两个 Key、固定 60 秒 TTL 的 Lua 教学示例。在允许所有相关命令、参数已验证且正常执行的前提下,仅成功创建后才设置过期;失败则不会修改已存在 Key。
redis
EVAL "local ok = redis.call('MSETNX', KEYS[1], ARGV[1], KEYS[2], ARGV[2]); if ok == 1 then redis.call('EXPIRE', KEYS[1], 60); redis.call('EXPIRE', KEYS[2], 60); end; return ok" 2 tutorial:{msetnx}:lease:a tutorial:{msetnx}:lease:b token-a token-b
脚本的 KEYS 必须是两个不同的业务 Key;ARGV 提供对应 Value,示例中的 token 只是演示文本。成功返回 1,冲突返回 0;第二次连续执行(在 60 秒内且无其他改动)通常返回 0。可用 TTL 检查剩余时间。
Lua 原子执行不代表运行时错误回滚:若后续命令因权限等原因报错,前面成功的写入不会自动撤销。生产中应预先验证参数、权限、资源限制及失败恢复,不能把短脚本视为所有故障下的事务提交协议。
六、并发、事务与重试
不要用客户端 EXISTS → 判断 → MSET 模拟本命令,检查后到写入前,别的客户端可能先写入。MSETNX 将这两个步骤放在原子边界内;多个客户端竞争同一组 Key,在无删除、过期、淘汰或故障切换的前提下,最多一个成功创建。
多个 SET ... NX 即使放在 Pipeline 或 MULTI/EXEC 内,也会逐个判断各自条件,可能只创建一部分;它们不等价于 MSETNX 的整批条件。事务能够防止执行期间交错,但不会因某条返回条件失败而自动撤销其他写入。
超时不表示未成功。第一次可能成功却丢失回复,重试返回 0 只能说明"现在至少有一个 Key 存在",不能证明是本请求创建或他人创建。需要可靠重试时写入唯一请求标识,并用适当的原子逻辑验证归属,不能失败后无条件 DEL 全部 Key。
七、不要直接当分布式锁
MSETNX 只提供一批不存在检查和创建,不自带租约、续期、所有者校验、释放协议或故障切换安全保障。无 TTL 的占位可能永久残留,盲目清理可能删除他人的新占位。
单资源短期占位通常从 SET key unique-token NX PX timeout 开始设计,并需原子验证 token 后释放。多资源锁、业务唯一性和跨系统事务远超本命令语义;重要约束仍应由权威数据库的唯一约束或专门协调机制承担。
八、集群与性能
Redis Open Source Cluster 中,全部 Key 必须在同一哈希槽;跨槽会返回 CROSSSLOT。官方多 Key 规则还将 Redis Software 集群中的 MSETNX 列为同槽操作,不能因为某部署的 MSET 支持跨槽就假设本命令也支持。$TRAE_REF
本文的 tutorial:{msetnx}:a 与 tutorial:{msetnx}:b 具有同一 hash tag。把请求拆给不同槽分别执行,会破坏"全部满足才全部写入"的条件,不能作为语义等价的自动重试方案。若强关联字段很多,考虑单个 Hash 或一个整体对象,但必须重新设计其初始化条件。
复杂度 O(N),同时关注参数总字节数、内存、复制与持久化成本。对于高冲突初始化,不要无退避地循环尝试;失败并不表示需要立即重试,也不要为检查存在性发送大量无意义的 Value。
九、Python 客户端示例
假设已安装 redis-py,并有可访问的本地测试 Redis。官方接口 msetnx(mapping) 接收映射;redis-py 常规解析为 True/False,与协议整数 1/0 对应。$TRAE_REF
python
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
a, b, c = [f"tutorial:{{msetnx}}:py:{x}" for x in ("a", "b", "c")]
r.delete(a, b, c) # 仅测试专用 Key
try:
print(r.msetnx({a: "A", b: "B"})) # True
print(r.msetnx({b: "new", c: "C"})) # False
print(r.mget([a, b, c])) # ['A', 'B', None]
finally:
r.delete(a, b, c)
Jedis 对应 jedis.msetnx("k1", "v1", "k2", "v2"),返回 long 1 或 0;不要把返回值解释成成功 Key 数。空映射应在客户端业务层直接处理,而非发送空参数命令。
十、命令对比与使用场景
| 需求 | 方案 |
|---|---|
| 整组 Key 首次初始化,任何冲突都放弃 | MSETNX |
| 无条件覆盖整组值 | MSET |
| 每个 Key 独立初始化,允许部分成功 | 多次 SET ... NX |
| 单 Key 条件创建并指定过期 | SET key value NX EX seconds |
| 同一 Hash 的普通多字段更新 | HSET,不是整对象不存在判断 |
| 查看当前整组值 | MGET;类型不符也可能返回 nil |
典型用途是为新逻辑对象建立一组关联 String、一次性初始化测试夹具、以任意冲突为失败条件的整组占位。不适合"补齐缺失配置":已有一个配置就会阻止所有其他配置写入。
十一、综合练习与排查
以下练习覆盖空字符串、非 String 类型、TTL 保留和已过期 Key。示例 Key 使用专用命名空间,开始前确认可删除。
redis
DEL tutorial:{msetnx}:x tutorial:{msetnx}:y
SET tutorial:{msetnx}:x "" EX 300
MSETNX tutorial:{msetnx}:x X tutorial:{msetnx}:y Y
EXISTS tutorial:{msetnx}:y
TTL tutorial:{msetnx}:x
DEL tutorial:{msetnx}:x
RPUSH tutorial:{msetnx}:x item
MSETNX tutorial:{msetnx}:x X tutorial:{msetnx}:y Y
TYPE tutorial:{msetnx}:x
DEL tutorial:{msetnx}:x
SET tutorial:{msetnx}:x old
EXPIREAT tutorial:{msetnx}:x 1
MSETNX tutorial:{msetnx}:x X tutorial:{msetnx}:y Y
MGET tutorial:{msetnx}:x tutorial:{msetnx}:y
TTL tutorial:{msetnx}:x
DEL tutorial:{msetnx}:x tutorial:{msetnx}:y
预期:空串已存在时返回 0,y 不存在,x 保留到期时刻;List 已存在时仍返回 0,类型为 list;过去的绝对时间令 x 被删除,再次写入返回 1,值为 "X"、"Y",TTL 为 -1。原有 TTL 检查需要在 300 秒内完成。其他章节的 a/b/c 和 lease Key 可按完整名称单独清理。
| 现象 | 排查方向 |
|---|---|
| 只想补写缺失项却什么也没写 | 任意 Key 已存在即整批失败,属设计语义 |
| 空字符串也阻止创建 | 空值不等于不存在 |
| 返回 0 却不知道冲突项 | 可用 TYPE/EXISTS 诊断,但后续查询不是失败时刻的快照 |
| 成功后永不过期 | 命令没有 TTL 选项,需要另行原子设计 |
| 事务失败分支改变了旧 TTL | 不要无条件执行后续 EXPIRE |
| 重试返回 0 | 不能判断首次是否成功,须按请求标识核验 |
| CROSSSLOT | 检查 hash tag,不要拆批冒充整批原子创建 |
总结
MSETNX 全部 Key 不存在才全部写入,成功返回 1,任意存在则返回 0 且全部不写。 它检查的是存在性,不是类型或值;新 Key 无 TTL。涉及过期、锁、重试与跨槽操作时,需要额外设计,不能把条件批量创建等同于完整业务事务。