├── .editorconfig ├── .gitignore ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Facades │ └── RedisLock.php ├── LuaScripts.php ├── Processor.php ├── Providers │ ├── LumenRedisLockServiceProvider.php │ └── RedisLockServiceProvider.php └── redislock.config.php └── tests └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode 4 | /README.html 5 | composer.lock 6 | *.swp 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-redis-lock 2 | 3 | [![Total Downloads](https://poser.pugx.org/ginnerpeace/laravel-redis-lock/downloads.svg)](https://packagist.org/packages/ginnerpeace/laravel-redis-lock) 4 | [![Latest Stable Version](https://poser.pugx.org/ginnerpeace/laravel-redis-lock/v/stable.svg)](https://packagist.org/packages/ginnerpeace/laravel-redis-lock) 5 | [![Latest Unstable Version](https://poser.pugx.org/ginnerpeace/laravel-redis-lock/v/unstable.svg)](https://packagist.org/packages/ginnerpeace/laravel-redis-lock) 6 | [![License](https://poser.pugx.org/ginnerpeace/laravel-redis-lock/license.svg)](https://packagist.org/packages/ginnerpeace/laravel-redis-lock) 7 | 8 | > Simple redis distributed locks for Laravel. 9 | 10 | ## Getting started 11 | 12 | ### Install 13 | > Using composer. 14 | 15 | ```bash 16 | composer require "ginnerpeace/laravel-redis-lock:~2.3" 17 | ``` 18 | 19 | ### Add service provider: 20 | > Normally. 21 | 22 | ```php 23 | [ 27 | // ... 28 | RedisLock\Providers\RedisLockServiceProvider::class, 29 | ], 30 | // Its optional. 31 | 'aliases' => [ 32 | // ... 33 | 'RedisLock' => RedisLock\Facades\RedisLock::class, 34 | ], 35 | // ... 36 | ]; 37 | ``` 38 | 39 | > After Laravel 5.5, the package auto-discovery is supported. 40 | 41 | ```javascript 42 | { 43 | "providers": [ 44 | "RedisLock\\Providers\\RedisLockServiceProvider" 45 | ], 46 | "aliases": { 47 | "RedisLock": "RedisLock\\Facades\\RedisLock" 48 | } 49 | } 50 | ``` 51 | 52 | > Lumen 53 | 54 | ```php 55 | $app->register(RedisLock\Providers\LumenRedisLockServiceProvider::class); 56 | ``` 57 | 58 | ### Publish resources (laravel only) 59 | > Copied config to `config/redislock.php`. 60 | 61 | ```bash 62 | php artisan vendor:publish --provider="RedisLock\Providers\RedisLockServiceProvider" 63 | ``` 64 | 65 | Default items: 66 | ```php 67 | connection('default') 71 | 'connection' => 'default', 72 | 'retry_count' => 3, 73 | 'retry_delay' => 200, 74 | ]; 75 | 76 | ``` 77 | 78 | ### Use 79 | ```php 80 | retryCount`, will retry some times with its value. 89 | // Default value is `config('redislock.retry_count')` 90 | $payload = RedisLock::lock('key', $millisecond); 91 | /* 92 | [ 93 | "key" => "key", 94 | "token" => "21456004925bd1532e64616", 95 | "expire" => 100000, 96 | "expire_type" => "PX", 97 | ] 98 | */ 99 | 100 | // If cannot get lock, will return empty array. 101 | $payload = RedisLock::lock('key', 100000); 102 | /* 103 | [] 104 | */ 105 | 106 | // Return bool. 107 | RedisLock::unlock($payload); 108 | 109 | // Determine a lock if it still effective. 110 | RedisLock::verify($payload); 111 | 112 | // Reset a lock if it still effective. 113 | // The returned value is same to use RedisLock::lock() 114 | RedisLock::relock($payload, $millisecond); 115 | 116 | ///////////////////// 117 | // Special usages: // 118 | ///////////////////// 119 | 120 | // Retry 5 times when missing the first time. 121 | // Non-null `$retry` param will priority over `$this->retryCount`. 122 | RedisLock::lock('key', 100000, 5); 123 | 124 | // No retry (Try once only). 125 | RedisLock::lock('key', 100000, 0); 126 | // If value less than 0, still means try once only. 127 | // RedisLock::lock('key', 100000, -1); 128 | // Hmmmmmmm...Not pretty. 129 | 130 | // Change property `$this->retryDelay` (Default value is `config('redislock.retry_delay')`). 131 | // Retry 10 times when missing the first time. 132 | // Every retry be apart 500 ~ 1000 milliseconds. 133 | RedisLock::setRetryDelay(1000)->lock('key', 100000, 10); 134 | // PS: 135 | // RedisLock is default registered to singleton, call method `setRetryDelay()` will affects subsequent code. 136 | 137 | // Use in business logic: 138 | try { 139 | if (! $lock = RedisLock::lock('do-some-thing', 100000)) { 140 | throw new Exception('Resource locked.'); 141 | } 142 | ////////////////////// 143 | // Call ur methods. // 144 | ////////////////////// 145 | } catch (Exception $e) { 146 | throw $e; 147 | } finally { 148 | RedisLock::unlock($lock); 149 | } 150 | 151 | ``` 152 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ginnerpeace/laravel-redis-lock", 3 | "homepage": "https://github.com/jay-youngn/laravel-redis-lock", 4 | "description": "Simple redis distributed locks for Laravel.", 5 | "keywords": ["redis", "mutex lock", "laravel", "lumen"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "JayYoungn", 10 | "email": "ginnerpeace@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.0|^8.0", 15 | "illuminate/redis": "^5.1|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", 16 | "illuminate/support": "^5.1|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", 17 | "predis/predis": "^1.1 || ^2.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "RedisLock\\": "src/" 22 | } 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "RedisLock\\Providers\\RedisLockServiceProvider" 28 | ], 29 | "aliases": { 30 | "RedisLock": "RedisLock\\Facades\\RedisLock" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | 18 | ./src/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Facades/RedisLock.php: -------------------------------------------------------------------------------- 1 | 12 | * @link https://github.com/jay-youngn/laravel-redis-lock 13 | */ 14 | class Processor 15 | { 16 | // Redis key prefix. 17 | const KEY_PREFIX = 'mutex-lock:'; 18 | 19 | // Expire type is milliseconds. 20 | const EXPIRE_TYPE = 'PX'; 21 | 22 | /** 23 | * Predis Client. 24 | * 25 | * @var \Predis\ClientInterface 26 | */ 27 | private $client; 28 | 29 | /** 30 | * Number of retry times. 31 | * 32 | * @var int|null 33 | */ 34 | private $retryCount = null; 35 | 36 | /** 37 | * How many times do you want to try again. 38 | * (milliseconds) 39 | * 40 | * @var int 41 | */ 42 | private $retryDelay = 200; 43 | 44 | /** 45 | * This params from service provider. 46 | * 47 | * @param \Predis\ClientInterface $client 48 | * @param int|null $retryCount 49 | * @param int|null $retryDelay 50 | */ 51 | public function __construct(ClientInterface $client, int $retryCount = null, int $retryDelay = null) 52 | { 53 | $this->client = $client; 54 | $this->retryCount = $retryCount; 55 | 56 | if (isset($retryDelay)) { 57 | $this->setRetryDelay($retryDelay); 58 | } 59 | } 60 | 61 | /** 62 | * Set retry delay time. 63 | * 64 | * @param int $milliseconds 65 | */ 66 | public function setRetryDelay(int $milliseconds): self 67 | { 68 | $this->retryDelay = $milliseconds; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Trying to get lock. 75 | * 76 | * @param string $key 77 | * @param int $expire 78 | * @param int|null $retry 79 | * @return array 80 | * - Not empty for getted lock. 81 | * - Empty for lock timeout. 82 | */ 83 | public function lock(string $key, int $expire, int $retry = null): array 84 | { 85 | $retry = $retry ?? $this->retryCount ?? 0; 86 | 87 | while (! $result = $this->hit($key, $expire)) { 88 | if ($retry-- < 1) { 89 | return $result; 90 | } 91 | 92 | usleep(mt_rand(floor($this->retryDelay / 2), $this->retryDelay) * 1000); 93 | }; 94 | 95 | return $result; 96 | } 97 | 98 | /** 99 | * Release the lock. 100 | * 101 | * @param array $payload 102 | * @return bool 103 | */ 104 | public function unlock(array $payload): bool 105 | { 106 | if (! isset($payload['key'], $payload['token'])) { 107 | return false; 108 | } 109 | 110 | return 1 === $this->client->eval( 111 | LuaScripts::del(), 112 | 1, 113 | self::KEY_PREFIX . $payload['key'], 114 | $payload['token'] 115 | ); 116 | } 117 | 118 | /** 119 | * Reset a lock if it still effective. 120 | * 121 | * @param array $payload 122 | * @param int $expire 123 | * @return array 124 | * - Not empty for relock success. 125 | * - Empty for cant relock. 126 | */ 127 | public function relock(array $payload, int $expire): array 128 | { 129 | if ( 130 | isset($payload['key'], $payload['token']) 131 | && 1 === $this->client->eval( 132 | static::EXPIRE_TYPE === 'PX' ? LuaScripts::pexpire() : LuaScripts::expire(), 133 | 1, 134 | self::KEY_PREFIX . $payload['key'], 135 | $payload['token'], 136 | $expire 137 | ) 138 | ) { 139 | return [ 140 | 'key' => $payload['key'], 141 | 'token' => $payload['token'], 142 | 'expire' => $expire, 143 | 'expire_type' => static::EXPIRE_TYPE, 144 | ]; 145 | } 146 | 147 | return []; 148 | } 149 | 150 | /** 151 | * Verify lock payload. 152 | * 153 | * @param array $payload 154 | * @return bool 155 | */ 156 | public function verify(array $payload): bool 157 | { 158 | if (! isset($payload['key'], $payload['token'])) { 159 | return false; 160 | } 161 | 162 | return $payload['token'] === $this->client->get(self::KEY_PREFIX . $payload['key']); 163 | } 164 | 165 | /** 166 | * Do it. 167 | * 168 | * @param string $key 169 | * @param int $expire 170 | * @param string $token 171 | * @return array 172 | */ 173 | protected function hit(string $key, int $expire): array 174 | { 175 | if ('OK' === (string) $this->client->set( 176 | self::KEY_PREFIX . $key, 177 | $token = uniqid(mt_rand()), 178 | static::EXPIRE_TYPE, 179 | $expire, 180 | 'NX' 181 | )) { 182 | return [ 183 | 'key' => $key, 184 | 'token' => $token, 185 | 'expire' => $expire, 186 | 'expire_type' => static::EXPIRE_TYPE, 187 | ]; 188 | } 189 | 190 | return []; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Providers/LumenRedisLockServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->configure('redislock'); 13 | 14 | $this->mergeConfigFrom(__DIR__.'/../redislock.config.php', 'redislock'); 15 | } 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | protected function versionCompare(string $compareVersion, string $operator) 21 | { 22 | // Lumen (5.8.12) (Laravel Components 5.8.*) 23 | $lumenVersion = $this->app->version(); 24 | 25 | if (preg_match('/Lumen \((\d\.\d\.\d{1,2})\)( \(Laravel Components (\d\.\d\.\*)\))?/', $lumenVersion, $matches)) { 26 | // Prefer Laravel Components version. 27 | $lumenVersion = isset($matches[3]) ? $matches[3] : $matches[1]; 28 | } 29 | 30 | return version_compare($lumenVersion, $compareVersion, $operator); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Providers/RedisLockServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 16 | __DIR__.'/../redislock.config.php' => config_path('redislock.php'), 17 | ], 'config'); 18 | 19 | $this->mergeConfigFrom(__DIR__.'/../redislock.config.php', 'redislock'); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function register() 26 | { 27 | $this->app->singleton(Processor::class, function($app) { 28 | $config = $app['config']['redislock']; 29 | 30 | if ($this->versionCompare('5.4', '>=')) { 31 | $predisClient = $app['redis']->connection($config['connection'])->client(); 32 | } else { 33 | $predisClient = $app['redis']->connection($config['connection']); 34 | } 35 | 36 | return new Processor( 37 | $predisClient, 38 | $config['retry_count'], 39 | $config['retry_delay'] 40 | ); 41 | }); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function isDeferred() 48 | { 49 | return true; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function provides() 56 | { 57 | return [Processor::class]; 58 | } 59 | 60 | /** 61 | * Compare illuminate component version. 62 | * - illuminate/redis 5.4 has a big upgrade. 63 | * 64 | * @param string $compareVersion 65 | * @param string $operator 66 | * @return bool|null 67 | */ 68 | protected function versionCompare(string $compareVersion, string $operator) 69 | { 70 | return version_compare($this->app->version(), $compareVersion, $operator); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/redislock.config.php: -------------------------------------------------------------------------------- 1 | 'default', 5 | 'retry_count' => 3, 6 | 'retry_delay' => 200, 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | unit test 2 | --------------------------------------------------------------------------------