PHP 基于redis的分布式锁

<?php
class ProcessRedisLock
{
    /**
     * redis key 前缀
     */
    const KEY_PREFIX = 'PROCESS_REDIS_LOCK:';

    /**
     * 默认超时时间(秒)
     */
    const DEFAULT_TIMEOUT = 5;

    /**
     * 最大超时时间(秒)
     */
    const MAX_TIMEOUT_SETTING = 60;

    /**
     * 随机数的最小值
     */
    const MIN_RAND_NUM = 0;

    /**
     * 随机数的最大值
     */
    const RAND_MAX_NUM = 100000;

    /**
     * 每次取锁间隔毫秒数
     */
    const GET_LOCK_SLEEP_MICRO_SECONDS = 0.1;

    /**
     * @var mixed redis 实例
     */
    private $redisIns;

    /**
     * @var string 锁名
     */
    private $lockName;

    /**
     * @var int 超时时间,不可超过 self::MAX_TIMEOUT_SETTING
     */
    private $timeout;

    /*
    单元测试步骤
    --------------------------------------------------------------------------------------
    1.分别用一个chrome和一个ie,模拟并发请求:
        1)http://xxx/test?queryName=task1&handlerTime=10&lockName=myLocktest&timeout=10
        2)http://xxx/test?queryName=task2&handlerTime=10&lockName=myLocktest&timeout=5
    2.如上
        task1处理耗时10s,取锁超时时间10s
        task2处理耗时10s,取锁超时时间5s
    3.日志结果
        2019-11-22 17:16:30 [task1]: 尝试取锁,超时时间设定10秒
        2019-11-22 17:16:30 [task1]: 获取到锁,唯一标志:PROCESS_REDIS_LOCK:5dd7a76e1bafe37915
        2019-11-22 17:16:32 [task2]: 尝试取锁,超时时间设定5秒
        2019-11-22 17:16:35 [task1]: 释放锁
        2019-11-22 17:16:35 [task2]: 获取到锁,唯一标志:PROCESS_REDIS_LOCK:5dd7a770d0c2b41355
        2019-11-22 17:16:45 [task2]: 释放锁
    --------------------------------------------------------------------------------------

    单元测试代码
    --------------------------------------------------------------------------------------
            // 浏览器模拟并发请求
            // 注意:这里测试的时候如果session以文件存储的话,要避免两个窗口共用一个会话id,因为同样的会话id在session_start()时会锁文件
            // 这样会造成请求阻塞,模拟不了请求并发的情况,所以应该使用两个不同的浏览器(如一个chrome,一个firefox)(同样的浏览器共享cookie也会导致拿到同样的会话id)
            public function testAction()
            {
                $params = $this->getRequest()->getParams();
                $this->requestTask($params['queryName'], $params['handlerTime'], $params['lockName'], $params['timeout']);
            }

            // 模拟取锁、耗时操作、释放锁
            public function requestTask($queryName, $handlerTime, $lockName, $timeout)
            {
                try {
                    // 获取redis实例
                    $processRedisLock = new ProcessRedisLock(redis(), $lockName, $timeout);

                    $this->echoAndSaveInfo($queryName, "尝试取锁,超时时间设定{$timeout}秒");

                    // 取锁
                    $id = $processRedisLock->lock();
                    // 如果到了超时时间还未取到会返回false,则直接抛异常
                    if($id === false){
                        throw new Exception('获取锁失败');
                    }

                    $this->echoAndSaveInfo($queryName, "获取到锁,唯一标志:".$id);

                    // 模拟耗时操作
                    sleep($handlerTime);

                    // 释放锁
                    $processRedisLock->unlock($id);
                    $this->echoAndSaveInfo($queryName, "释放锁");
                } catch (Exception $e) {
                    $this->echoAndSaveInfo($queryName, $e->getMessage());
                    // do something
                }
            }

            // 输出并且记录日志
            public function echoAndSaveInfo($queryName, $content)
            {
                $info = date('Y-m-d H:i:s') . " [{$queryName}]: {$content}" . PHP_EOL;
                echo $info;
                file_put_contents('test.txt', $info . PHP_EOL, FILE_APPEND);
            }
    --------------------------------------------------------------------------------------
    */

    /**
     * ProcessRedisLock constructor.
     * @param $redisIns
     * @param $lockName
     * @param int $timeout
     * @throws Exception
     */
    public function __construct($redisIns, $lockName, $timeout = self::DEFAULT_TIMEOUT)
    {
        if (!$redisIns) {
            new Exception('The redis instance is empty');
        }

        if(!$lockName){
            throw new Exception('Lock name invalid');
        }

        // 校验超时时间
        $timeout = intval($timeout);
        if (!($timeout > 0 && $timeout <= self::MAX_TIMEOUT_SETTING)) {
            throw new Exception('The timeout interval is (0,' . self::MAX_TIMEOUT_SETTING . ']');
        }

        $this->redisIns = $redisIns;
        $this->lockName = $lockName;
        $this->timeout = $timeout;
    }

    /**
     * 加锁
     * @return bool
     * @Date 2019/11/22
     */
    public function lock()
    {
        // redis key
        $key = $this->getRedisKey();

        // 唯一标志
        $id = $this->getId();

        // 超时时间
        $endTime = time() + $this->timeout;

        // 循环取锁
        while (time() < $endTime) {
            // 尝试加锁,若给定的 key 已经存在,则 SETNX 不做任何动作。
            if ($this->redisIns->setnx($key, $id)) {
                // 设置过期时间,防止程序异常退出没有解锁导致死锁
                $this->redisIns->expire($key, $this->timeout);
                // 返回唯一标志,用于解锁
                return $id;
            }
            usleep(self::GET_LOCK_SLEEP_MICRO_SECONDS);
        }

        return false;
    }


    /**
     * 解锁
     * @param string $id 唯一标志,加锁成功时返回
     * @return bool
     * @Date 2019/11/22
     */
    public function unlock($id)
    {
        $key = self::getRedisKey();

        // 如果锁的值与没有被修改
        if ($this->redisIns->get($key) == $id) {
            // 开始事务
            $this->redisIns->multi();
            // 释放该锁
            $this->redisIns->del($key);
            // 执行
            $this->redisIns->exec();
            return true;
        } else {
            return false;
        }
    }

    /**
     * 获取redis key
     * @return string
     * @Date 2019/11/22
     */
    public function getRedisKey()
    {
        return self::KEY_PREFIX . $this->lockName;
    }

    /**
     * 获取唯一标志位
     * @Date 2019/11/22
     */
    public function getId()
    {
        return uniqid(self::KEY_PREFIX) . mt_rand(self::MIN_RAND_NUM, self::RAND_MAX_NUM);
    }
}

 

posted on 2019-11-22 17:36  多多明明  阅读(369)  评论(0编辑  收藏  举报