SETNX 已废弃

用法
SETNX key value
复杂度
O(1)
始于
1.0.0
ACL 类别
@string, @write, @fast

如果 key 不存在,则将 key 的值设置为字符串 value。在这种情况下,它等同于 SET 命令。当 key 已经持有值时,不执行任何操作。SETNX 是 “SET if Not eXists”(如果不存在则设置)的缩写。

示例

127.0.0.1:6379> SETNX mykey "Hello"
(integer) 1
127.0.0.1:6379> SETNX mykey "World"
(integer) 0
127.0.0.1:6379> GET mykey
"Hello"

设计模式:使用 SETNX 进行锁定

请注意

  1. 不建议使用以下模式,而推荐使用 Redlock 算法,它在实现上只稍微复杂一点,但提供了更好的保证,并且具有容错性。
  2. 我们仍然记录这种旧模式,因为某些现有实现将此页面作为参考。此外,这也是一个有趣的例子,说明如何使用 Valkey 命令来构建编程原语。
  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 仍持有锁,因此 Valkey 将向 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 回复

以下之一