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 mykey "Hello" SETNX mykey "World" GET mykey

设计模式:使用 SETNX 实现锁定

请注意

  1. 建议使用 Redlock 算法,不推荐以下模式。Redlock 算法实现起来稍微复杂一些,但提供更好的保证且容错。
  2. 尽管如此,我们仍然记录了旧模式,因为某些现有实现将此页面作为参考。此外,它也是一个有趣的示例,说明如何使用 Redis 命令来构建编程原语。
  3. 无论如何,即使假设是单实例锁定原语,从 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 回复

以下之一


评价此页面
返回顶部 ↑