redis分布式锁
一、方案
方案 | 实现原理 | 优点 | 缺点 |
---|---|---|---|
redis命令 | 1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令 |
实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好 | 1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入 |
redis Lua脚本能力 | 1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证 |
同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。 | 不支持锁重入,不支持阻塞等待 |
二、分布式锁
2.1 什么是分布式锁?
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
2.2 分布式锁需要具备哪些条件
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
三、基于redis命令
3.1流程图
3.1代码实现
<?php
class Lock
{
private $redis;
public function __construct($redis)
{
$this->redis=$redis;
}
/**
* @param string $scene 加锁的场景,业务
* @param int $TTL 加锁的过期时间
* @param int $tryTime 尝试获取锁的次数
* @param int $sleep 休眠的时间
* @return bool
*/
//加锁
public function lock($scene='secKill',$TTL=5,$tryTime=5,$sleep=1000000){
$res=false;
while ($tryTime-->0){
$value=rand();//生成不重复随机数
//只有一个用户能加锁,NX当键不存在的时候才能创建成功。互斥锁
//EX设置键的过期时间,避免redis出意外出现死锁。
$res=$this->redis->set($scene,$value,['NX','EX'=>$TTL]);
if ($res){
var_dump('加锁成功');
break;//加锁成功跳出循环
}
else{
var_dump('尝试获取锁');
}
usleep($sleep);
}
return $res;
}
//解锁
public function unlock($scene){
$res=false;
$res=$this->redis->del($scene);
if($res){
var_dump('解锁成功');
}
else{
var_dump('解锁失败');
}
return $res;
}
}
$redis=new redis();
$redis->connect('127.0.0.1',6379);
$scene='secKill';
$lock=new Lock($redis);
if($lock->lock($scene)){
var_dump('执行事务');
sleep(4);
$lock->unlock($scene);
}
3.3解锁的优化
3.3.1问题
请求A | 请求B |
---|---|
加锁lock($scene,5) | 加锁lock($scene,5)–在A失效后第6s获取锁 |
业务处理(程序超时8s)key在第5s失效 | 业务处理 |
第8s解锁unlock()–此时删除B的锁 |
要求:解铃还须系铃人–在上述代码加一个lockID(用于标识当前加锁的请求)
3.3.2优化:
<?php
class Lock
{
private $redis;
protected $lockID; //当前加锁的id,防止误删锁
public function __construct($redis)
{
$this->redis=$redis;
}
/**
* @param string $scene 加锁的场景,业务
* @param int $TTL 加锁的过期时间
* @param int $tryTime 尝试获取锁的次数
* @param int $sleep 休眠的时间
* @return bool
*/
//加锁
public function lock($scene='secKill',$TTL=5,$tryTime=5,$sleep=1000000){
$res=false;
while ($tryTime-->0){
$value=rand(); //生成不重复随机数
$ID=$this->lockID[$scene]=$value;
//只有一个用户能加锁,NX当键不存在的时候才能创建成功。互斥锁
//EX设置键的过期时间,避免redis出意外出现死锁。
$res=$this->redis->set($scene,$value,['NX','EX'=>$TTL]);
if ($res){
var_dump('加锁成功');
break;//加锁成功跳出循环
}
else{
var_dump('尝试获取锁');
}
usleep($sleep);
}
return $res;
}
//解锁
public function unlock($scene){
$res=false;
$ID=$this->lockID[$scene];
$value=$this->redis->get($scene);
if($value==$ID){
$res=$this->redis->del($scene);
}
if($res){
var_dump('解锁成功');
}
else{
var_dump('解锁失败');
}
return $res;
}
}
$redis=new redis();
$redis->connect('127.0.0.1',6379);
$scene='secKill';
$lock=new Lock($redis);
if($lock->lock($scene)){
var_dump('执行事务');
sleep(4);
$lock->unlock($scene);
}
四、基于redis Lua脚本能力
4.1基于redis命令问题
if($value==$ID)
{
sleep();//在极端的情况下,解锁过程中判断完$value==$ID,出现超时,也会误删除他人的锁。没有原子性,比较和删除是分开的
$res=$this->redis->del($scene);
}
4.2 Redis中的Lua
Redis中使用EVAL
命令来直接执行指定的Lua脚本。
EVAL luascript numkeys key [key ...] arg [arg ...]
EVAL
命令的关键字。luascript
Lua 脚本。numkeys
指定的Lua脚本需要处理键的数量,其实就是key
数组的长度。key
传递给Lua脚本零到多个键,空格隔开,在Lua 脚本中通过KEYS[INDEX]
来获取对应的值,其中1 <= INDEX <= numkeys
。arg
是传递给脚本的零到多个附加参数,空格隔开,在Lua脚本中通过ARGV[INDEX]
来获取对应的值,其中1 <= INDEX <= numkeys
。
4.3Lua的优点
1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
2.原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
3.复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。
4.4代码实现分布式锁
<?php
class Lock_Lua
{
private $redis;
protected $lockID; //当前加锁的id,防止误删锁
public function __construct($redis)
{
$this->redis=$redis;
}
/**
* @param string $scene 加锁的场景,业务
* @param int $TTL 加锁的过期时间
* @param int $tryTime 尝试获取锁的次数
* @param int $sleep 休眠的时间
* @return bool
*/
public function lock($scene='secKill',$TTL=5,$tryTime=5,$sleep=1000000){
$res=false;
while ($tryTime-->0){
$value=rand(); //生成不重复随机数
$ID=$this->lockID[$scene]=$value;
//只有一个用户能加锁,NX当键不存在的时候才能创建成功。互斥锁
//EX设置键的过期时间,避免redis出意外出现死锁。
$res=$this->redis->set($scene,$value,['NX','EX'=>$TTL]);
if ($res){
var_dump('加锁成功');
break;//加锁成功跳出循环
}
else{
var_dump('尝试获取锁');
}
usleep($sleep);
}
return $res;
}
//解锁
public function unlock($scene){
$script=<<<LUA
local key=KEY[1]
local value=ARGV[1]
if(redis.call('get',key)==value)
then
return redis.call('del',key)
else
return 0
end
LUA;
if(isset($this->lockID[$scene])){
$ID=$this->lockID[$scene];
return $this->redis->eval($script,[$scene,$ID]);
}
}
}
$redis=new redis();
$redis->connect('127.0.0.1',6379);
$scene='secKill';
$lock=new Lock_Lua($redis);
if($lock->lock($scene)){
var_dump('执行事务');
sleep(4);
$lock->unlock($scene);
}