分布式锁 - Redlock

Redlock

锁所需要具有的属性

  1. 独享 - 在任意一个时刻,只有一个客户端持有锁
  2. 无死锁 - 持锁客户端失联(崩溃、网络分区),锁仍然可以被自动释放
  3. 容错 - 只要大部分Redis节点都活着,客户端就可以获取和释放锁

面临的问题

  1. 系统层 - 网络分区、崩溃、重启等问题
  2. Redis层 - Redis节点的崩溃、重启等问题
  3. 客户端层 - 客户端的崩溃、重启等问题
  4. 重入 - 同一个客户端多次获取锁
  5. 锁续期 - 锁的有效期到了,但是业务还没有执行完,需要续期
  6. 释放 - 释放锁的时候,锁已经被其他客户端获取
  7. 时钟漂移 - 不同的Redis节点的时间不一致
  8. 时钟回拨 - 时钟回拨导致锁的有效期不准确
  9. 误删 - 误删其他客户端的锁

解决方法

  1. 互斥性 - 通过SETNX命令实现
  2. 无死锁 - 通过设置锁的过期时间实现
  3. 容错性 - 通过大多数原则实现
  4. 重入 - 通过客户端ID实现
  5. 锁续期 - 通过锁的过期时间实现
  6. 释放 - 通过客户端ID实现
  7. 时钟漂移 - 通过设置锁的过期时间实现
  8. 时钟回拨 - 通过设置锁的过期时间实现
  9. 误删 - 通过客户端ID实现

算法

单机算法

  1. 获取锁

    SET resource_name my_random_value NX PX 30000

  2. 释放锁

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then 
return redis.call("del",KEYS[1])
else
return 0
end

分布式算法

  1. 获取锁
    1. 生成一个随机的客户端ID
    2. 依次向N个Redis节点获取锁
    3. 如果大多数Redis节点都获取到了锁,那么认为获取锁成功
    4. 如果获取锁失败,那么向所有Redis节点释放锁

Yii2 Redis lock 代码实现

类图
  1. 随机数获取实现 Yii::$app->security->generateRandomString 底层依赖 https://www.php.net/manual/en/function.random-bytes.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 /**
* Acquires a lock by name.
* @param string $name of the lock to be acquired. Must be unique.
* @param int $timeout time (in seconds) to wait for lock to be released. Defaults to `0` meaning that method will return
* false immediately in case lock was already acquired.
* @return bool lock acquiring result.
*/
protected function acquireLock($name, $timeout = 0)
{
$key = $this->calculateKey($name);
$value = Yii::$app->security->generateRandomString(20);

$result = $this->retryAcquire($timeout, function () use ($key, $value) {
return $this->redis->executeCommand('SET', [$key, $value, 'NX', 'PX', (int) ($this->expire * 1000)]);
});

if ($result) {
$this->_lockValues[$name] = $value;
}
return $result;
}

/**
* Releases acquired lock. This method will return `false` in case the lock was not found or Redis command failed.
* @param string $name of the lock to be released. This lock must already exist.
* @return bool lock release result: `false` in case named lock was not found or Redis command failed.
*/
protected function releaseLock($name)
{
static $releaseLuaScript = <<<LUA
if redis.call("GET",KEYS[1])==ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
LUA;
if (
!isset($this->_lockValues[$name])
|| !$this->redis->executeCommand('EVAL', [
$releaseLuaScript,
1,
$this->calculateKey($name),
$this->_lockValues[$name],
])
) {
return false;
}

unset($this->_lockValues[$name]);
return true;
}

/**
* Generates a unique key used for storing the mutex in Redis.
* @param string $name mutex name.
* @return string a safe cache key associated with the mutex name.
*/
protected function calculateKey($name)
{
return $this->keyPrefix . md5(json_encode([__CLASS__, $name]));
}

分布式锁 - Redlock
http://example.com/2024/11/27/redlock.html
作者
soul11201
发布于
2024年11月27日
许可协议