redis+lua distributed lock (seckill ticket grabbing scenario)

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

  1. Mutually exclusive. At any time, only one client can hold the lock.
  2. 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.
  3. 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.
  4. 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

image-20210316144023857

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 EVALcommands to directly execute the specified Lua script.

EVAL luascript numkeys key [key ...] arg [arg ...]
  • EVAL The keyword of the command.
  • luascript Lua script.
  • numkeysLua scripts need to specify the number of processing key, in fact, keythe length of the array.
  • keyLua script passed to zero or more keys, separated by spaces, by Lua script KEYS[INDEX]to obtain the corresponding value, wherein the 1 <= INDEX <= numkeys.
  • argIt is zero or more additional parameters passed to the script, separated by spaces, and passed in the Lua script ARGV[INDEX]to get the corresponding value 1 <= 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);

}

Guess you like

Origin blog.csdn.net/weixin_49298265/article/details/114889746