日常开发中我们少不了要用分布式锁,保证资源访问的互斥,下面将一步步给出最佳的解决方案:
1、redis2.6以上可以用lua脚本实现加锁和设置有效期的原子性操作,但是redis2.6以下不支持lua脚本,只能采取get+setex或setnx+expire两种方案,前者忽略掉了get和setex之间的并发,而后者是忽视了ex失败的问题。
2、可以升级到新版本,支持set同时设置nx和ex参数,实现如下(加锁失败的自旋没有实现,可根据业务自行实现):
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'key:1';
$random = random(); //自定义的随机值函数
$ok = $redis->set($key, 1, ['nx', 'ex'=>$expire])
if( $ok ){
doSomethings(); //执行业务
if( $redis->get($key) == $random ) {
//用随机值,防止当前线程执行时间过长,有效期到了之后自动删除当前锁,其他线程获得锁并加锁,当前线程超时结束后误删其他线程对当前资源加的锁
del($key);
}
}
?>
3、上面的锁还有问题,假如在获取对比随机值和删除之间宕机了,就只能等待锁自动失效。并且在高并发场景下,应该尽量减少redis命令的次数,所以在释放锁时,我们可以用lua实现原子命令,改动后的代码如下:
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'key:1';
$random = uniqid(); //假如并发极高,同微秒还是会生成相同的id,可以通过参数附加随机前缀,也可以参考我的另一篇文章用雪花算法生成uuid
$expire = 10;
$ok = $redis->set($key, $random, ['nx', 'ex' => $expire]);
if ($ok) {
sleep(5); //模拟执行业务
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';
$redis->eval($script, [$key, $random], 1);
}
127.0.0.1:6379> monitor
1594377461.656859 [0 127.0.0.1:58550] "SET" "key:1" "5f0844f5a0357" "ex" "10" "nx"
1594377466.663522 [0 127.0.0.1:58550] "EVAL" "\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n " "1" "key:1" "5f0844f5a0357"
1594377466.663617 [0 lua] "GET" "key:1"
1594377466.663646 [0 lua] "DEL" "key:1"
4、上面的代码你以为就万无一失了?假如我们的redis服务器宕机了怎么办,这时有人可能会脱口而出,主从复制+哨兵啊,仔细想想此时会不会出现新问题?
肯定是会的,A线程在master加锁后,假如在锁的有效期內,主节点宕机,从节点升级为主节点,此时B线程又来获取锁,这时新的主节点是没有锁的,就会导致B线程加锁成功并获取资源,这就出现了锁竞态。那么这个问题如何解决呢?redis的开发者已经给我们提供了现成的解决方法:redlock。
核心思想:使用多个redis主节点(要大于等于3个),加锁时所有节点都加锁,只有半数以上的节点加锁成功才算成功,释放锁时所有节点都释放。
redlock代码:https://github.com/ronnylt/redlock-php/blob/master/src/RedLock.php