Redisは分散ロックphpを実現する


一、分布式ロックの役割:
redis書き込み時にロック機能を持たないで、複数のプロセスが同時に1つの操作を行うことを防止するために、予想外の結果が出て、so...キャッシュの挿入更新操作時にロック機能をカスタマイズします.
 
二、RedisのNX接尾辞コマンド
Redisには一連のコマンドがあり、NXで終わるのが特徴で、NXの意味はNOT EXISTS(存在しない)と理解でき、SETNXコマンド(SET IF NOT EXISTS)は存在しなければ挿入すると理解でき、Redis分散ロックの実現は主にSETNXコマンドを使用することである.
 
三、実現原理
プロセスが操作を要求する前に、ロックが成功したかどうかを判断し、ロックが成功した場合、次の操作を実行することができる.
成功しない場合は、ロックの値(タイムスタンプ)が現在時刻より大きいかどうかを判断し、現在時刻より大きい場合は、ロックの取得に失敗し、次の操作は許可されません.
ロックの値(タイムスタンプ)が現在時刻よりも小さく、GETSETコマンドによって取得されたロックの古い値が現在時刻よりも小さい場合、ロックの取得に成功すると、次の操作を実行できます.
ロックの値(タイムスタンプ)が現在時刻よりも小さく、GETSETコマンドによって取得されたロックの古い値が現在時刻よりも大きい場合、ロックの取得に失敗して次のステップの操作は許可されません.
 
四、$redis->setnx()設定ロック
$expire = 10;//   10 
$key = 'lock';//key
$value = time() + $expire;//    = Unix    +      
$lock = $redis->setnx($key, $value);
//
if(!empty($lock))
{
     //    ...       
}

1を返すと、現在のプロセスがロックされ、現在のキャッシュの挿入/更新の操作権限が取得されていることを示します.
0を返すと、ロックが他のプロセスによって取得されたことを示します.これは、プロセスが結果を返すか、現在のロックの失効後の再要求を待つことができることです.
五、デッドロックの解決
SETNXコマンドのみでロックを設定すると、ロックを持つプロセスがクラッシュしたり、ロックの削除に失敗したりすると、他のプロセスがロックを取得できなくなり、問題が大きくなります.
解決策は、ロックの取得に失敗したときにロックの値を取得し、値を現在の時間と比較し、値が現在の時間より小さい場合はロックが期限切れで失効したことを示し、プロセスはRedisのDELコマンドを使用してロックを削除することができる.
$expire = 10;//   10 
$key = 'lock';//key
$value = time() + $expire;//    = Unix    +      
$status = true;
while($status)
{
    $lock = $redis->setnx($key, $value);
    if(empty($lock))
    {
        $value = $redis->get($key);
        if($value < time())
        {
            $redis->del($key);
        }       
    }else{
        $status = false;
        //    ....
    }
}

ただし、簡単に乱暴にDELコマンドでロックを削除してSETNXコマンドでロックしても問題が発生します.例えば、プロセス1がロックを取得してクラッシュしたり、ロックの削除に失敗したりした場合、プロセス2はロックの存在を検出し、DELコマンドでロックを削除し、SETNXコマンドでロックを設定し、プロセス3もロックの期限切れを検出し、DELコマンドでロックを削除してもSETNXコマンドでロックが設定されている場合、プロセス2とプロセス3は同時にロックを取得する.問題が大きい!
この問題を解決するために,ここではRedisのGETSETコマンドを用い,GETSETコマンドはロックに新しい値を設定すると同時にロックの古い値を返すが,ここではGETSETコマンドが同時に取得して値を付与する特性を利用しており,その間他のプロセスではロックの値を変更することができない.
例:
プロセス1がロックを取得した後、操作がタイムアウト/クラッシュ/ロックの削除に失敗しました.
プロセス2は、ロックが存在することを検出しますが、ロックの値を取得すると、現在の時間と比較してロックが期限切れであることがわかります.
プロセス2は、GETSETコマンドによりロックに新たな値を付与し、取得したロックの古い値を再度比較し、ロックの古い値と現在時間を再度比較し、ロックの古い値が現在時間よりも小さい場合、プロセス2は、プロセス1の残りの古いロックを無視して次の操作を行うことができる.
プロセス2は、次のステップの操作が完了してから戻る前にロックを削除する必要がありますが、ロックを削除するときは、ロックが期限切れであるかどうかを検出し、期限切れでない場合は削除する必要はありません.他のプロセスがロックの期限切れを検出したときにロックを取得した可能性が高いからです.
ここで説明するのは、他のプロセスがプロセス2の前にロックを取得した場合、プロセス2はロックの取得に失敗するが、プロセス2はGETSETでロックの古い値を取得する際にもロックの新しい値を付与し、他のプロセスがロックを付与したタイムアウト値を書き換えたものである.これを見て疑問に思うかもしれませんが、プロセス2がロックを取得していないのに、どのようにロックの値を変えることができますか?はい、プロセス2はロックの元の値を変更しましたが、この小さな時間誤差の影響は無視できます.
以下はRedisが分散ロックを実現する完全なPHPコードである.
php
/**
 *   Redis   
 */
 
