使用redis的比较完美的加锁解锁

使用redis的比较完美的加锁解锁

tags:redis read&write redis加锁和解锁 phpphp


习惯性说一下写这篇文章要说明什么,咱们常常用redis进行加锁操做,目的是为了解决并发可能带来的问题。可是使用redis加锁的方式有多种,本文对常见的几种方式进行解析,并提供一种相对完美的方案。redis

read & write 问题

这是一个经典问题,请看代码:并发

//redis中的某个键自增
    $val = $this->redis->get($key);
    $val ++;
    $this->redis->set($val);

这段代码逻辑没有问题,就是先读取数据,再修改数据,在写回修改,这里是但愿每次访问都递增变量$val的值,但在并发状况下,存在状况是两个进程都读取到了同样的初始值,而后都加1,最后写回Redis,这种状况就会统计数据比实际的少。这个问题应该有许多人遇到过,思考过怎么解决这类问题。这里给出一个统一的解决方案,就是尽可能保证操做的原子性,好比能够用redis的incr命令来实现自增(能够认为redis的命令是原子的)。this

加锁

由上面的问题再进一步,来探讨一个你们经常使用的,为一个操做进行加锁。lua

问题场景以下:有一个商品,每一个用户均可以去修改商品信息。假设用户id分别为6和8的用户对id为123的商品进行操做。操作系统

错误示例1

$key = '123';
    $val = $this->redis->get($key);
    if(!$val){
        $this->redis->set($key,'123');
        $this->redis->expire($key,'4');
        /**此处修改商品信息操做
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '错误提示';
    }

上面这个错误示例,
错误点1:set和expire是分开写的,若是说程序执行中再执行了set()后出现崩溃,则这个就变成了永久锁(虽然这是个小几率事件)。code

错误点2:这个商品中设置的key是商品id,val也是商品id,不少人认为只有一个key就能够了,val是什么无所谓。这就缺乏了锁的标识,没法判断这个锁的拥有者是谁,从而会带来一系列影响以下。队列

  1. 用户1进程获取key对应的val,发现没有锁,因此调用了set,可能在set前,另外一个用户2的进程也发现没有这个锁,也进行set,就形成了两个进程都认为本身获取到了锁的状况,
  2. 而后继续,若是1用户的进程执行完了操做,删除了key,用户2进程未执行完毕,此时因为没法识别是不是本身加的锁,就删除了key,这时再有新的进程进入,检查不到锁,能够当即执行,则有可能和用户2的修改冲突。

针对错误1和错误2的第1点,咱们只须要去除read & write模式就能够解决,解决方案为进程

//同时设置val和过时时间,并使用setnx
    $status = $this->redis->setnx($key,$val,$expireTime);
    if($status){
         /**此处修改商品信息操做
                ******
        **/
        $this->redis->del($key);
    }else{
        echo '错误提示';
    }

setnx,能够在设置时检查是否存在锁不存在则设置并返回1,若是存在不覆盖并返回0。事件

针对错误2第2点,咱们须要为每一个进程设置一个独立的本身能够识别的val,若是一个用户只能开一个进程,这个val能够为用户id,若是一个用户能够设置多个进程,那么必须按照实际车状况采用其余方式来区分,这里咱们以用户id为例,而且在删除的时候只能删除本身的锁。那么这里问题又出现了,若是咱们写成这样:

//同时设置val和过时时间,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此处修改商品信息操做
                ******
        **/
        if($this->redis->get($key) == $userId){
            $this->redis->del($key);
        }
        
    }else{
        echo '错误提示';
    }

这种状况看似没有什么问题,其实否则,你们注意我再设置所得时候,设置了一个过时时间,假如这个时间设置的是4秒,那么若是进程A执行到删除前一刻一不当心超过了4秒,那么这个锁就自动消失了。而另外一个进程B查到没有锁,就加了一把本身的锁,此时进程A执行删除,就把B的锁给删除了(极小几率事件)。

这里解决方案有两种

  1. 设置比较长的expire时间,弊端:设置的太长,占用内存时间长,设置的过短不能彻底解决问题。(可能有人会想不设置过时时间就能够,那么回到最初的错误点,若是程序设置了锁后崩溃了就变成了永久的锁。)
  2. 把对比和删除弄成一个原子操做,这里呢找到了一个方法,就是用redis的eval,把语句变成原子操做。注意redis用的是lua语法,我也是新学的
//同时设置val和过时时间,并使用setnx
    $userId = 2;
    $status = $this->redis->setnx($key,$userId,$expireTime);
    if($status){
         /**此处修改商品信息操做
                ******
        **/
        //由于写这个博客的机器没有装redis,因此没有验证这个语法对不对。请你们见谅
         $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        $result = $this->redis->eval(script,array($key,$val),1);
        if ($result) {
            return true;
        }

    }else{
        echo '错误提示';
    }

这里就把两个操做变成了一个原子操做。解决的加锁和解锁可能出现的问题。

咱们来讲一些题外话拓展:在进程有可能出现冲突的地方,通常咱们叫作临界区(操做系统中也有这个概念,是经过另外一种叫作PV信号量的方式来解决的,其实能够理解为组织等待进程队列,P操做不能获取到资源使用权的则进入等待队列,等待V操做释放资源后,检查是否有等待队列,进行进程释放。固然PV操做也是原子性的。因此说解决类似问题的办法也有必定的类似性)。

欢迎你们评论补充   ---  vinter_he
相关文章
相关标签/搜索