我们在进行BS开如经常遇到的一类问题即是对记录的竞态访问,最近我们的平台也遇到了类似的问题,查找了一下几个比较有名的开源电商项目发现他们并没有实现记录的并发访问,于是自己动手实现了一个,典型的应用场景如下:一个用户审核记录时另一用户不需再审核;一个用户在处理发货时另一用户不用再处理;
现与各位分享(若想实现安全的锁则只需要设置不规律的$uid即可):
/**
*
* @author fzq
* @comment 以Redis为基础实现的分布式锁 需要script的支持
* @date 2016-09-02
*/
class RedLock //implements \Phalcon\DI\InjectionAwareInterface
{
private $redis = null;
// private $_di = null;
const LOCK_EX_PREFIX = 'str_exlock_';
const LOCK_PREFIX = 'str_lock_';
public function __construct( $redis )
{
$this->redis = $redis;
}
/**
* 加扩展锁
*
* 算法:先查看是否持有锁 如果持有锁则直接返回
* 否则加锁
* 加锁失败则返回false否则返加真
* @param string $strKey: biz key
* @param string $irecor: record id
* @param string $uid: user id
* @param int $ttl time to live
* @return boolean|number
*/
public function lockEx( $strBiz, $iRecordID, $uid, $ttl = 60 )
{
if( !$this->redis )
return false;
$curTime = TimeUtils::getIntTime();
$strKeyID = '"' . self::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID . '"';
// $strUID = '"' . $uid . '"';
if( is_string( $uid ) )
{
$uid = '"' . $uid . '"';
}
$script = <<<EOT
local ttl = redis.call( 'ttl', $strKeyID );
if( ttl >= $ttl ) then
local strVal = redis.call( 'get', $strKeyID );
local objVal = cjson.decode( strVal );
if objVal['uid'] == $uid then
return true;
else
return false;
end
elseif ttl < 0 then
local val = {};
val["uid"] = $uid;
val["localTime"] = $curTime;
local setRes = redis.call( 'setnx', $strKeyID, cjson.encode( val ) );
if setRes == 1 then
return redis.call( 'expire', $strKeyID, $ttl );
end
elseif ttl > 0 and ttl < $ttl then
local strVal = redis.call( 'get', $strKeyID );
local objVal = cjson.decode( strVal );
if objVal['uid'] == $uid then
return redis.call( 'expire', $strKeyID, $ttl );
else
return false;
end
else
return false;
end
EOT;
return $this->redis->eval( $script );
}
/**
* 加锁
*
* @param string $strBiz
* @param int $id
* @param int $ttl
* @return boolean
*/
public function lock( $strBiz, $iRecordID, $ttl = 60 )
{
if( !$this->redis )
return false;
$strKeyID = self::LOCK_PREFIX . $strBiz . '_' . $iRecordID;
if( $this->redis->set ( $strKeyID, TimeUtils::getIntTime(), [
'NX',
'EX' => $ttl
] ))
{
return true;
}
return false;
}
/**
* 解扩展锁
*
* 只能解自己持有的锁
* @param string $strBiz
* @param int $id
* @param int $uid
*/
public function unlockEx( $strBiz, $iRecordID, $uid )
{
if( !$this->redis )
return false;
$strKeyID = '"' . self::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID . '"';
if( is_string( $uid ) )
{
$uid = '"' . $uid . '"';
}
$script = <<<EOT
local lockData = redis.call( 'get', $strKeyID );
if lockData == false then
return true;
else
local objData = cjson.decode( lockData );
if( objData[ 'uid' ] == $uid ) then
return redis.call( 'del', $strKeyID );
end
return false;
end
EOT;
return $this->redis->eval( $script );
}
/**
* 解锁
* @param string $strBiz
* @param int $iRecordID
*/
public function unlock( $strBiz, $iRecordID )
{
if( !$this->redis )
return false;
$strKeyID = self::LOCK_PREFIX . $strBiz . '_' . $iRecordID;
return $this->redis->del ( $strKeyID );
}
/**
* 检测是否持有锁
*
* 只对扩展锁有效
* @param $strBiz
* @param $id
* @param $uid
*/
public function isHoldLock( $strBiz, $iRecordID, $uid )
{
if( !$this->redis )
return false;
if( is_string( $uid ) )
{
$uid = '"' . $uid . '"';
}
$strKeyID = '"' . self::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID . '"';
$script = <<<EOT
local lockData = redis.call( 'get', $strKeyID );
if lockData == false then
return false;
else
local objData = cjson.decode( lockData );
if( objData[ 'uid' ] == $uid ) then
return true;
else
return false;
end
end
EOT;
return $this->redis->eval( $script );
}
/**
* @param string $strBiz
* @param int $iRecordID
* return object(stdClass)[97]
* public 'localTime' => int 1473045341
* public 'uid' => int 3
*/
public function getLockInfo( $strBiz, $iRecordID )
{
if( !$this->redis )
return false;
$strKeyID = self::LOCK_EX_PREFIX . $strBiz . '_' . $iRecordID;
$strJson = $this->redis->get( $strKeyID );
if( $strJson )
{
$jsonData = json_decode( $strJson );
$jsonData->recordID = $iRecordID;
$jsonData->biz = $strBiz;
return $jsonData;
}
return false;
}
// public function setDI(\Phalcon\DiInterface $dependencyInjector)
// {
// $this->_di = $dependencyInjector;
// }
// public function getDI()
// {
// return $this->_di;
// }
}
用法
$rl = new RedLock( $this->nredis );
$strUID = 'asdflkjasdflkjq;we13123412351asdfasdf';
var_dump( $rl->lockEx( 'audit_narrator', 9, $strUID, 300 ));//true ttl 300
var_dump( $rl->lockEx( 'audit_narrator', 9, $strUID, 500 ));//true ttl 500
var_dump( $rl->lockEx( 'audit_narrator', 9, 2, 500 ));//false
var_dump( $rl->lockEx( 'audit_narrator', 9, 3, 500 ));//true
var_dump( $rl->unlockEx( 'audit_narrator', 9,3 ));//true
var_dump( $rl->lockEx( 'audit_narrator', 9, 3, 900 ));//true