概述
锁是保护一些共享的资源,这个资源通常会发生竞争。
场景
以下代码,如果5秒内有多个请求,就会超卖
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$num = $redis->get('num');
if ($num < 1) {
// 两个客户端并发执行的时候,因为前面的库存还没执行到增加num
sleep(5);// 模拟购买逻辑时间
$store = $redis->incr('num');
var_dump($store);
} else {
echo '已经卖完';
}
单机锁
上述的问题,我们可以用文件锁的方法,不过这文件通常只能放在一台机器中,如果有多台机器的话,就会有问题
if (flock($fp,LOCK_EX)) {// 加悲观排他锁阻塞等待
fwrite($fp,"lock success\n");
sleep(5);
flock($fp,LOCK_UN);// 解锁
} else {
echo "文件被其它进程占用";
}
fclose($fp);
我们还可以通过数据库的行锁进行锁定
select * from erp_storage where id = 1 for update;
分布式锁
分布式锁的基本条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。例如下例中,请求a的业务逻辑时间太长,导致锁缓存过期失效,请求b就获取到了锁,然后这个时候请求a业务逻辑执行完成了,要释放锁了,就把请求b的锁给释放了
以下代码符合上述条件
<?php
class Lock {
private $redis;
private $clientUniqueId;
public function __construct()
{
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function lock($scene = '小米11库存', $expire = 5, $retry = 5, $sleep = 1)
{
$res = false;
while ($retry > 0) { // 获取锁,要多次尝试,不能一次返回
$value = session_create_id(); // 获取唯一字符
$this->clientUniqueId = $value;
$res = $redis->set($scene, $value, [
'NX', // 不存在则设置,也就是没有锁就加锁
'EX' => $expire, // 防止死锁,万一加锁后没有解锁,会自动释放锁
]);
if ($res) { // 加锁成功后返回
break;
}
echo "尝试获取锁";
sleep($sleep);
$retry--;
}
}
public function unlock($scene)
{
// $value = $this->redis->get($scene);
// if ($value == $this->clientUniqueId) {// 不能删除掉别人的锁
// 正好锁过期了,这个时候,客户端b能获取到锁,这里就会把客户端b的锁给删除
// sleep(5);//极端情况下,可能这里会有io阻塞,导致把别人的锁给删除了
// $this->redis->del($scene);
// }
// KEYS类似全局变量
$script = <<<LUA
local key=KEYS[1]
local value=ARGV[1]
if(redis.call('get','key') == value)
then
return redis.call('del',key)
end
LUA;
// 为了保证原子性,get和del一起执行,不能分开执行
$this->redis->eval($script,[$scene,$this->clientUniqueId]);
}
}