⬆️ ⬇️

Implementing Lock's on Alawar Sites

Introduction

Today we will talk about locks and show our implementation. Each developer has repeatedly encountered a problem when it is necessary to ensure single-threaded use of a resource.



Often, to ensure such a lock, a scheme is used to create a special file, the presence of which determines whether a particular resource is busy.



This approach is quite simple to implement, but has several disadvantages. Among the shortcomings can be identified:



When do we need locks?

Each time the needs are different, mostly they are reduced to the exclusion of simultaneous repeated actions, ensuring consistent work with some resource, ensuring uniform load.



How to make yourself?

To implement the correct locks, you need to understand the principles of atomicity and transactionality . We will not describe them in this article, since There is already a lot of information on these topics on the Internet.

')

During implementation, we identified the main operations when working with locks:



In fact, it is not very important what will be used as a provider of locks, these can be files, mysql, memcache, or any other tool convenient for you.



Redis was closer to us, so we implemented our own blocking mechanism, which does not have the disadvantages listed above, on Redis.



How are locks made with us

Today we present our implementation “as is” with additional comments on almost every line and an example of usage.



Our implementation is used in a project on the Yii framework and uses it to connect to Redis via the Rediska library. But the focus on Yii, as in Rediska, is small, so this code can be used in any PHP project.



So, let's get down to the most interesting:



Base lock class
<?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 { } 


RedisLock class
In this class, locking is done using 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; } } 


Usage example
Suppose we have a script that generates reports. And this script does not make sense to run simultaneously in several threads, because as a result, they all will produce the same result, but each will require a large amount of resources during the work. We know that this script will work on average in 40-50 minutes, so we will make a small margin and set the lock for 60 minutes.

  $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 ); } } 


We hope that our implementation will be useful to you.



We are waiting for your questions and comments.

Source: https://habr.com/ru/post/158799/



All Articles