好记性不如烂笔头,记录一下,方便以后自己查阅
一、redis单实例情况下
<?php
class RedisLock {
private $redis;
private $lockKey; // 锁键名
private $expire; // 锁失效时间(秒)
private $uniqueId;
public function __construct($redis, $lockKey, $expire)
{
$this->redis = $redis;
$this->lockKey = $lockKey;
$this->expire = $expire;
}
/**
* @param $retryTime int 抢锁失败重试次数
* @param $retryDelay int 重试延迟时间(微秒)
* @return boolean
*/
public function lock($retryTime, $retryDelay)
{
$this->uniqueId = uniqid();
while (true) {
// set key value [EX seconds] [PX milliseconds] [NX|XX] 需要redis 2.6.12版本以上
// 如果键名不存在则设置值并指定过期时间,原子操作
if ($this->redis->set($this->lockKey, $this->uniqueId, ['nx', 'ex' => $this->expire])) {
return true;
}
// 没抢到锁,睡眠一段时间后重试,如果重试了指定次数还没抢到则返回false
if ($retryTime-- <= 0) {
break;
}
usleep($retryDelay);
}
return false;
}
public function unlock()
{
if (!$this->uniqueId) {
return;
}
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';
// eval方法需要redis 2.6.0版本以上,原子操作
return $this->redis->eval($script, [$this->lockKey, $this->uniqueId], 1);
}
}
/************** 测试代码 ******************/
try {
$redis = new Redis();
$result = $redis->connect('127.0.0.1', 6379);
if ($result) {
$redis->auth('123456');
$redis->select(0);
$lock = new RedisLock($redis, 'lockKey', 10);
$flag = $lock->lock(3, 10000); // 重试3次,每次间隔10毫秒
if ($flag) {
echo "抢到了锁,执行业务逻辑...\n";
$lock->unlock();
echo "释放锁...\n";
} else {
echo "系统繁忙,请稍候再试!\n";
}
$redis->close();
} else {
echo 'connect redis failed';
}
} catch (RedisException $e) {
echo 'RedisException: ' . $e->getMessage();
}
二、redis多实例情况下(各实例之间完全独立)
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 = 3)
{
$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);
}
}
/************** 测试代码 ******************/
$servers = [
['127.0.0.1', 6379, 0.01],
['127.0.0.1', 6389, 0.01],
['127.0.0.1', 6399, 0.01],
];
$redLock = new RedLock($servers);
$lock = $redLock->lock('my_resource_name', 1000);
if ($lock) {
// 抢到了锁
}
// 无论抢到锁与否,最后都要调用unlock方法
$redLock->unlock($lock);
RedLock类代码来自redis官方 https://redis.io/topics/distlock