<?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; } }