Zookeeper实现分布式锁(三)FairLock

在Zookeeper实现分布式锁的前两篇文章

Zookeeper实现分布式锁(一)While版
Zookeeper实现分布式锁(二)Watcher版

我们实现了两种类型的锁,第一种是while循环,由于无限循环带来的cpu和zookeeper服务器的消耗,我们使用了watcher的方式,watcher方式采用了客户端监听锁节点的方式,完美解决了while出现的问题,但是这种方式又会存在“惊群”效应,本篇文章我们来实现一种排队锁也就是公平锁。

Zookeeper的节点类型

zookeeper实现公平锁依赖于zookeeper的顺序节点,先来了解一下zookeeper的节点都有什么类型的节点。

《Zookeeper实现分布式锁(三)FairLock》 97Z0@F$BP%S)Y1W07WZK`41.png

一共有四种

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:性能低,但是安全性高,适合速度要求一般,按顺序来的场景

githup代码:https://github.com/granett/zookeeper/blob/master/src/main/java/zookeeper/zookeeper/Zookeeper_3_FairLock.java

    原文作者:激情的狼王
    原文地址: https://www.jianshu.com/p/c263f7e1df89
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