redis distributed lock
One, plan
Program | Realization principle | advantage | Disadvantage |
---|---|---|---|
redis command | 1. Lock: execute setnx, if successful then execute expire to add expiration time 2. Unlock: execute delete command |
Simple to implement, compared to the implementation of databases and distributed systems, this solution is the lightest and has the best performance | 1. Setnx and expire are executed in 2 steps, non-atomic operations; if setnx is executed successfully, but expire execution fails, deadlock may occur 2. The delete command may delete locks held by non-current threads by mistake 3. Blocking is not supported Waiting, not reentrant |
Redis Lua scripting capabilities | 1. Lock: execute SET lock_name random_value EX seconds NX command 2. Unlock: execute Lua script to verify when the lock is released |
Same as above; the implementation logic is also more rigorous, except for single-point problems, the production environment adopts this solution, and the problem is not big. | Does not support lock reentry, does not support blocking wait |
Two, distributed locks
2.1 What is a distributed lock?
Distributed lock is a lock implementation that controls shared resources between distributed systems or different systems. If a resource is shared between different systems or different hosts of the same system, mutual exclusion is often required to prevent each other Interference to ensure consistency.
2.2 What are the requirements for distributed locks
- Mutually exclusive. At any time, only one client can hold the lock.
- No deadlock will occur. Even if a client crashes while holding the lock and does not actively unlock it, it can be guaranteed that other clients can lock it in the future.
- The trouble should end it. Locking and unlocking must be the same client, the client itself cannot unlock the locks added by others, that is, it cannot be unlocked by mistake.
- It is fault-tolerant. As long as most Redis nodes are running normally, the client can acquire and release locks.
Three, based on redis command
3.1 Flow chart
3.1 Code implementation
<?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 Optimization of unlocking
3.3.1 Problem
Request A | Request B |
---|---|
Lock($scene,5) | Lock lock($scene,5)-Acquire the lock at the 6th second after A fails |
Business processing (program timeout 8s) key becomes invalid at 5s | Business processing |
8s unlock unlock()-delete B's lock at this time |
Requirement: To unlock the ring, you must also tie the ringer – add a lockID to the above code (used to identify the current lock request)
3.3.2 Optimization:
<?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);
}
Fourth, based on redis Lua scripting capabilities
4.1 Based on the redis command problem
if($value==$ID)
{
sleep();//在极端的情况下,解锁过程中判断完$value==$ID,出现超时,也会误删除他人的锁。没有原子性,比较和删除是分开的
$res=$this->redis->del($scene);
}
4.2 Lua in Redis
Redis uses EVAL
commands to directly execute the specified Lua script.
EVAL luascript numkeys key [key ...] arg [arg ...]
EVAL
The keyword of the command.luascript
Lua script.numkeys
Lua scripts need to specify the number of processing key, in fact,key
the length of the array.key
Lua script passed to zero or more keys, separated by spaces, by Lua scriptKEYS[INDEX]
to obtain the corresponding value, wherein the1 <= INDEX <= numkeys
.arg
It is zero or more additional parameters passed to the script, separated by spaces, and passed in the Lua scriptARGV[INDEX]
to get the corresponding value1 <= INDEX <= numkeys
.
4.3 Advantages of Lua
1. Reduce network overhead: The original 5 network request operations can be completed with one request, and the logic of the original 5 requests is completed on the redis server. The use of scripts reduces the network round-trip delay.
2. Atomic operation: Redis will execute the entire script as a whole without being inserted by other commands in the middle.
3. Reuse: The script sent by the client will be permanently stored in Redis, which means that other clients can reuse the script without using code to complete the same logic.
4.4 Code to implement distributed locks
<?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);
}