page contents

php与Redis实现分布式锁

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

attachments-2020-06-IBfi6eZo5ef69d9c909f8.png

一、分布式锁的作用:

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的数量。



attachments-2020-06-ZQzoaVBu5ef69db242a19.jpg

  • 发表于 2020-06-27 09:15
  • 阅读 ( 601 )
  • 分类:PHP开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1474 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章