PHP 基于redis的分布式锁
<?php class ProcessRedisLock { /** * redis key 前缀 */ const KEY_PREFIX = 'PROCESS_REDIS_LOCK:'; /** * 默认超时时间(秒) */ const DEFAULT_TIMEOUT = 5; /** * 最大超时时间(秒) */ const MAX_TIMEOUT_SETTING = 60; /** * 随机数的最小值 */ const MIN_RAND_NUM = 0; /** * 随机数的最大值 */ const RAND_MAX_NUM = 100000; /** * 每次取锁间隔毫秒数 */ const GET_LOCK_SLEEP_MICRO_SECONDS = 0.1; /** * @var mixed redis 实例 */ private $redisIns; /** * @var string 锁名 */ private $lockName; /** * @var int 超时时间,不可超过 self::MAX_TIMEOUT_SETTING */ private $timeout; /* 单元测试步骤 -------------------------------------------------------------------------------------- 1.分别用一个chrome和一个ie,模拟并发请求: 1)http://xxx/test?queryName=task1&handlerTime=10&lockName=myLocktest&timeout=10 2)http://xxx/test?queryName=task2&handlerTime=10&lockName=myLocktest&timeout=5 2.如上 task1处理耗时10s,取锁超时时间10s task2处理耗时10s,取锁超时时间5s 3.日志结果 2019-11-22 17:16:30 [task1]: 尝试取锁,超时时间设定10秒 2019-11-22 17:16:30 [task1]: 获取到锁,唯一标志:PROCESS_REDIS_LOCK:5dd7a76e1bafe37915 2019-11-22 17:16:32 [task2]: 尝试取锁,超时时间设定5秒 2019-11-22 17:16:35 [task1]: 释放锁 2019-11-22 17:16:35 [task2]: 获取到锁,唯一标志:PROCESS_REDIS_LOCK:5dd7a770d0c2b41355 2019-11-22 17:16:45 [task2]: 释放锁 -------------------------------------------------------------------------------------- 单元测试代码 -------------------------------------------------------------------------------------- // 浏览器模拟并发请求 // 注意:这里测试的时候如果session以文件存储的话,要避免两个窗口共用一个会话id,因为同样的会话id在session_start()时会锁文件 // 这样会造成请求阻塞,模拟不了请求并发的情况,所以应该使用两个不同的浏览器(如一个chrome,一个firefox)(同样的浏览器共享cookie也会导致拿到同样的会话id) public function testAction() { $params = $this->getRequest()->getParams(); $this->requestTask($params['queryName'], $params['handlerTime'], $params['lockName'], $params['timeout']); } // 模拟取锁、耗时操作、释放锁 public function requestTask($queryName, $handlerTime, $lockName, $timeout) { try { // 获取redis实例 $processRedisLock = new ProcessRedisLock(redis(), $lockName, $timeout); $this->echoAndSaveInfo($queryName, "尝试取锁,超时时间设定{$timeout}秒"); // 取锁 $id = $processRedisLock->lock(); // 如果到了超时时间还未取到会返回false,则直接抛异常 if($id === false){ throw new Exception('获取锁失败'); } $this->echoAndSaveInfo($queryName, "获取到锁,唯一标志:".$id); // 模拟耗时操作 sleep($handlerTime); // 释放锁 $processRedisLock->unlock($id); $this->echoAndSaveInfo($queryName, "释放锁"); } catch (Exception $e) { $this->echoAndSaveInfo($queryName, $e->getMessage()); // do something } } // 输出并且记录日志 public function echoAndSaveInfo($queryName, $content) { $info = date('Y-m-d H:i:s') . " [{$queryName}]: {$content}" . PHP_EOL; echo $info; file_put_contents('test.txt', $info . PHP_EOL, FILE_APPEND); } -------------------------------------------------------------------------------------- */ /** * ProcessRedisLock constructor. * @param $redisIns * @param $lockName * @param int $timeout * @throws Exception */ public function __construct($redisIns, $lockName, $timeout = self::DEFAULT_TIMEOUT) { if (!$redisIns) { new Exception('The redis instance is empty'); } if(!$lockName){ throw new Exception('Lock name invalid'); } // 校验超时时间 $timeout = intval($timeout); if (!($timeout > 0 && $timeout <= self::MAX_TIMEOUT_SETTING)) { throw new Exception('The timeout interval is (0,' . self::MAX_TIMEOUT_SETTING . ']'); } $this->redisIns = $redisIns; $this->lockName = $lockName; $this->timeout = $timeout; } /** * 加锁 * @return bool * @Date 2019/11/22 */ public function lock() { // redis key $key = $this->getRedisKey(); // 唯一标志 $id = $this->getId(); // 超时时间 $endTime = time() + $this->timeout; // 循环取锁 while (time() < $endTime) { // 尝试加锁,若给定的 key 已经存在,则 SETNX 不做任何动作。 if ($this->redisIns->setnx($key, $id)) { // 设置过期时间,防止程序异常退出没有解锁导致死锁 $this->redisIns->expire($key, $this->timeout); // 返回唯一标志,用于解锁 return $id; } usleep(self::GET_LOCK_SLEEP_MICRO_SECONDS); } return false; } /** * 解锁 * @param string $id 唯一标志,加锁成功时返回 * @return bool * @Date 2019/11/22 */ public function unlock($id) { $key = self::getRedisKey(); // 如果锁的值与没有被修改 if ($this->redisIns->get($key) == $id) { // 开始事务 $this->redisIns->multi(); // 释放该锁 $this->redisIns->del($key); // 执行 $this->redisIns->exec(); return true; } else { return false; } } /** * 获取redis key * @return string * @Date 2019/11/22 */ public function getRedisKey() { return self::KEY_PREFIX . $this->lockName; } /** * 获取唯一标志位 * @Date 2019/11/22 */ public function getId() { return uniqid(self::KEY_PREFIX) . mt_rand(self::MIN_RAND_NUM, self::RAND_MAX_NUM); } }