- SETNX key value
将key的值设为value,当且仅当key不存在。
若给定的key已经存在,则SETNX不做任何动作。
SETNX是”SET if Not eXists”(如果不存在,则SET)的简写。
- 时间复杂度:
- O(1)
- 返回值:
-
数字,只有以下两种值:
1
如果key被set0
如果key没有被set
//SETNX echo '<br><br>SETNX<br>'; $redis->EXISTS('job'); # job不存在 //bool(false); $redis->SETNX('job', "programmer"); # job设置成功 //bool(true) $redis->SETNX('job', "code-farmer"); # job设置失败 //bool(false) echo $redis->GET('job'); # 没有被覆盖 //"programmer"
设计模式(Design pattern): 将SETNX用于加锁(locking)
SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key)foo加锁,客户端可以尝试以下方式:
SETNX lock.foo <current Unix time + lock timeout + 1>
如果SETNX返回1,说明客户端已经获得了锁,key设置的unix时间则指定了锁失效的时间。之后客户端可以通过DEL lock.foo来释放锁。
如果SETNX返回0,说明key已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。
处理死锁(deadlock)
上面的锁算法有一个问题:如果因为客户端失败、崩溃或其他原因导致没有办法释放锁的话,怎么办?
这种状况可以通过检测发现——因为上锁的key保存的是unix时间戳,假如key值的时间戳小于当前的时间戳,表示锁已经不再有效。
但是,当有多个客户端同时检测一个锁是否过期并尝试释放它的时候,我们不能简单粗暴地删除死锁的key,再用SETNX上锁,因为这时竞争条件(race condition)已经形成了:
- C1和C2读取lock.foo并检查时间戳,SETNX都返回0,因为它已经被C3锁上了,但C3在上锁之后就崩溃(crashed)了。
- C1向lock.foo发送DEL命令。
- C1向lock.foo发送SETNX并成功。
- C2向lock.foo发送DEL命令。
- C2向lock.foo发送SETNX并成功。
- 出错:因为竞争条件的关系,C1和C2两个都获得了锁。
幸好,以下算法可以避免以上问题。来看看我们聪明的C4客户端怎么办:
- C4向lock.foo发送SETNX命令。
- 因为崩溃掉的C3还锁着lock.foo,所以Redis向C4返回0。
- C4向lock.foo发送GET命令,查看lock.foo的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
- 另一方面,如果lock.foo内的unix时间戳比当前时间戳老,C4执行以下命令:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
- 因为GETSET的作用,C4可以检查看GETSET的返回值,确定lock.foo之前储存的旧值仍是那个过期时间戳,如果是的话,那么C4获得锁。
- 如果其他客户端,比如C5,比C4更快地执行了GETSET操作并获得锁,那么C4的GETSET操作返回的就是一个未过期的时间戳(C5设置的时间戳)。C4只好从第一步开始重试。
注意,即便C4的GETSET操作对key进行了修改,这对未来也没什么影响。(这里是不是有点问题?C4的确是可以重试,但C5怎么办?它的锁的过期被C4修改了。——译注)警告
为了让这个加锁算法更健壮,获得锁的客户端应该常常检查过期时间以免锁因诸如DEL等命令的执行而被意外解开,因为客户端失败的情况非常复杂,不仅仅是崩溃这么简单,还可能是客户端因为某些操作被阻塞了相当长时间,紧接着DEL命令被尝试执行(但这时锁却在另外的客户端手上)。