はじめに
今日はロックについて話し、実装を示します。 リソースのシングルスレッド使用を保証する必要がある場合、各開発者は繰り返し問題に直面しています。多くの場合、このようなロックを確保するために、特別なファイルの作成にスキームが使用され、その存在がリソースの使用の事実を決定します。
このアプローチは実装が非常に簡単ですが、いくつかの欠点があります。 欠点を特定できます:
- 多数のスレッドによるブロックの100%保証の欠如;
- ブロッキングは1つのサーバー内で機能します。
- そして最も不愉快なことは、何らかの理由でロックを設定したプロセスがそれを削除しなかった場合、残りのプロセスは手動でまたは他の方法でこのロックが解除されるまでこのリソースにアクセスできないことです。
いつロックが必要ですか?
ニーズが異なるたびに、基本的に、同時に繰り返されるアクションを排除し、ある種のリソースとの一貫した作業を確保し、均一な負荷を確保することになります。自分でそれを行う方法は?
正しいロックを実装するには、 原子性とトランザクション 性の原則を理解する必要があります。 この記事ではそれらを説明しません。 これらのトピックに関する多くの情報がすでにインターネット上にあります。実装中に、ロックを操作する際の基本的な操作を決定しました。
- 一定時間ロックする
- 指定した時間後にロック解除
- ロックを延長
- ロックが傍受された場合に対処する
実際、ロックプロバイダーとして使用されるものはそれほど重要ではありません。ファイル、mysql、memcache、またはその他の便利なツールを使用できます。
Redisは私たちに近いので、Redisに上記の欠点のないロックメカニズムを実装しました。
ロックがどのように行われるか
本日、ほぼすべての行にコメントを追加し、使用例を示した「現状のまま」の実装を提供します。実装はYiiフレームワークのプロジェクトで使用され、Rediskaライブラリを介してRedisに接続するために使用されます。 しかし、RediskaのようなYiiのタイは小さいため、このコードは任意のPHPプロジェクトで使用できます。
それでは、楽しい部分に移りましょう。
ベースロッククラス
<?php /** * * */ class Lock { /** * * * @param string $key * @return string */ static protected function getKey( $key ) { return $key; } /** * true, * * @param string $key - * @param float $timeWait - * @param float $maxExecuteTime - * @return bool */ static public function getLock( $key, $timeWait = 0, $maxExecuteTime = 3600 ) { throw new Lock_Exception('Not defined method getLock'); return false; } /** * - * * @return string */ static protected function getCurrentProcessId() { static $myProcessId = false; if ( $myProcessId === false ) { $uname = posix_uname(); $mypid = getmypid(); $myProcessId = $uname['nodename'] . '_' . $mypid; } return $myProcessId; } /** * * * @param string $key - * @param float $delayAfter - * @return bool */ static public function releaseLock( $key, $delayAfter = 0 ) { throw new Lock_Exception('Not defined method releaseLock'); return false; } /** * * * @param string $key - * @param float $timeProlongate - * @return bool - false, */ static public function prolongate( $key, $timeProlongate ) { throw new Lock_Exception('Not defined method prolongate'); return false; } } class Lock_Exception extends Exception { } class Timeout_Lock_Exception extends Lock_Exception { } class LostLock_Timeout_Lock_Exception extends Timeout_Lock_Exception { }
クラスの再ロック
このクラスでは、Redisを使用してロックが作成されます。 <?php /** * Redis * */ class RedisLock extends Lock { /** * noSQL * * @param string $key * @return string */ static protected function getKey( $key ) { // lock@ return 'lock@'.$key; } /** * true, * * @param string $key - * @param float $timeWait - * @param float $maxExecuteTime - * @param integer $policy - : * 0 - , , * 1 - 10, * @return bool */ static public function getLock( $key, $timeWait = 0, $maxExecuteTime = 3600, $policy = 0 ) { /** * , */ $timeStop = microtime(true) + $timeWait; // Yii Redis Rediska $rediska = Yii::app()->rediskaConnection->connect(); while ( true ) { $currentTime = microtime(true); if ( $policy == 0 ) { /** * , */ $expireAt = $rediska->getFromHash( self::getKey($key), 'expireAt' ); /** * , , , * */ if ( $expireAt > $timeStop ) { return false; } /** * , , */ elseif ( $expireAt > $currentTime ) { usleep( 1000000 * intval($expireAt - $currentTime) ); $currentTime = microtime(true); } } elseif ( $policy == 1 ) { /** * , */ $expireAt = $rediska->getFromHash( self::getKey($key), 'expireAt' ); while ( $expireAt > $timeStop || $expireAt > $currentTime ) { usleep( 10000 ); /** * , */ $expireAt = $rediska->getFromHash( self::getKey($key), 'expireAt' ); $currentTime = microtime(true); if ( $currentTime >= $timeStop ) { return false; } } } $getLock = false; /** * * getConnectionByKeyName , Redis */ $transaction = $rediska->transaction( $rediska->getConnectionByKeyName( self::getKey($key) ) ); $transaction->watch( self::getKey($key) ); $arData = $rediska->getHash( self::getKey($key) ); // $daddy = isset($arData['daddy']) ? $arData['daddy'] : ''; // $expireAt = isset($arData['expireAt']) ? $arData['expireAt'] : 0; /** * , , * */ if ( $daddy != self::getCurrentProcessId() && $expireAt < $currentTime ) { $transaction->setToHash( self::getKey($key), array( 'daddy' => self::getCurrentProcessId(), 'expireAt' => $currentTime + $maxExecuteTime ) ); $transaction->expire( self::getKey($key), ceil($currentTime + $maxExecuteTime), true ); try { $transaction->execute(); $getLock = 1; } catch ( Rediska_Transaction_Exception $e ) { /** * */ $getLock = false; } } else { $getLock = false; $transaction->discard(); } /** * */ if ( $getLock != 1 ) { // HSETNX $getLock = $rediska->setToHash( self::getKey($key), 'daddy', self::getCurrentProcessId(), false ); } /** * */ if ( $getLock == 1 ) { /** * , */ $rediska->setToHash(self::getKey($key), 'expireAt', $currentTime + $maxExecuteTime); $rediska->expire(self::getKey($key), ceil($currentTime + $maxExecuteTime), true); return true; } else { /** * , */ if ( $timeStop > $currentTime ) { usleep(20000); } /** * , */ else { return false; } } } } /** * * * @param string $key - * @param float $delayAfter - * @return bool */ static public function releaseLock( $key, $delayAfter = 0 ) { $currentTime = microtime(true); $rediska = Yii::app()->rediskaConnection->connect(); $transaction = $rediska->transaction( $rediska->getConnectionByKeyName( self::getKey($key) ) ); $transaction->watch( self::getKey($key) ); $arData = $rediska->getHash( self::getKey($key) ); if ( is_array($arData) && isset($arData['daddy']) && isset($arData['expireAt']) ) { $daddy = $arData['daddy']; $expireAt = $arData['expireAt']; } else { $daddy = false; $expireAt = 0; } /** * , */ if ( $daddy == self::getCurrentProcessId() ) { $transaction->setToHash(self::getKey($key), 'expireAt', $currentTime + $delayAfter); $transaction->expire(self::getKey($key), ceil($currentTime + $delayAfter), true); $transaction->deleteFromHash( self::getKey($key), 'daddy' ); /** * */ try { $transaction->execute(); $result = true; } catch (Rediska_Transaction_Exception $e) { $result = false; } } else { $transaction->discard(); $result = false; } // if ( $expireAt < $currentTime ) { if ( $result ) { /** * , */ throw new Timeout_Lock_Exception('Timeout Lock on release'); } else { /** * */ throw new LostLock_Timeout_Lock_Exception('Timeout Lock and it was lost before release'); } } return $result; } /** * * * @param string $key - * @param float $timeProlongate - * @return bool - false, */ static public function prolongate( $key, $timeProlongate ) { $rediska = Yii::app()->rediskaConnection->connect(); $transaction = $rediska->transaction( $rediska->getConnectionByKeyName( self::getKey($key) ) ); $transaction->watch( self::getKey($key) ); $arData = $rediska->getHash( self::getKey($key) ); $daddy = $arData['daddy']; $expireAt = $arData['expireAt']; $currentTime = microtime(true); $result = false; if ( $daddy == self::getCurrentProcessId() ) { $transaction->setToHash( self::getKey($key), 'expireAt', $currentTime + $timeProlongate ); $transaction->expire(self::getKey($key), ceil($currentTime + $timeProlongate), true); try { $transaction->execute(); $result = true; } catch (Rediska_Transaction_Exception $e) { $result = false; } } else { $transaction->discard(); $result = false; } if ( $expireAt < $currentTime ) { if ( $result ) { throw new Timeout_Lock_Exception('Timeout Lock on prolongate'); } else { throw new LostLock_Timeout_Lock_Exception('Timeout Lock and Lost them on prolongate'); } } return $result; } }
使用例
レポートを生成するスクリプトがあるとします。 そして、このスクリプトは複数のスレッドで同時に実行する意味がありません。 それらはすべて結果として同じ結果を生成しますが、それぞれが作業中に大量のリソースを必要とします。 このスクリプトは平均で40〜50分で実行されることがわかっているため、わずかなマージンを取り、ロックを60分間に設定します。 $lockKey = 'cron-report'; $timeWait = 0; $timeLock = 3600; if ( RedisLock::getLock( $lockKey, $timeWait, $timeLock ) ) { // , ... // , try { RedisLock::releaseLock( $lockKey, 0 ); echo 'Ok'; } catch ( Timeout_Lock_Exception $e ) { // // , , echo 'Timeout_Lock_Exception ' . ( $endTime - $currentTime ); } catch ( LostLock_Timeout_Lock_Exception $e ) { // // , , echo 'LostLock_Timeout_Lock_Exception' . ( $endTime - $currentTime ); } }
私たちの実装があなたに役立つことを願っています。
質問やコメントを待っています。