SETNX (已废弃)
从 Redis 2.6.12 版本开始,此命令被视为已废弃。
在迁移或编写新代码时,可以使用带 NX
参数的 SET
命令来替代它。
SETNX key value
- 可用版本
- Redis 开源版 1.0.0
- 时间复杂度
- O(1)
- ACL 类别
-
@write
,@string
,@fast
,
如果 key
不存在,则将 key
的值设置为字符串 value
。在这种情况下,它等同于 SET
。当 key
已有值时,不执行任何操作。SETNX
是 "SET if Not eXists"(如果不存在则设置)的缩写。
示例
设计模式:使用 SETNX
实现锁定
请注意
- 建议使用 Redlock 算法,不推荐以下模式。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 持有。 - 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 将键设置为未来几秒的时间戳,这也不是问题。
为了使此锁定算法更健壮,持有锁的客户端在使用 DEL
解锁之前,应始终检查超时是否已过期,因为客户端故障可能很复杂,不仅仅是崩溃,还可能在某些操作上阻塞很长时间,并在很长时间后(当锁已被其他客户端持有)才尝试执行 DEL
。
RESP2/RESP3 回复
以下之一