setNX,是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回 1 , 设置失败时返回 0 。能够利用它来实现锁的效果,可是不少人在使用的过程当中都有一些问题没有考虑到。

例如某个查询数据库的接口由于请求量比较大因此加了缓存,并设定缓存过时后刷新。当并发量比较大而且缓存过时的瞬间,大量并发请求会直接查询数据库致使雪崩。若是使用锁机制来控制只有一个请求去更新缓存就能避免雪崩的问题。下面是不少人下意识想到的加锁方法php

$rs = $redis->setNX($key, $value);
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁
$redis->del($key);
}

经过 setNX 获取锁,若是成功了则更新缓存而后删除锁。其实这里有一个严重的问题:若是更新缓存的时候由于某些缘由意外退出了,那么这个锁就不会被删除而一直存在,以致于缓存再也得不到更新。为了解决这个问题有人可能会想到给锁设置一个过时时间,以下redis

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

由于 setNX 不具有设置过时时间的功能,因此要借助 Expire 来设置,同时须要使用 Multi/Exec 来确保请求的原子性,以避免 setNX 成功了 Expire 却失败了。这样还有问题:当多个请求到达时,虽然只有一个请求的 setNX 能够成功,可是任何一个请求的 Expire 却均可以成功,这就意味着即使获取不到锁也能够刷新过时时间,致使锁一直有效,仍是解决不了上面的问题。显然 setNX 知足不了需求,Redis从 2.6.12 起,SET 涵盖了 SETEX 的功能, SET 自己又包含了设置过时时间的功能,因此使用 SET 就能够解决上面遇到的问题数据库

$rs = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁
$redis->del($key);
}

到这一步其实仍是有问题的,若是一个请求更新缓存的时间比锁的有效期还要长,致使在缓存更新过程当中锁就失效了,此时另外一个请求就会获取到锁,但前一个请求在缓存更新完毕的时候,直接删除锁的话就会出现误删其它请求建立的锁的状况。因此要避免这种问题,能够在建立锁的时候须要引入一个随机值并在删除锁的时候加以判断缓存

$rs = $redis->set($key, $random, array('nx', 'ex' => $ttl));
if ($rs) {
//处理更新缓存逻辑
// ......
//先判断随机数,是同一个则删除锁
if ($redis->get($key) == $random) {
$redis->del($key);
}
}