{/**
* 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 functionlock()
{//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 functiongetRedisKey()
{return self::KEY_PREFIX . $this->lockName;
}/**
* 获取唯一标志位
* @Date 2019/11/22*/
public functiongetId()
{return uniqid(self::KEY_PREFIX) . mt_rand(self::MIN_RAND_NUM, self::RAND_MAX_NUM);
}
}