RedisLock.php 2.94 KB
<?php

namespace common\components\redislock;

use Yii;
use yii\base\Component;
use yii\redis\Connection;
use Exception;
use function class_exists;
use function is_object;
use function uniqid;
use function microtime;
use function mt_rand;
use function floor;
use function usleep;

/**
 * 实现Redis事务锁
 * Created by PhpStorm.
 * User: weigong
 * Date: 18/5/9
 * Time: 下午3:01
 */
class RedisLock extends Component
{
    public $redis;
    public $retryDelay;
    public $retryCount;
    private $clockDriftFactor = 0.01;

    public function init()
    {
        if (!class_exists('\yii\redis\Connection')) {
            throw new Exception('the extension yii\redis\Connection does not exist ,you need it to operate redis ,you can run "composer require yiisoft/yii2-redis" to gei it!');
        }
        parent::init();

        if (!is_object($this->redis)) {
            $this->redis = Yii::createObject($this->redis);
            if(!$this->redis || !$this->redis instanceof Connection){
                throw new Exception('Redis class injected must be instanceof of "yii\redis\Connection"');
            }
        }
    }

    public function lock($resource, $ttl)
    {
        $token = uniqid();
        $retry = $this->retryCount;

        do {
            $hasGetLock = false;

            $startTime = microtime(true) * 1000;

            if ($this->lockInstance($resource, $token, $ttl)) {
                $hasGetLock = true;
            }

            # Add 2 milliseconds to the drift to account for Redis expires
            # precision, which is 1 millisecond, plus 1 millisecond min drift
            # for small TTLs.
            $drift = ($ttl * $this->clockDriftFactor) + 2;
            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
            if ($hasGetLock && $validityTime > 0) {
                return [
                    'validity' => $validityTime,
                    'resource' => $resource,
                    'token'    => $token,
                ];
            } else {
                $this->unlockInstance($resource, $token);
            }

            // Wait a random delay before to retry
            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
            usleep($delay * 1000);
            $retry--;
        } while ($retry > 0);

        return false;
    }

    public function unlock(array $lock)
    {
        $resource = $lock['resource'];
        $token    = $lock['token'];

        $this->unlockInstance($resource, $token);
    }

    private function lockInstance($resource, $token, $ttl)
    {
        return $this->redis->set($resource, $token, 'PX', $ttl, 'NX');
    }

    private function unlockInstance($resource, $token)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';

        return $this->redis->eval($script, 1, $resource, $token);
    }
}