Redis lock implementation, based on the function of SetNX without expiration time, make up for the comparative analysis of schemes 1, 2 and 3
1. Reference URL:https://www.shuijingwanwq.com/2017/01/08/1505/, the process of realizing the locking mechanism of Redis under Yii2.0, its core is to use Redis SetNX.
2. Generally speaking, after the lock is successful, the corresponding business logic is executed, and then the lock is deleted. However, if the business logic is accidentally exited for some reason, resulting in a lock but not deleted, then this lock will always exist. Therefore, you need to add an expiration time to the lock just in case.
3. Because Redis SETNX does not have the function of expiration time. Scenario 1: Set up with the help of Expire, and at the same time, we need to wrap the two with Multi/Exec to ensure the atomicity of the request, so that SetNX succeeds Expire but fails. And only after the lock is successful, the expiration time is set. The lua script looks like this:
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. Since you want to use the lua script, it is still too troublesome. In fact, Redis has since 2.6.12, SET covers the function of SetEx, and SET itself already includes the function of setting the expiration time, that is to say, the functions we need earlier can only be implemented with SET. The code for scenario 2 is as follows:
set($key, $value, array('nx', 'ex' => $ttl));
if ($ok) {
// 业务逻辑代码
$redis->del($key);
}
?>
5. However, there are still problems in the above implementation. Imagine, if the execution time of a request for business logic code is relatively long, even longer than the validity period of the lock, resulting in the failure of the lock during execution. At this time, the other request If the previous request is completed, if the lock is directly deleted without judgment, the lock created by other requests will be accidentally deleted, so we need to introduce a random value when creating the lock. The optimization code for scheme 2 is as follows:
set($key, $random, array('nx', 'ex' => $ttl));
if ($ok) {
// 业务逻辑代码
if ($redis->get($key) == $random) {
$redis->del($key);
}
}
?>
6. The process of realizing the locking mechanism of Redis under Yii2.0 belongs to the third scheme. Its core is using Redis SetNX, and expire is not used to set the expiration time.
* @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. When it is locked, set the value of value to: current server time + expiration time. When locking, even if it has been locked by other clients, in order to prevent deadlock, the expiration time of the current lock is obtained. By comparison with the current server time, it is judged whether it has expired. If you use GetSet, it is still possible to lock successfully. Overall, scheme 3 has been tested in the actual production environment, and there are no bugs.
8. Scenario 3 Cannot solve the problem that exists in step 5 of scheme 2. In general, under scheme three, if you want to avoid the problems existing in step 5. It is only possible to avoid the case where the execution time is greater than the expiration time.
9. Whether it is scheme 1, 2, and 3, there is another problem, which is similar to the problem in step 5, that is, if the execution time of a request business logic code is relatively long, even the validity period of the lock is longer, resulting in the execution process. If the lock is invalid, another request will obtain the lock, and then the business logic code may be executed repeatedly, or even parallel execution. If the business logic code does not support repeated execution and parallel execution, new problems will arise. This leads to problems such as dirty data that are not expected. The solution to this problem is to add an optimistic lock for MySQL at the level of the business logic code and the like. To avoid duplication or parallel execution problems. Reduce the probability of such problems with the greatest probability.
10. Reference URL:http://www.redis.cn/commands/set.html. The code implementation logic of Scheme 3 is still too complex, compared to Scenario 2. In order to avoid the process of direct locking as much as possible, a method to determine whether the lock exists is also implemented. After planning to have spare time in the future, it is ready to adjust the locking implementation to scheme 2. The logic of scheme 2 is simpler and more readable. Due tosetCommand plus options can be completely replacedsetnx, setx, PPSETEXThe function, so in future versions, Redis may not recommend and finally abandon these commands. as shown in Figure 1
