zoukankan      html  css  js  c++  java
  • Redis实现分布式锁 php

    一、分布式锁的作用:

        redis写入时不带锁定功能,为防止多个进程同时进行一个操作,出现意想不到的结果,so...对缓存进行插入更新操作时自定义加锁功能。

    二、Redis的NX后缀命令

      Redis有一系列的命令,其特点是以NX结尾,NX的意思可以理解为 NOT EXISTS(不存在),SETNX命令 (SET IF NOT EXISTS) 可以理解为如果不存在则插入,Redis分布式锁的实现主要就是使用SETNX命令。

    三、实现原理

        在进程请求执行操作前进行判断,加锁是否成功,加锁成功允许执行下步操作;

        如果不成功,则判断锁的值(时间戳)是否大于当前时间,如果大于当前时间,则获取锁失败不允许执行下步操作;

        如果锁的值(时间戳)小于当前时间,并且GETSET命令获取到的锁的旧值依然小于当前时间,则获取锁成功允许执行下步操作;

        如果锁的值(时间戳)小于当前时间,并且GETSET命令获取到的锁的旧值大于当前时间,则获取锁失败不允许执行下步操作;

    四、$redis->setnx() 设置锁

    $expire = 10;//有效期10秒
    $key = 'lock';//key
    $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期
    $lock = $redis->setnx($key, $value);
    //判断是否上锁成功,成功则执行下步操作
    if(!empty($lock))
    {
         //下步操作...       
    }

    如果返回 1 ,则表示当前进程获得锁,并获得了当前插入/更新缓存的操作权限。

    如果返回 0,表示锁已被其他进程获取,这是进程可以返回结果或者等待当前锁失效再请求。

    五、解决死锁

      如果只用SETNX命令设置锁的话,如果当持有锁的进程崩溃或删除锁失败时,其他进程将无法获取到锁,问题就大了。

    解决方法是在获取锁失败的同时获取锁的值,并将值与当前时间进行对比,如果值小于当前时间说明锁以过期失效,进程可运用Redis的DEL命令删除该锁。

    $expire = 10;//有效期10秒
    $key = 'lock';//key
    $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期
    $status = true;
    while($status)
    {
        $lock = $redis->setnx($key, $value);
        if(empty($lock))
        {
            $value = $redis->get($key);
            if($value < time())
            {
                $redis->del($key);
            }       
        }else{
            $status = false;
            //下步操作....
        }
    }

      但是,简单粗暴的用DEL命令删除锁再SETNX命令上锁也会出现问题。比如,进程1获得锁后崩溃或删除锁失败,这时进程2检测到锁存在当已过期,用DEL命令删除锁并用SETNX命令设置锁,进程3也检测到锁过期,也用DEL命令删除锁也用SETNX命令设置了锁,这时进程2和进程3同时获得了锁。问题大了!

      为了解决这个问题,这里用到了Redis的GETSET命令,GETSET命令在给锁设置新值的同时返回锁的旧值,这里利用了GETSET命令同时获取和赋值的特性,在此期间其他进程无法修改锁的值。

      例如:

        进程1获得锁后操作超时/崩溃/删除锁失败,

        进程2检测到锁已存在,但获取锁的值对比当前时间发现锁已过期,

        进程2通过GETSET命令重新给锁赋予新的值,并获取到的锁的旧值,再次对比锁的旧值与当前时间,如果锁的旧值依然小于当前时间的话,这时进程2就可以忽略进程1余留下的废锁进行下步操作了。

        进程2完成下步操作后返回前应该删除锁,但在删除锁时可以先检测锁是否还未过期,未过期才做删除操作,已过期的就没必要在去删除锁了,因为很有可能其他进程检测到锁过期时已经去获取锁了。

        这里要说明的是,如果有其他进程在进程2之前获取到锁,那么进程2将获取锁失败,但是进程2在用GETSET获取锁的旧值时也赋予了锁新的值,改写了其他进程赋予锁的超时值。看到这大家可能会有疑问了,进程2没获取到锁怎么能改变锁的值呢?是的,进程2改变了锁的原有值,但这一点小小的时间误差带来的影响是可以忽略。

    以下是Redis实现分布式锁的完整PHP代码:

    <?php
    /**
     * 实现Redis分布锁
     */
     
    $key        = 'test';       //要更新信息的缓存KEY
    $lockKey    = 'lock:'.$key; //设置锁KEY
    $lockExpire = 10;           //设置锁的有效期为10秒
     
    //获取缓存信息
    $result = $redis->get($key);
    //判断缓存中是否有数据
    if(empty($result))
    {
        $status = TRUE;
        while ($status)
        {
            //设置锁值为当前时间戳 + 有效期
            $lockValue = time() + $lockExpire;
            /**
             * 创建锁
             * 试图以$lockKey为key创建一个缓存,value值为当前时间戳
             * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
             * 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了
             * @var [type]
             */
            $lock = $redis->setnx($lockKey, $lockValue);
            /**
             * 满足两个条件中的一个即可进行操作
             * 1、上面一步创建锁成功;
             * 2、   1)判断锁的值(时间戳)是否小于当前时间    $redis->get()
             *      2)同时给锁设置新值成功    $redis->getset()
             */
            if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))
            {
                //给锁设置生存时间
                $redis->expire($lockKey, $lockExpire);
                //******************************
                //此处执行插入、更新缓存操作...
                //******************************
     
                //以上程序走完删除锁
                //检测锁是否过期,过期锁没必要删除
                if($redis->ttl($lockKey))
                    $redis->del($lockKey);
                $status = FALSE;
            }else{
                /**
                 * 如果存在有效锁这里做相应处理
                 *      等待当前操作完成再执行此次请求
                 *      直接返回
                 */
                sleep(2);//等待2秒后再尝试执行操作
            }
        }
    } 

    实现分布式锁用到的Redis命令介绍:

    setnx(key, value)

        将key的值设为value,当且仅当key不存在。

        若给定的key已经存在,则SETNX不做任何动作。

        SETNX是”SET if Not eXists”(如果不存在,则SET)的简写。

        返回值:

            设置成功,返回1。

            设置失败,返回0。

        get(key)

        返回key所关联的字符串值。

        如果key不存在则返回特殊值nil。

        假如key储存的值不是字符串类型,返回一个错误,因为GET只能用于处理字符串值。

        返回值:

            key的值。

            如果key不存在,返回nil。

        getset(key, value)

            将给定key的值设为value,并返回key的旧值。

            当key存在但不是字符串类型时,返回一个错误。

            返回值:

                返回给定key的旧值(old value)。

                当key没有旧值时,返回nil。

        expire(key, seconds)

        为给定key设置生存时间。

        当key过期时,它会被自动删除。

        在Redis中,带有生存时间的key被称作“易失的”(volatile)。

        在低于2.1.3版本的Redis中,已存在的生存时间不可覆盖。

        从2.1.3版本开始,key的生存时间可以被更新,也可以被PERSIST命令移除。(详情参见 http://redis.io/topics/expire)。

        返回值:

            设置成功返回1。

            当key不存在或者不能为key设置生存时间时(比如在低于2.1.3中你尝试更新key的生存时间),返回0。

        ttl(key)

        返回给定key的剩余生存时间(time to live)(以秒为单位)。

        返回值:

            key的剩余生存时间(以秒为单位)。

            当key不存在或没有设置生存时间时,返回-1 。

        del(key)

        移除给定的一个或多个key。

        返回值:

            被移除key的数量。

  • 相关阅读:
    css去掉iPhone、iPad默认按钮样式
    STL~Deque简介
    Centos 7 ssh登录速度慢
    C++ delete 两次
    编译gdb 报错 No module named gdb.frames
    gdb 脚本
    转载: CentOS/Linux 解决 SSH 连接慢
    百度经验:Win10查看已存储WiFi密码的两种方法
    git 操作
    Avoiding memory leaks in POSIX thread programming, 多线程避免内存泄漏
  • 原文地址:https://www.cnblogs.com/wenxiong/p/3954174.html
Copyright © 2011-2022 走看看