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

设计模式:使用 SETNX 锁定

请注意

  1. 不推荐使用以下模式,而推荐使用 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 持有,而 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 回复

以下之一


RATE THIS PAGE
Back to top ↑