PHP 一个redis锁案例

<?php

class RedLock {
    private $retryDelay;
    private $retryCount;
    private $clockDriftFactor = 0.01;
    private $quorum;
    private $servers = array();
    private $instances = array();

    function __construct(array $servers, $retryDelay = 200, $retryCount = 0) {
        $this->servers = $servers;
        $this->retryDelay = $retryDelay;
        $this->retryCount = $retryCount;
        $this->quorum = min(count($servers), (count($servers) / 2 + 1));
    }

    public function lock($resource, $ttl) {
        $this->initInstances();
        $token = uniqid();
        $retry = $this->retryCount;
        do {
            $n = 0;
            $startTime = microtime(true) * 1000;
            foreach ($this->instances as $instance) {
                if ($this->lockInstance($instance, $resource, $token, $ttl)) {
                    $n++;
                }
            }
            # 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 ($n >= $this->quorum && $validityTime > 0) {
                return [
                    'validity' => $validityTime,
                    'resource' => $resource,
                    'token' => $token,
                ];
            } else {
                foreach ($this->instances as $instance) {
                    $this->unlockInstance($instance, $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) {
        $this->initInstances();
        $resource = $lock['resource'];
        $token = $lock['token'];
        foreach ($this->instances as $instance) {
            $this->unlockInstance($instance, $resource, $token);
        }
    }

    private function initInstances() {
        if (empty($this->instances)) {
            foreach ($this->servers as $server) {
                list($host, $port, $timeout) = $server;
                $redis = new \Redis();
                $redis->connect($host, $port, $timeout);
                $this->instances[] = $redis;
            }
        }
    }

    private function lockInstance($instance, $resource, $token, $ttl) {
        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
    }

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

简易版实现

use Ramsey\Uuid\Uuid;

class RedisLock {

    private $redis;

    /**
     * RedisLock constructor.
     * @param $redis Redis
     */
    public function __construct($redis) {
        $this->redis = $redis;
    }

    /**
     * @param $lock_name
     * @param int $acquire_time
     * @param int $lock_timeout
     * @return bool|string
     *
     * @throws Exception
     */
    public function acquire_lock($lock_name, $acquire_time = 0, $lock_timeout = 10) {
        $identifier = (string)Uuid::uuid4();
        $lock_name = 'lock:' . $lock_name;
        $lock_timeout = intval(ceil($lock_timeout));
        if ($acquire_time == 0) {
            $result = $this->got_lock($lock_name, $identifier, $lock_timeout);
        } else {
            $end_time = time() + $acquire_time;
            while (time() < $end_time) {
                $result = $this->got_lock($lock_name, $identifier, $lock_timeout);
                if ($result != '1') {
                    //sleep 0.1 sec
                    usleep(100000);
                }
            }
        }
        return $result == '1' ? $identifier : false;
    }

    private function got_lock($lock_name, $identifier, $lock_timeout) {
        $script = <<<luascript
                 local result = redis.call('setnx',KEYS[1],ARGV[1]);
                    if result == 1 then
                        redis.call('expire',KEYS[1],ARGV[2])
                        return 1
                    elseif redis.call('ttl',KEYS[1]) == -1 then
                       redis.call('expire',KEYS[1],ARGV[2])
                       return 0
                    end
                    return 0
luascript;
        $result = $this->redis->evaluate($script, array($lock_name, $identifier, $lock_timeout), 1);
        return $result;
    }

    /**
     * @param $lock_name
     * @param $identifier
     * @return bool
     */
    public function release_lock($lock_name, $identifier) {
        $lock_name = 'lock:' . $lock_name;
        while (true) {
            $script = <<<luascript
                local result = redis.call('get',KEYS[1]);
                if result == ARGV[1] then
                    if redis.call('del',KEYS[1]) == 1 then
                        return 1;
                    end
                end
                return 0
luascript;
            $result = $this->redis->evaluate($script, array($lock_name, $identifier), 1);
            if ($result == 1) {
                return true;
            }
            break;
        }
        return false;
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注