- 用法
-
SETNX key value
- 复杂度
- O(1)
- 始于
- 1.0.0
- ACL 类别
- @string, @write, @fast
- 不建议使用以下模式,而推荐使用 Redlock 算法,它在实现上只稍微复杂一点,但提供了更好的保证,并且具有容错性。
- 我们仍然记录这种旧模式,因为某些现有实现将此页面作为参考。此外,这也是一个有趣的例子,说明如何使用 Valkey 命令来构建编程原语。
- 即便假设是单实例锁原语,从 2.6.12 版本开始,也可以创建一个更简单的锁原语,其功能与此处讨论的相同,使用
SET
命令获取锁,以及一个简单的 Lua 脚本来释放锁。该模式已在SET
命令页面中详细说明。 - 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 发送
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 将键的过期时间设置为未来几秒,这也不是问题。
如果 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
进行锁定
请注意
尽管如此,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
,因为这里存在竞态条件,即多个客户端检测到锁已过期并试图释放它。
幸运的是,可以使用以下算法来避免此问题。让我们看看 C4(我们的正常客户端)如何使用这个好算法
为了使此锁定算法更加健壮,持有锁的客户端在通过 DEL
解锁键之前,应始终检查超时是否已过期,因为客户端故障可能很复杂,不仅仅是崩溃,还可能在某些操作上阻塞很长时间,并试图在很长时间后(当锁已被其他客户端持有)发出 DEL
命令。
RESP2/RESP3 回复
以下之一