1、参考网址:https://www.shuijingwanwq.com/2017/01/08/1505/ ,在 Yii2.0 下实现 Redis 的锁定机制的流程,其核心是使用 Redis setnx。
2、一般来说,在加锁成功后,执行相应的业务逻辑,然后删除锁。但是,如果业务逻辑因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在。因此,需要给锁加一个过期时间以防万一。
3、由于 Redis setnx 不具备过期时间的功能。方案一:借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 setnx 成功了 Expire 却失败了。并且只有当加锁成功后,才设置过期时间。Lua 脚本如下所示:
local key = KEYS[1]
local value = KEYS[2]
local ttl = KEYS[3]
local ok = redis.call('setnx', key, value)
if ok == 1 then
redis.call('expire', key, ttl)
end
return ok
4、由于要使用到 Lua 脚本,还是过于麻烦了些。其实 Redis 从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。方案二的代码如下所示:
<?php
$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($ok) {
// 业务逻辑代码
$redis->del($key);
}
?>
5、但是如上实现仍然存在问题,设想一下,如果一个请求业务逻辑代码的执行时间比较长,甚至比锁的有效期还要长,导致在执行过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在执行完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况,所以我们在创建锁的时候需要引入一个随机值。方案二的优化代码如下所示:
<?php
$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));
if ($ok) {
// 业务逻辑代码
if ($redis->get($key) == $random) {
$redis->del($key);
}
}
?>
6、在 Yii2.0 下实现 Redis 的锁定机制的流程 属于 方案三。其核心是使用 Redis setnx,且未使用 Expire 来设置过期时间。
<?php
namespace common/logics/redis;
use Yii;
/**
* This is the model class for table "{{%lock}}".
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Lock extends /yii/redis/ActiveRecord
{
/**
* Redis模型的锁定实现
* @param string $lockKeyName 锁定键名
* 格式如下:
*
* 'game_category' //锁定键名,如比赛分类
*
* @param int $timeOut Redis锁定超时时间,单位为秒
* 格式如下:3
*
* @return bool 成功返回真/失败返回假
* 格式如下:
*
* true //状态,获取锁定成功,可继续执行
*
* 或者
*
* false //状态,获取锁定失败,不可继续执行
*
*/
public function lock($lockKeyName, $timeOut = 3)
{
// 设置锁定的过期时间,获取相关锁定参数
$time = time();
$lockKey = Yii::$app->params['redisLock']['keyPrefix'] . $lockKeyName;
$lockExpire = $time + $timeOut;
// 获取 Redis 连接,以执行相关命令
$redis = Yii::$app->redis;
// 获取锁定
$executeCommandResult = $redis->setnx($lockKey, $lockExpire);
// 返回0,表示已经被其他客户端锁定
if ($executeCommandResult == 0) {
// 防止死锁,获取当前锁的过期时间
$lockCurrentExpire = $redis->get($lockKey);
// 判断锁是否过期,如果已经过期
if ($time > $lockCurrentExpire) {
// 防止并发锁定,检查存储在 key 的旧值是否仍然是过期的时间戳,如果是,则获取锁定,否则返回假
$executeCommandResult = $redis->getset($lockKey, $lockExpire);
if ($lockCurrentExpire != $executeCommandResult) {
return false;
}
}
// 返回0,表示已经被其他客户端锁定,且不存在死锁,返回假
if ($executeCommandResult == 0) {
return false;
}
}
return true;
}
/**
* 判断Redis模型的锁定是否存在
* @param string $lockKeyName 锁定键名
* 格式如下:
*
* 'game_category' //锁定键名,如比赛分类
*
* @return bool 锁定是否存在
* 格式如下:
*
* true //状态:已存在
*
* 或者
*
* false //状态:不存在
*
*/
public function isLockExist($lockKeyName)
{
// 获取相关锁定参数
$time = time();
$lockKey = Yii::$app->params['redisLock']['keyPrefix'] . $lockKeyName;
// 获取 Redis 连接,以执行相关命令
$redis = Yii::$app->redis;
// 获取锁定
$executeCommandResult = $redis->get($lockKey);
// 返回NULL,表示不存在锁定,否则表示存在
if ($executeCommandResult === null) {
return false;
} else {
// 如果存在锁定,判断锁是否过期,如果已经过期,则仍然认定为不存在锁定
if ($time > $executeCommandResult) {
// 如果已经过期,则释放锁定
$redis->del($lockKey);
return false;
}
}
return true;
}
/**
* Redis模型的释放锁定实现
* @param string $lockKeyName 锁定键名
* 格式如下:
*
* 'game_category' //锁定键名,如比赛分类
*
* @return integer 被删除的keys的数量
* 格式如下:
*
* 1 //被删除的keys的数量
*
* 或者
*
* 0 //被删除的keys的数量
*
*/
public function unlock($lockKeyName)
{
// 获取相关锁定参数
$lockKey = Yii::$app->params['redisLock']['keyPrefix'] . $lockKeyName;
// 获取 Redis 连接,以执行相关命令
$redis = Yii::$app->redis;
// 释放锁定
return $redis->del($lockKey);
}
}
7、其具体方案为加锁时,设置 value 的值为:当前服务器时间 + 过期时间。在加锁时,即使已经被其他客户端锁定,为了防止死锁,获取当前锁的过期时间。通过与当前服务器时间的比较,判断是否过期。再使用 getset,仍然有可能加锁成功。总体来看,方案三在实际的生产环境中经受了考验,不存在 Bug。
8、方案三 无法解决 方案二在步骤5中存在的问题。一般而言,在方案三下,如果要想避免步骤5中存在的问题。只能够尽量避免出现执行时间大于过期时间的情况了的。
9、且不论是方案一、二、三,还存在另外一个问题,与步骤5中的问题类似,即如果一个请求业务逻辑代码的执行时间比较长,甚至比锁的有效期还要长,导致在执行过程中,锁就失效了,此时另一个请求会获取锁,然后业务逻辑代码可能就会重复执行,甚至是并行执行,如果业务逻辑代码不支持重复执行与并行执行的话,就会产生新的问题。最终导致出现预期之外的脏数据之类的问题。此问题的解决方案为最好在业务逻辑代码层面再增加一个 MySQL 的乐观锁之类的实现。以避免出现重复或者并行执行的问题。以最大概率的降低此类问题出现的概率。
10、参考网址:http://www.redis.cn/commands/set.html 。方案三 的代码实现逻辑仍然过于复杂,相对于 方案二 而言。且为了尽量避免执行到直接加锁的流程,还实现了一个判断锁定是否存在的方法。计划后续有空余时间后,准备将 锁定实现 调整为 方案二。方案二的逻辑更为简单,可读性更好。由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。如图1
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/250643.html
