DistributedRedisLock.class.php
5.08 KB
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
<?php
/**
* Redis 分布式锁
* 注意: Redis 版本必须大于等于 2.6.12
* Created by PhpStorm.
* User: zhoutao
* Date: 2018/1/31
* Time: 下午6:30
*/
namespace Org\Util;
class DistributedRedisLock
{
/**
* 重试等待
* @var int
*/
private $retryDelay;
/**
* 重试次数
* @var int
*/
private $retryCount;
/**
* 漂移因子
* @var float
*/
private $clockDriftFactor = 0.01;
/**
* 权重数量
* @var mixed
*/
private $quorum;
/**
* Redis 服务器实例
* @var array
*/
private $instances = [];
/**
* Redis 服务器配置
* @var array
*/
private $servers = [];
/**
* DistributedRedisLock constructor.
* @param array $servers
* host Redis 地址
* port Redis 端口
* pwd Redis 秘钥
* timeout 断开时长
* [
* ['host 127.0.0.1', 'port 6379', 'pwd 123456', 'timeout 0.0'],
* ['host 127.0.0.1', 'port 6379', 'pwd 123456', 'timeout 0.0'],
* ]
* @param int $retryDelay 重试等待
* @param int $retryCount 重试次数
*/
function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
{
if (! extension_loaded('redis')) {
E(L('_NOT_SUPPORT_') . ':redis');
}
$this->servers = $servers;
$this->retryDelay = $retryDelay;
$this->retryCount = $retryCount;
// 需要抢到的 Redis 数量 (权重)
$this->quorum = min(count($servers), (count($servers) / 2 + 1));
}
/**
* 分发锁
* @param String $resource Redis Key (资源名称)
* @param Int $ttl 有效时间 (毫秒)
* @return array|bool
*/
public function lock($resource, $ttl)
{
$this->initInstances();
// 唯一标识
$token = uniqid();
// 重试次数 (防止污染)
$retry = $this->retryCount;
do {
$n = 0;
$startTime = microtime(true) * 1000;
foreach ($this->instances as $instance) {
if ($this->lockInstance($instance, $resource, $token, $ttl)) {
// 抢到锁
$n ++;
}
}
// 通过有效时间 计算偏差因素
$drift = ($ttl * $this->clockDriftFactor) + 2;
// 超时
$validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
// 所有 Redis 获得锁
if ($n >= $this->quorum && $validityTime > 0) {
return [
'validity' => $validityTime,
'resource' => $resource,
'token' => $token,
];
} else {
// 解锁 获得的锁
foreach ($this->instances as $instance) {
$this->unlockInstance($instance, $resource, $token);
}
}
// 等待一个随机事件来重试
$delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
usleep($delay * 1000);
$retry --;
} while ($retry > 0);
return false;
}
/**
* 解锁
* @param array $lock
*/
public function unlock(array $lock)
{
$this->initInstances();
$resource = $lock['resource'];
$token = $lock['token'];
foreach ($this->instances as $instance) {
$this->unlockInstance($instance, $resource, $token);
}
}
/**
* 初始化 Redis 集群链接
*/
private function initInstances()
{
if (empty($this->instances)) {
foreach ($this->servers as $server) {
if (is_array($server)) {
list($host, $port, $pwd, $timeout) = $server;
$redis = new \Redis();
$redis->connect($host, $port, $timeout);
// 需要密码时
if (! empty($pwd)) {
$redis->auth($pwd);
}
} elseif ($server instanceof \Redis) {
$redis = $server;
} else {
E('Redis Init Error');
}
$this->instances[] = $redis;
}
}
}
/**
* 获得锁
* @param $instance
* @param $resource
* @param $token
* @param $ttl
* @return mixed
*/
private function lockInstance($instance, $resource, $token, $ttl)
{
return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
}
/**
* 解锁
* @param $instance
* @param $resource
* @param $token
* @return mixed
*/
private function unlockInstance($instance, $resource, $token)
{
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';
return $instance->eval($script, [$resource, $token], 1);
}
}