谈谈Redis的SETNX

谈谈Redis的SETNX
发表于2015-09-14	

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,能够利用它来实现锁的效果,不过不少人没有意识到 SETNX 有陷阱!

好比说:某个查询数据库的接口,由于调用量比较大,因此加了缓存,并设定缓存过时后刷新,问题是当并发量比较大的时候,若是没有锁机制,那么缓存过时的瞬间,大量并发请求会穿透缓存直接查询数据库,形成雪崩效应,若是有锁机制,那么就能够控制只有一个请求去更新缓存,其它的请求视状况要么等待,要么使用过时的缓存。

下面以目前 PHP 社区里最流行的 PHPRedis 扩展为例,实现一段演示代码:

<?php

$ok = $redis->setNX($key, $value);

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

缓存过时时,经过 SetNX  获取锁,若是成功了,那么更新缓存,而后删除锁。看上去逻辑很是简单,惋惜有问题:若是请求执行由于某些缘由意外退出了,致使建立了锁可是没有删除锁,那么这个锁将一直存在,以致于之后缓存再也得不到更新。因而乎咱们须要给锁加一个过时时间以防不测:

<?php

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

?>

由于 SetNX 不具有设置过时时间的功能,因此咱们须要借助 Expire 来设置,同时咱们须要把二者用 Multi/Exec 包裹起来以确保请求的原子性,以避免 SetNX 成功了 Expire 却失败了。 惋惜还有问题:当多个请求到达时,虽然只有一个请求的 SetNX 能够成功,可是任何一个请求的 Expire 却均可以成功,如此就意味着即使获取不到锁,也能够刷新过时时间,若是请求比较密集的话,那么过时时间会一直被刷新,致使锁一直有效。因而乎咱们须要在保证原子性的同时,有条件的执行 Expire,接着便有了以下 Lua 代码:

local key   = KEYS[1]
local value = KEYS[2]
local ttl   = KEYS[3]

local ok = redis.call('setnx', key, value)
 
if ok == 1 then
  redis.call('expire', key, ttl)
end
 
return ok

没想到实现一个看起来很简单的功能还要用到 Lua 脚本,着实有些麻烦。其实 Redis 已经考虑到了你们的疾苦,从 2.6.12 起,SET 涵盖了 SETEX 的功能,而且 SET 自己已经包含了设置过时时间的功能,也就是说,咱们前面须要的功能只用 SET 就能够实现。

<?php

$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

如上代码是完美的吗?答案是还差一点!设想一下,若是一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,致使在缓存更新过程当中,锁就失效了,此时另外一个请求会获取锁,但前一个请求在缓存更新完毕的时候,若是不加以判断直接删除锁,就会出现误删除其它请求建立的锁的状况,因此咱们在建立锁的时候须要引入一个随机值:

<?php

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();

    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}

?>

如此基本实现了单机锁,假如要实现分布锁,请参考:Distributed locks with Redis,这里就不深刻讨论了,总结:避免掉入 SETNX 陷阱的最好方法就是永远不要使用它!
相关文章
相关标签/搜索