SETNX (已弃用)
从 Redis 版本 2.6.12 开始,此命令被视为已弃用。
在迁移或编写新代码时,可以使用 SET
命令的 NX
参数来代替。
SETNX key value
- 可用时间
- 1.0.0
- 时间复杂度
- O(1)
- ACL 类别
-
@write
,@string
,@fast
,
如果 key
不存在,则将 key
设置为保存字符串 value
,在这种情况下,它等同于 SET
。当 key
已经保存了值时,不会执行任何操作。SETNX
是 "SET if Not eXists" 的缩写。
示例
设计模式:使用 SETNX
锁定
请注意
- 不推荐使用以下模式,而推荐使用 Redlock 算法,它只比实现复杂一点点,但提供更好的保证并且是容错的。
- 我们仍然记录旧模式,因为某些现有实现将此页面链接作为参考。此外,它是一个有趣的示例,展示了如何使用 Redis 命令来构建编程原语。
- 尽管如此,即使假设只有一个实例的锁定原语,从 2.6.12 版本开始,就可以使用
SET
命令获取锁,以及简单的 Lua 脚本释放锁,来创建更简单的锁定原语,等效于此处讨论的原语。此模式在SET
命令页面中进行了说明。
也就是说,SETNX
可以使用,而且在历史上也曾被用作锁定原语。例如,要获取 foo
键的锁,客户端可以尝试以下操作
SETNX lock.foo <current Unix time + lock timeout + 1>
如果 SETNX
返回 1
,则客户端获取了锁,将 lock.foo
键设置为锁不再有效时的 Unix 时间。稍后,客户端将使用 DEL lock.foo
来释放锁。
如果 SETNX
返回 0
,则该键已被其他客户端锁定。如果这是一个非阻塞锁,我们可以返回给调用者,或者进入循环,不断尝试获取锁,直到成功或者某个超时时间到期。
处理死锁
在上面的锁定算法中,存在一个问题:如果客户端出现故障、崩溃或无法释放锁,会发生什么情况?可以检测到这种情况,因为锁键包含一个 Unix 时间戳。如果此时间戳等于当前 Unix 时间,则锁不再有效。
发生这种情况时,我们不能简单地对键调用 DEL
来删除锁,然后尝试发出 SETNX
,因为这里存在竞争条件,当多个客户端检测到锁已过期并尝试释放它时。
- C1 和 C2 读取
lock.foo
以检查时间戳,因为它们在执行SETNX
后都收到了0
,因为锁仍然被 C3 持有,而 C3 在持有锁后崩溃了。 - C1 发送
DEL lock.foo
- C1 发送
SETNX lock.foo
,并且成功了 - C2 发送
DEL lock.foo
- C2 发送
SETNX lock.foo
,并且成功了 - 错误:由于竞争条件,C1 和 C2 都获取了锁。
幸运的是,可以使用以下算法来避免此问题。让我们看看 C4(我们的正常客户端)如何使用此良好算法
-
C4 发送
SETNX lock.foo
以获取锁 -
崩溃的客户端 C3 仍然持有它,因此 Redis 将向 C4 回复
0
。 -
C4 发送
GET lock.foo
以检查锁是否已过期。如果未过期,它将休眠一段时间,然后从头开始重试。 -
相反,如果锁已过期,因为
lock.foo
的 Unix 时间早于当前 Unix 时间,则 C4 尝试执行GETSET lock.foo <current Unix timestamp + lock timeout + 1>
-
由于
GETSET
的语义,C4 可以检查key
中存储的旧值是否仍然是过期时间戳。如果是,则获取了锁。 -
如果另一个客户端,例如 C5,比 C4 快,并且使用
GETSET
操作获取了锁,那么 C4 的GETSET
操作将返回一个未过期的时间戳。C4 将从第一步重新开始。注意,即使 C4 在未来几秒内设置了 key,这也不是问题。
为了使此锁定算法更加健壮,持有锁的客户端在使用 DEL
解锁 key 之前,应始终检查超时是否已过期,因为客户端故障可能很复杂,不仅是崩溃,还可能在某些操作上阻塞很长时间,并尝试在很长时间后发出 DEL
(此时 LOCK 已被另一个客户端持有)。
RESP2/RESP3 回复
以下之一