简介
多线程情况下访问一些共享资源需要加锁,否则就会导致数据错乱的问题
分布式锁可以通过 DB,Redis,Zk 等方式实现,本节主要介绍 php 使用 Redis 实现分布式锁
set 命令
setnx key value 设置一个值,当 key 已经存在时,返回 flase,代表失败
使用 setnx 实现分布锁有个缺陷,setnx 操作无法设置 key 的 ttl,需要配合 exprie key ttl 一起使用
好在 set 命令就集成了 nx 和 ex 操作 set key name NX PX 10000
$redis = new Redis();
$redis->connect('127.0.0.1', 6380);
$rs = $redis->set('testnx', 123, ['nx', 'ex' => 10]);
var_dump($rs);
Redlock
set 命令还有一个问题,当你要提前释放这个锁的时候,使用 expire key 0 或者使用 del key
如果 expire 或者 del 命令发送了阻塞,锁自动失效,这时候 B 获取了锁,expire/del 命令到达,导致 B 获取的锁失效
Redlock 在加锁的时候 value 值要保证唯一性,在释放锁的时候要验证 value 是否和申请锁时 value 是否一致
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],
];
$redLock = new RedLock($servers);
while (true) {$lock = $redLock->lock('test', 10000);
if ($lock) {print_r($lock);
$redLock->unlock(['resource' => 'test', 'token' => '5d1c123121538']);
} else {print "Lock not acquired\n";}
}
后续
Redis 分布式锁还有没有问题?
解决方法:引入版本号
参考:https://time.geekbang.org/col…