原理
- Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系
- Redis中可以使用setNx命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 setNx不做任何动作
- SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
- 返回值:设置成功,返回 1 。设置失败,返回 0 。
- 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
- 为了防止获取锁后程序出现异常,导致其他线程/进程调用setNx命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除
利用Redis实现分布式锁的步骤
1.连接Redis
在PHP中,连接Redis使用的是PHPRedis扩展,需要安装扩展后才能使用。具体安装方法可以参考官方文档。
2.获取锁
实现一个获取锁的函数如下:
protected function lock($lock_key, $expire_time = 5)
{
$redis = new Redis();
$redis->connect('localhost', 6379); // 连接Redis
$micro_second = 1000000;
$timeout = 10 * $micro_second; //等待锁超时时间
while($timeout >= 0)
{
$microtime = microtime(true);
$timeout -= $micro_second;
$current_lock_time = $microtime + $expire_time + 1; //锁过期时间
if($redis->setnx($lock_key, $current_lock_time)) //获取锁成功
{
$redis->expire($lock_key, $expire_time); //设置过期时间,防止死锁
return $current_lock_time;
}
//检查锁是否过期
$lock_time = $redis->get($lock_key);
if($lock_time < $microtime)
{
$new_lock_time = $microtime + $expire_time + 1; //设置新的过期时间
$old_lock_time = $redis->getset($lock_key, $new_lock_time); //获取旧的过期时间并设置新的过期时间
if($old_lock_time < $microtime) //锁已经过期,获取锁成功
{
$redis->expire($lock_key, $expire_time); //设置过期时间,防止死锁
return $new_lock_time;
}
}
//等待一段时间后再次尝试获取锁
usleep(10000); //等待10毫秒
}
return false;
}
该函数的作用是获取指定key的锁,若获取成功,则返回锁的过期时间;若获取失败,则返回false。
3.释放锁
不论是由于获取锁成功后执行完操作后,还是等待一段时间后获取锁失败,都需要释放锁。实现一个释放锁的函数如下:
protected function unlock($lock_key, $current_lock_time)
{
$redis = new Redis();
$redis->connect('localhost', 6379); // 连接Redis
$lock_time = $redis->get($lock_key);
if($lock_time == $current_lock_time) //判断是否为当前持有锁的客户端
$redis->del($lock_key); //释放锁
}
注意事项
1.锁的过期时间要根据实际情况合理设置。如果锁的过期时间太短,可能会导致锁失效或者频繁获取不到锁;如果锁的过期时间太长,可能会导致锁过期时间过长,影响系统性能。
2.在设置过期时间时,一定要注意防止死锁。如有一个客户端获取到锁之后,由于意外退出或崩溃等原因导致锁没有被释放,就会影响其他客户端获取锁,造成死锁问题。
3.由于分布式锁的实现依赖于客户端的唯一标识,所以不同客户端需要使用不同的标识,否则可能会导致一个客户端释放其他客户端的锁。在PHP中,可以使用PHP_SESSION_ID或者IP地址/进程ID等作为客户端的唯一标识。