Redis 基础教程

Redis 命令

Redis 高级教程

Redis 笔记

Redis SETNX 命令

Redis 字符串(Strings)底层数据结构及所有相关命令 Redis 字符串(Strings)底层数据结构及所有相关命令


Redis SETNX 命令只有在指定的 key 不存在的情况下,将 key 设置为 value 值,这时等同于 SET 命令;若指定的 key 存在,则什么也不做。SETNX 是“SET if Not eXists”的缩写。

命令格式

SETNX key value

可用版本:>=1.0.0

时间复杂度:O(1)

命令返回值

若设置成功,返回 1。

若设置失败,则返回 0。

示例

redis> EXISTS cpc            # cpc 不存在
(integer) 0
redis> SETNX cpc "ctr"       # cpc 设置成功     
(integer) 1
redis> SETNX cpc "pv"        # 尝试覆盖 cpc,失败      
(integer) 0
redis> GET cpc               # 没有被覆盖
"ctr"

SETNX 实现分布式锁

如上所述,SETNX 只有在 key 不存在的时才设置成功,结合 Redis 服务单线程的性质,可以实现分布式锁的效果,但是其实现有一些陷阱,需要开发者了解。

SETNX 最典型的使用场景是,某个查询数据库的接口,因为调用量比较大,所以加了缓存,并设定缓存过期后刷新,问题是当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,大量并发请求会穿透缓存直接查询数据库,造成雪崩效应,如果有锁机制,那么就可以控制只有一个请求去更新缓存,其它的请求视情况要么等待,要么使用过期的缓存。

以 PHP 代码为例:

<?php

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

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

?>

上述实例的步骤如下:

  • 每个请求都先执行 SETNX 命令,按照设计,只有一个请求获取锁;
  • 获取锁的请求,更新缓存;
  • 然后再释放锁(删除对应的 key)。

上述逻辑看上去逻辑非常简单,但会有问题:如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新

于是乎我们尝试给锁加一个过期时间以防不测:

<?php

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

?>

由于 SETNX 不能设置过期时间,所以我们需要借助 EXPIRE 命令来设置过期时间,同时我们需要把两个命令用 Redis 事务相关的 MULTI 和 EXEC 命令包裹起来以确保请求的原子性。

但是还会存在问题:当多个请求到达时,虽然能保证只有一个请求的 SETNX 可以成功,但是任何一个请求的 EXPIRE 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求过于密集的话,那么过期时间会一直被刷新,导致锁一直释放不了

于是乎我们需要在保证原子性的同时,有条件的执行 EXPIRE 操作:

<?php

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

   if ($ok) {
       $redis->expire($key, $ttl);
       $cache->update();
   }

?>

值得庆幸的是,Redis 从 2.6.12 版本开始,可以创建一个更加简单的加锁原语,那就是 SET 命令,它涵盖了 SETNX 功能和 SETEX 功能,也就是说,我们前面需要的功能只用 SET 就可以实现。

<?php

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

   if ($ok) {
       $cache->update();
       $redis->del($key);
   }
   //这里$ttl设置的时间要恰到好处,要大于请求执行的时间。
?>

上述实例看起来很完美,但还是存在小问题:如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况

所以我们在创建锁的时候需要引入一个唯一值:

<?php

   $uuid = uuid();
   //对查询进行加锁(一般是唯一数据项,例如订单号)
   $ok = $redis->set($key, $uuid, array('nx', 'ex' => $ttl));
   if($ok){
       //相当于业务这里一般来处理逻辑
       $cache->update();//更新缓存
    
       if ($redis->get($key) == $uuid) {//保证删自己的,别因为请求时间长而删掉别人的内容
             $redis->del($key);//处理完之后 释放锁
       }
   }

?>