php实现zookeeper分布式锁
半人猛犸 2019-08-15 19:23:58 438 收藏 3
展开
php实现zookeeper分布式锁
zookeeper和redis实现分布式锁的对比:
1、redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能;zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
2、如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁。
分布式锁原理
这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
1、保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
2、控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时顺序节点。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
获取锁
方法1:创建一个临时节点
在需要获取排他锁时,所有的客户端都会试图通过调用 create -e 接口,在/distribute_lock节点下创建临时子节点
/distribute_lock/lock。 ZooKeeper会保证在所有的客户端中,最络只有一个客户端能够创建成功,那么就可以认为该客户端
获取了锁。同时,所有没有获取到锁的客户端就需要对 /distribute_lock/lock 节点上注册一个Watcher监听,以便实时监听到
lock节点的变更情况。如果节点被使用完删除了,zookeeper要向所有监听者发送通知,这会阻塞其他操作,并且会导致所有客户端来
争抢锁,这种情况称为“羊群效应”,试想一下,如果监听者众多的话,会拖累性能。
方法2:创建临时顺序节点
create -s -e /distribute_lock/lock- data
1、每个试图加锁的客户端都会创建一个临时顺序节点 /distribute_lock/lock-xxxxx,并且zk可以保证序号连续且唯一;
2、然后获取 /distribute_lock/ 下的所有子节点,并按从小到大排序list;
3、判断最小节点是不是自己,如果是,证明你就获取锁了,可以去处理业务逻辑了;
4、如果不是,获取到list中你的上一个节点名称(不一定是 -1 的那一个,因为此时它对应的客户端有可能主动放弃了),对其实施
监听操作 get /distribute_lock/lock-xxxxx watch 如果get监听失败了,说明节点已经别清除了,重复 2,3 直到监听成功
或者获取锁,如果监听成功,就在这里阻塞,等待通知;
5、如果通知过来了,重复 2,3,4 的步骤,直到获取锁,因为上一个节点被释放的原因并不一定是它得到锁-使用完-释放,有可能
是客户端断开连接了;
6、锁用完后记得主动清除,不然要等到心跳检测的时候才会清除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对比可以看出,方法2虽然比方法1麻烦一点,但是更加合理。
代码实现:
test-zookeeper.php
<?php
/*
* zookeeper 类属性常量参考
* https://www.php.net/manual/zh/class.zookeeper.php#zookeeper.class.constants.perms
*/
class zkCli {
protected static $zk;
protected static $myNode;
protected static $isNotifyed;
protected static $root;
public static function getZkInstance($conf, $root){
try{
if(isset(self::$zk)){
return self::$zk;
}
$zk = new \Zookeeper($conf['host'] . ':' . $conf['port']);
if(!$zk){
throw new \Exception('connect zookeeper error');
}
self::$zk = $zk;
self::$root = $root;
return $zk;
} catch (\ZookeeperException $e){
die($e->getMessage());
} catch (\Exception $e){
die($e->getMessage());
}
}
// 获取锁
public static function tryGetDistributedLock($lockKey, $value){
try{
// 创建根节点
self::createRootPath($value);
// 创建临时顺序节点
self::createSubPath(self::$root . $lockKey, $value);
// 获取锁
return self::getLock();
} catch (\ZookeeperException $e){
return false;
} catch (\Exception $e){
return false;
}
}
// 释放锁
public static function releaseDistributedLock(){
if(self::$zk->delete(self::$myNode)){
return true;
}else{
return false;
}
}
public static function createRootPath($value){
$aclArray = [
[
'perms' => Zookeeper::PERM_ALL,
'scheme' => 'world',
'id' => 'anyone',
]
];
// 判断根节点是否存在
if(false == self::$zk->exists(self::$root)){
// 创建根节点
$result = self::$zk->create(self::$root, $value, $aclArray);
if(false == $result){
throw new \Exception('create '.self::$root.' fail');
}
}
return true;
}
public static function createSubPath($path, $value){
// 全部权限
$aclArray = [
[
'perms' => Zookeeper::PERM_ALL,
'scheme' => 'world',
'id' => 'anyone',
]
];
/**
* flags :
* 0 和 null 永久节点,
* Zookeeper::EPHEMERAL临时,
* Zookeeper::SEQUENCE顺序,
* Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE 临时顺序
*/
self::$myNode = self::$zk->create($path, $value, $aclArray, Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE);
if(false == self::$myNode){
throw new \Exception('create -s -e '.$path.' fail');
}
echo 'my node is ' . self::$myNode.'-----------'.PHP_EOL;
return true;
}
public function getLock(){
// 获取子节点列表从小到大,显然不可能为空,至少有一个节点
$res = self::checkMyNodeOrBefore();
if($res === true){
return true;
}else{
self::$isNotifyed = false;// 初始化状态值
// 考虑监听失败的情况:当我正要监听before之前,它被清除了,监听失败返回 false
$result = self::$zk->get($res, [zkCli::class, 'watcher']);
while(!$result){
$res1 = self::checkMyNodeOrBefore();
if($res1 === true){
return true;
}else{
$result = self::$zk->get($res1, [zkCli::class, 'watcher']);
}
}
// 阻塞,等待watcher被执行,watcher执行完回到这里
while(!self::$isNotifyed){
echo '.';
usleep(500000); // 500ms
}
return true;
}
}
/**
* 通知回调处理
* @param $type 变化类型 Zookeeper::CREATED_EVENT, Zookeeper::DELETED_EVENT, Zookeeper::CHANGED_EVENT
* @param $state
* @param $key 监听的path
*/
public static function watcher($type, $state, $key){
echo PHP_EOL.$key.' notifyed ....'.PHP_EOL;
self::$isNotifyed = true;
self::getLock();
}
public static function checkMyNodeOrBefore(){
$list = self::$zk->getChildren(self::$root);
sort($list);
$root = self::$root;
array_walk($list, function(&$val) use ($root){
$val = $root . '/' . $val;
});
if($list[0] == self::$myNode){
echo 'get locak node '.self::$myNode.'....'.PHP_EOL;
return true;
}else{
// 找到上一个节点
$index = array_search(self::$myNode, $list);
$before = $list[$index - 1];
echo 'before node '.$before.'.........'.PHP_EOL;
return $before;
}
}
}
function zkLock($resourceId){
$conf = ['host'=>'127.0.0.1', 'port'=>2181];
$root = '/lockKey_' . $resourceId;
$lockKey = '/lock_';
$value = 'a';
$client = zkCli::getZkInstance($conf, $root);
$re = zkCli::tryGetDistributedLock($lockKey, $value);
if($re){
echo 'get lock success'.PHP_EOL;
}else{
echo 'get lock fail'.PHP_EOL;
return ;
}
try {
doSomething();
} catch(\Exception $e) {
echo $e->getMessage() . PHP_EOL;
} finally {
$re = zkCli::releaseDistributedLock();
if($re){
echo 'release lock success'.PHP_EOL;
}else{
echo 'release lock fail'.PHP_EOL;
}
return ;
}
}
function doSomething(){
$n = rand(1, 20);
switch($n){
case 1:
sleep(15);// 模拟超时
break;
case 2:
throw new \Exception('system throw message...');// 模拟程序中止
break;
case 3:
die('system crashed...');// 模拟程序崩溃
break;
default:
sleep(13);// 正常处理过程
}
}
// 执行
zkLock(0);
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
分别开启三个窗口 php test-zookeeper.php
1、等待顺序执行完。
2、将第二个ctrl+c 挂掉。
————————————————
版权声明:本文为CSDN博主「半人猛犸」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/raoxiaoya/article/details/99651669