page contents

Redis并发锁

使用场景: 只要有可能由于并发而产生错误数据的地方都需要使用并发锁,而鉴于redis的速度优势和强大的功能,并发锁首先考虑用redis来实现。 首先需要了解关键的几个redis命令: SETNX key valu...

使用场景:


只要有可能由于并发而产生错误数据的地方都需要使用并发锁,而鉴于redis的速度优势和强大的功能,并发锁首先考虑用redis来实现。


首先需要了解关键的几个redis命令:

  1. SETNX key value
    只有在 key 不存在时才能成功设置 key 的值。
    设置成功,返回 1 。
    设置失败,返回 0 。

  2. GET key
    获取指定的 key 的值。
    当 key 不存在时,返回 nil ,否则,返回 key 的值。
    如果 key 不是字符串类型,那么返回一个错误。

  3. GETSET key value
    将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
    功能同SET key value 和GET key 两个命令一起用。


死锁:

在某一段时间段内,有多于一个的进程同时拥有锁,会产生死锁问题。


加锁流程图:

attachments-2019-12-kW2dICxO5e01d6030a2c1.png


加锁流程解读:

  1. 开始;
  2. 用SETNX命令来加锁,并获取返回值。
  3. 如果返回1,说明加锁成功,返回当前锁,程序结束;
  4. 如果返回0,说明之前有其他进程已经加过锁了,但是不确定加锁的那个进程是否已崩溃,这时候需要GET命令获取到上次的锁,并判断是否已超时。
  5. 如果未超时,则当前进程等待一段时间,再重复第4步;
  6. 如果已超时,则用GETSET重新加锁并返回新锁之前的锁。
  7. 判断GETSET获取到的旧锁是否已经超时(这个判断尤为重要,因为调用GETSET命令之前,或许其他进程也已经执行到了第6步,所以,GETSET命令获取到的有可能是其他进程上得锁);
  8. 如果未超时,则继续执行第5步。
  9. 如果已超时,则把GETSET的新锁返回。程序结束;

注意:第7步,由于GETSET命令同时执行了set new_value和get old_value两个操作,并对old_value加上超时判断,可以有效避免死锁情况的发生


下面给出php的实现代码:

<?php
/**
 * redis并发锁
 * @author rockyfc
 *
 */
class RedisLockHelper
{
    private $_key;

    /**
     * @var int 过期时长
     */
    private $_timeout;

    /**
     * @var int 锁的过期时间戳
     */
    private $_expires;

    /**
     * @var Redis
     */
    private $_redis;


     /**
     * RedisLockHelper constructor.
     * @param string|array $key 键,请注意,生成键名称最好不要跟其他业务的键名称有重复
     * @param int $timeout 过期时长,默认10秒
     */
    public function __construct($key, $timeout = 10)
    {
        if(is_array($key)){
            $key = serialize($key);
        }
        $this->_key = md5($key);
        $this->_timeout = $timeout;
        $this->_redis = \Yii::$app->redis;
    }
    /**
     * 加锁
     * @param callable|null $callback 业务回调
     * @return bool|mixed
     * @throws \Exception
     */
    public function lock(callable $callback = null)
    {

        $lock = 0;

        // 获取锁
        while ($lock != 1) {
            $now = time();
            $this->_expires = $now + $this->_timeout + 1;
            $lock = $this->_redis->setnx($this->_key, $this->_expires);

            if ($lock == 1 or ($now > $this->_redis->get($this->_key)) and $now > $this->_redis->getset($this->_key, $this->_expires)) {
                //获取到锁,跳出
                break;
            } else {
                //休眠2毫秒
                usleep(2000);
            }
        }

        //执行业务代码
        if ($callback) {
            try{
                $rs = call_user_func($callback, $this->_expires);
                $this->unlock();
                return $rs;
            }catch (\Exception $e){
                $this->unlock();
                throw $e;
            }

        }


        //返回锁
        return true;

    }

    /**
     * 释放锁。
     *
     * 注意:在释放锁之前,需要先判断锁是否已经超时,如果已经超时的话,那么锁可能已由其他进程获得,
     * 这时直接执行释放操作会导致把其他进程已获得的锁也释放掉
     *
     * @return mixed
     */
    public function unlock()
    {
        $now = time();
        if ($now < $this->_expires) {
            $this->_redis->del($this->_key);
        }
    }
}


使用锁:

//有多个条件生成key
$locker = new RedisLockHelper([
            __METHOD__,
            'some_conditions...',
            'some conditions...',
]);
$locker->lock();

//业务 code ...
    
$locker->unlock();
    


或者通过回调函数的形式使用:

//有多个条件生成key
$locker = new RedisLockHelper([
    __METHOD__,
    'some_conditions...',
    'some conditions...',
]);
$locker->lock(function () {
    //业务 code ...
});

    


以上,便是一个redis并发锁的实现流程以及php的实现。



  • 发表于 2019-12-24 17:14
  • 阅读 ( 721 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

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