redis + lua分散ロック(seckillチケット取得シナリオ)

redis分散ロック

1つ、計画

プログラム 実現原理 利点 不利益
redisコマンド 1.ロック:setnxを実行します。成功した場合は、expireを実行して有効期限を追加します
。2。ロック解除:deleteコマンドを実行します。
データベースや分散システムの実装と比較して、実装が簡単なこのソリューションは、最も軽量で最高のパフォーマンスを発揮します。 1. Setnxとは2つの段階、非アトミック操作で実行される期限切れ; setnxが正常に実行が、失敗した実行を期限切れにした場合、デッドロックが発生することが
deleteコマンドが誤って非現在のスレッドによって保持されたロックを削除してもよい2.
3.ブロッキングでありますサポートされていません待機中、再入可能ではありません
RedisLuaスクリプト機能 1.ロック:SET lock_name random_value EX秒NXコマンドを
実行します。2。ロック解除:Luaスクリプトを実行して、ロックが解除されたことを確認します。
上記と同じです。実装ロジックも、シングルポイントの問題を除いてより厳密であり、実稼働環境ではこのソリューションが採用されており、問題は大きくありません。 ロックの再突入をサポートせず、待機のブロックをサポートしません

2つの分散ロック

2.1分散ロックとは何ですか?

分散ロックは、分散システム間または異なるシステム間で共有リソースを制御するロック実装です。特定のリソースが異なるシステム間または同じシステムの異なるホスト間で共有される場合、一貫性を確保するために相互干渉を防ぐために相互排除が必要になることがよくあります。

2.2分散ロックの要件は何ですか

  1. 相互に排他的。いつでも、1人のクライアントだけがロックを保持できます。
  2. デッドロックは発生しません。ロックを保持しているときにクライアントがクラッシュし、アクティブにロックを解除しなくても、他のクライアントが将来ロックできることが保証されます。
  3. トラブルはそれを終わらせるべきです。ロックとロック解除は同じクライアントである必要があります。クライアント自体が他のユーザーによって追加されたロックをロック解除することはできません。つまり、誤ってロックを解除することはできません。
  4. フォールトトレラントです。ほとんどのRedisノードが正常に実行されている限り、クライアントはロックを取得して解放できます。

3、redisコマンドに基づく

3.1フローチャート

画像-20210316144023857

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 lock($ scene、5)-Aが失敗してから6秒後にロックを取得します
ビジネス処理(プログラムタイムアウト8秒)キーは5秒で無効になります ビジネス処理
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);

}

第4に、redisLuaスクリプト機能に基づく

4.1redisコマンドの問題に基づく

if($value==$ID)
{
    
     
    sleep();//在极端的情况下,解锁过程中判断完$value==$ID,出现超时,也会误删除他人的锁。没有原子性,比较和删除是分开的
$res=$this->redis->del($scene);
   }

4.2RedisのLua

RedisはEVALコマンドを使用して、指定されたLuaスクリプトを直接実行します。

EVAL luascript numkeys key [key ...] arg [arg ...]
  • EVAL コマンドのキーワード。
  • luascript Luaスクリプト。
  • numkeysLuaスクリプトは、処理キーの数、実際keyには配列の長さを指定する必要があります
  • keyLuaスクリプトによって、スペースで区切られた0個以上のキーに渡されKEYS[INDEX]、対応する値が取得されます1 <= INDEX <= numkeysここで、
  • argこれは、スクリプトに渡され、スペースで区切られARGV[INDEX]、対応する値を取得するためにLuaスクリプト渡される0個以上の追加パラメーターです1 <= INDEX <= numkeys

4.3Luaの利点

1.ネットワークオーバーヘッドの削減:元の5つのネットワークリクエスト操作は1つのリクエストで完了でき、元の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);

}

おすすめ

転載: blog.csdn.net/weixin_49298265/article/details/114889746