$key        = 'test';       //        KEY
$lockKey    = 'lock:'.$key; //   KEY
$lockExpire = 10;           //        10 
 
//      
$result = $redis->get($key);
//          
if(empty($result))
{
    $status = TRUE;
    while ($status)
    {
        //           +    
        $lockValue = time() + $lockExpire;
        /**
         *    
         *    $lockKey key      ,value       
         *   setnx()          key          
         *   ,                             
         * @var [type]
         */
        $lock = $redis->setnx($lockKey, $lockValue);
        /**
         *                 
         * 1、         ;
         * 2、   1)     (   )            $redis->get()
         *      2)              $redis->getset()
         */
        if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))
        {
            //        
            $redis->expire($lockKey, $lockExpire);
            //******************************
            //      、      ...
            //******************************
 
            //         
            //       ,        
            if($redis->ttl($lockKey))
                $redis->del($lockKey);
            $status = FALSE;
        }else{
            /**
             *               
             *                     
             *          
             */
            sleep(2);//  2         
        }
    }
} 

 
分散ロックの実装に使用されるRedisコマンドの説明:
setnx(key, value)
キーの値をvalueに設定し、キーが存在しない場合にのみ使用します.
与えられたkeyが既に存在する場合、SETNXは何もしない.
SETNXは、「SET if Not eXists」(存在しない場合はSET)の略語である.
戻り値:
設定に成功し、1を返します.
設定に失敗し、0を返します.
 
    get(key)
keyに関連付けられた文字列値を返します.
keyが存在しない場合は、特殊な値nilを返します.
keyが格納した値が文字列タイプでない場合、GETは文字列値の処理にのみ使用できるため、エラーが返されます.
戻り値:
keyの値.
キーが存在しない場合はnilを返します.
 
    getset(key, value)
与えられたkeyの値をvalueに設定し、keyの古い値を返します.
keyが存在するが文字列タイプではない場合、エラーが返されます.
戻り値:
与えられたkeyの古い値(old value)を返します.
キーに古い値がない場合はnilを返します.
 
    expire(key, seconds)
所与のkeyに生存時間を設定します.
keyが期限切れになると自動的に削除されます.
Redisでは,生存時間を持つkeyを「失われやすい」(volatile)と呼ぶ.
2.1を下回る.3バージョンのRedisでは、既存の生存時間は上書きできません.
2.1から.3バージョンからkeyの生存時間を更新したり、PERSISTコマンドで削除したりすることができます.(詳細はhttp://redis.io/topics/expire).
戻り値:
設定は正常に1を返しました.
キーが存在しないか、キーに生存時間を設定できない場合(例えば2.1.3未満でキーの生存時間を更新しようとした場合)、0を返します.
 
    ttl(key)
与えられたkeyの残りの生存時間(time to live)を秒単位で返す.
戻り値:
keyの残りの生存時間(秒単位).
keyが存在しないか、生存時間が設定されていない場合は、-1を返します.
 
    del(key)
指定した1つ以上のkeyを削除します.
戻り値:
削除されたkeyの数.
 
転載先:https://www.cnblogs.com/wenxiong/p/3954174.html