Zookeeper实现分布式锁(三)FairLock
在Zookeeper实现分布式锁的前两篇文章
我们实现了两种类型的锁,第一种是while循环,由于无限循环带来的cpu和zookeeper服务器的消耗,我们使用了watcher的方式,watcher方式采用了客户端监听锁节点的方式,完美解决了while出现的问题,但是这种方式又会存在“惊群”效应,本篇文章我们来实现一种排队锁也就是公平锁。
Zookeeper的节点类型
zookeeper实现公平锁依赖于zookeeper的顺序节点,先来了解一下zookeeper的节点都有什么类型的节点。
一共有四种
EPHEMERAL:临时节点,当客户端与ZooKeeper集合断开连接时,临时节点会自动删除。
PERSISTENT:持久节点,即使在创建该节点的客户端断开连接后,持久节点仍然存在。
EPHEMERAL_SEQUENTIAL:临时顺序节点
PERSISTENT_SEQUENTIAL:持久顺序节点
重点说一下顺序节点的含义:顺序节点可以是持久的或临时的。当一个新的znode被创建为一个顺序节点时,ZooKeeper通过将10位的序列号附加到原始名称来设置znode的路径。例如,如果将具有路径 /myapp 的znode创建为顺序节点,则ZooKeeper会将路径更改为 /myapp0000000001 ,并将下一个序列号设置为0000000002。如果两个顺序节点是同时创建的,那么ZooKeeper不会对每个znode使用相同的数字。
根据同时创建的同名顺序节点zookeeper会自动在命名上排序的特性,可以实现我们的公平锁。
FairLock实现
1.获取锁
多个客户端在lockName目录下创建同名的临时顺序节点,因为zookeeper会为我们保证节点的命名不重复性和顺序性,所以可以利用节点的顺序进行锁的判断。
public void lock(){
String path = null;
try {
path = zk.create(lockName+"/mylock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
lockZnode = path;
List<String> minPath = zk.getChildren(lockName,false);
System.out.println(minPath);
Collections.sort(minPath);
System.out.println("最小的节点是:"+minPath.get(0));
if (path!=null&&!path.isEmpty()
&&minPath.get(0)!=null&&!minPath.get(0).isEmpty()
&&path.equals(lockName+"/"+minPath.get(0))) {
System.out.println(Thread.currentThread().getName() + " 获取锁...");
return;
}
String watchNode = null;
for (int i=minPath.size()-1;i>=0;i--){
if(minPath.get(i).compareTo(path.substring(path.lastIndexOf("/") + 1))<0){
watchNode = minPath.get(i);
break;
}
}
if (watchNode!=null){
final String watchNodeTmp = watchNode;
final Thread thread = Thread.currentThread();
Stat stat = zk.exists(lockName + "/" + watchNodeTmp,new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType() == Event.EventType.NodeDeleted){
System.out.println("delete事件来了");
thread.interrupt();
System.out.println("打断当前线程");
}
}
});
if(stat != null){
System.out.println(Thread.currentThread().getName() + " waiting for " + lockName + "/" + watchNode);
}
}
try {
Thread.sleep(10000);
}catch (InterruptedException ex){
System.out.println(Thread.currentThread().getName() + " 被唤醒");
System.out.println(Thread.currentThread().getName() + " 获取锁...");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
获取锁逻辑:
1.首先创建顺序节点,然后获取当前目录下最小的节点,判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
2.获取锁失败的节点获取当前节点上一个顺序节点,对此节点注册watcher监听,并使当前线程进入sleep状态。
3.当监听的节点unlock删除节点之后会捕获到delete事件,这说明前面的线程都执行完了,当前线程interrupt,打断sleep状态,获取锁。
2.释放锁
public void unlock(){
try {
System.out.println(Thread.currentThread().getName() + "释放 Lock...");
zk.delete(lockZnode,-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
3.输出结果
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
[mylock_0000000112]
最小的节点是:mylock_0000000112
pool-1-thread-2 获取锁...
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
Receive event WatchedEvent state:SyncConnected type:None path:null
connection is ok
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的节点是:mylock_0000000112
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的节点是:mylock_0000000112
[mylock_0000000113, mylock_0000000112, mylock_0000000115, mylock_0000000114]
最小的节点是:mylock_0000000112
pool-1-thread-2释放 Lock...
pool-1-thread-1 waiting for /mylock/mylock_0000000113
pool-1-thread-3 waiting for /mylock/mylock_0000000114
pool-1-thread-4 waiting for /mylock/mylock_0000000112
delete事件来了
打断当前线程
pool-1-thread-4 被唤醒
pool-1-thread-4 获取锁...
pool-1-thread-4释放 Lock...
delete事件来了
打断当前线程
pool-1-thread-1 被唤醒
pool-1-thread-1 获取锁...
pool-1-thread-1释放 Lock...
delete事件来了
打断当前线程
pool-1-thread-3 被唤醒
pool-1-thread-3 获取锁...
pool-1-thread-3释放 Lock...
总结
根据输出结果,可以看出来这次的客户端获取锁是公平的,排着队一个一个来的,因为每个节点都有自己的一个数字id,根据数字id来监听比自己小的节点,这样释放锁只唤醒一个客户端,而不会产生惊群效应。
while版、watcher版、FairLock版这三种锁
while:性能好,但是资源消耗大,适合业务逻辑时间短的场景
watcher:性能中上,但是容易惊群,适合客户端比较少的场景
FairLock:性能低,但是安全性高,适合速度要求一般,按顺序来的场景