Zookeeper实现分布式锁(Java客户端)

一、原理

ZK实现分布式锁的原理:每当加锁时,在ZK中创建临时有序节点作为锁节点。而节点的创建代表要加锁,而节点的删除代表锁被释放。

锁进一步细粒度分为读锁和写锁。两者的伪代码分别如下:

(一)读锁

  1. 创建一个临时序号节点,节点为值read表示读锁。

  2. 获取当前ZK中序号比自己小的所有节点

  3. 判断最小的节点是否是读锁:(因为如果中间出现写锁,是不会加锁成功的,所以只需要判断最小的即可。)

    • 如果不是读锁,则上锁失败,为最小节点设置监听。阻塞等待。ZK的watch机制会当最小节点发生变化时通知当前节点,再执行第二步流程。

    • 如果是读锁的话 上锁成功。

(二)写锁

  1.  创建一个临时序号节点,节点值为write表示写锁。
  2. 获得zk中所有的子节点

  3. 判断自己是否是最小的节点:

    • 如果是,则上锁成功

    • 如果不是,则上锁失败,监听最小节点。当最小节点发生变化,则回到第二步。

二、代码实现

加锁、释放锁的代码实现

  //加读锁
    public String lockReadLock() throws Exception{
        //1、创建临时节点
        String createZnode = zooKeeper.create(znodePath,
                readLockValue.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        Thread thread = Thread.currentThread();
        //如果加锁失败 被唤醒后需要从这步开始再做
        while(true) {
            //2、获得所有子节点
            List children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet treeSet = new TreeSet<>(children);
            while (!treeSet.isEmpty()) {
                //4、获得最小节点,并从set中删除
                String smallestZnode = treeSet.first();
                treeSet.remove(smallestZnode);
                //5、如果是刚创建的临时节点,说明前面没有任何锁,可以直接加锁。
                if (createZnode.equals(lockPath + "/" + smallestZnode)) {
                    //加锁成功
                    System.out.println(Thread.currentThread().getName() + "  Read Lock is ok");
                    return createZnode;
                }
                //6、查看最小节点的值
                byte[] dataBytes = new byte[1024];
                try{
                   dataBytes = zooKeeper.getData(lockPath + "/" + smallestZnode, null, new Stat());
                }catch (Exception e){
                    //如果获得数据出错,说明该节点的锁已经释放,需要再重新获得下最小的节点
                    continue;
                }
                String dataStr = new String(dataBytes);
                //7
                if (readLockValue.equals(dataStr)) {
                    //7.1第一个是读锁,可以加锁
                    System.out.println(Thread.currentThread().getName() + "  Read Lock is ok");
                    return createZnode;
                } else {
                    //7.2 第一个不是读锁(是写锁)则要等到第一个锁被删除再说
                    zooKeeper.addWatch(lockPath + "/" + smallestZnode, new Watcher() {
                        @Override
                        public void process(WatchedEvent watchedEvent) {
                            if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
                                //监听的节点被删除时 唤醒程序
                                LockSupport.unpark(thread);
                                try {
                                    Thread.sleep(1000);
                                }catch (Exception e){
                                    System.out.println(e.toString());
                                }
                            }
                        }
                    }, AddWatchMode.PERSISTENT_RECURSIVE);
                    //阻塞
                    LockSupport.park();

                }
            }
            return null;
        }
    }

    //解除读锁
    public boolean unlockReadLock(String createZnode) throws Exception{
        zooKeeper.delete(createZnode, 0);
        System.out.println( Thread.currentThread().getName()+ "删除读锁");
        return true;
    }

    //加写锁
    public String lockWriteLock() throws Exception{
        //1、创建临时节点,设置值为write表示读锁
        String createZnode = zooKeeper.create(znodePath, writeLockValue.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        Thread thread = Thread.currentThread();
        while(true){
            //2、获得所有子节点
            List children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet treeSet = new TreeSet<>(children);
            //4、查看最小节点的值
            String smallestZnode = treeSet.first();
            treeSet.remove(smallestZnode);
            //5、查看当前节点是否是最小节点
            if(createZnode.equals(lockPath+"/"+smallestZnode)){
                //5.1 是最小节点,可以加写锁
                System.out.println(Thread.currentThread().getName()+" 加写锁 成功"+System.currentTimeMillis());
                return createZnode;
            }else{
                //5.2 不是最小则添加删除节点监听事件
                zooKeeper.addWatch(lockPath+"/"+smallestZnode, new Watcher() {
                    @Override
                    public void process(WatchedEvent watchedEvent) {
                        if(watchedEvent.getType() == Event.EventType.NodeDeleted){
                            LockSupport.unpark(thread);
                            try {
                                Thread.sleep(1000);
                            }catch (Exception e){
                                System.out.println(e.toString());
                            }
                        }
                    }
                },AddWatchMode.PERSISTENT_RECURSIVE);
                LockSupport.park();
            }

        }
    }

    //解除写锁
    public boolean unlockWriteLock(String createZnode) throws Exception{
        zooKeeper.delete(createZnode, 0);
        System.out.println( Thread.currentThread().getName()+ "删除读锁");
        return true;
    }

    //加锁后干的事情
    public void doSomethingAfterLock(){
        //System.out.println(nodePath+"加锁后开始做事"+readOrWrite);
        try {
            Thread.sleep(3*1000);
        }catch (Exception e){
            System.out.println(e.toString());
        }
        //System.out.println(nodePath+"加锁后结束某事"+readOrWrite);
    }

 为了更好地表示,我们分别启动了10个加读锁进程和10个加写锁进程,main方法为:

//加读锁
    public String lockReadLock() throws Exception{
        //1、创建临时节点
        String createZnode = zooKeeper.create(znodePath,
                readLockValue.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        Thread thread = Thread.currentThread();
        //如果加锁失败 被唤醒后需要从这步开始再做
        while(true) {
            //2、获得所有子节点
            List children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet treeSet = new TreeSet<>(children);
            while (!treeSet.isEmpty()) {
                //4、获得最小节点,并从set中删除
                String smallestZnode = treeSet.first();
                treeSet.remove(smallestZnode);
                //5、如果是刚创建的临时节点,说明前面没有任何锁,可以直接加锁。
                if (createZnode.equals(lockPath + "/" + smallestZnode)) {
                    //加锁成功
                    System.out.println(Thread.currentThread().getName() + "  Read Lock is ok");
                    return createZnode;
                }
                //6、查看最小节点的值
                byte[] dataBytes = new byte[1024];
                try{
                   dataBytes = zooKeeper.getData(lockPath + "/" + smallestZnode, null, new Stat());
                }catch (Exception e){
                    //如果获得数据出错,说明该节点的锁已经释放,需要再
                    continue;
                }
                String dataStr = new String(dataBytes);
                //7
                if (readLockValue.equals(dataStr)) {
                    //7.1第一个是读锁,可以加锁
                    System.out.println(Thread.currentThread().getName() + "  Read Lock is ok");
                    return createZnode;
                } else {
                    //7.2 第一个不是读锁(是写锁)则要等到第一个锁被删除再说
                    zooKeeper.addWatch(lockPath + "/" + smallestZnode, new Watcher() {
                        @Override
                        public void process(WatchedEvent watchedEvent) {
                            if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
                                //监听的节点被删除时 唤醒程序
                                LockSupport.unpark(thread);
                                try {
                                    Thread.sleep(100);
                                }catch (Exception e){
                                    System.out.println(e.toString());
                                }
                            }
                        }
                    }, AddWatchMode.PERSISTENT_RECURSIVE);
                    //阻塞
                    LockSupport.park();

                }
            }
            return null;
        }
    }

    //解除读锁
    public boolean unlockReadLock(String createZnode) throws Exception{
        zooKeeper.delete(createZnode, 0);
        System.out.println( Thread.currentThread().getName()+ "删除读锁");
        return true;
    }

    //加写锁
    public String lockWriteLock() throws Exception{
        //1、创建临时节点,设置值为write表示读锁
        String createZnode = zooKeeper.create(znodePath, writeLockValue.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        Thread thread = Thread.currentThread();
        while(true){
            //2、获得所有子节点
            List children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet treeSet = new TreeSet<>(children);
            //4、查看最小节点的值
            String smallestZnode = treeSet.first();
            treeSet.remove(smallestZnode);
            //5、查看当前节点是否是最小节点
            if(createZnode.equals(lockPath+"/"+smallestZnode)){
                //5.1 是最小节点,可以加写锁
                System.out.println(Thread.currentThread().getName()+" 加写锁 成功"+System.currentTimeMillis());
                return createZnode;
            }else{
                //5.2 不是最小则添加删除节点监听事件
                zooKeeper.addWatch(lockPath+"/"+smallestZnode, new Watcher() {
                    @Override
                    public void process(WatchedEvent watchedEvent) {
                        if(watchedEvent.getType() == Event.EventType.NodeDeleted){
                            LockSupport.unpark(thread);
                            try {
                                Thread.sleep(100);
                            }catch (Exception e){
                                System.out.println(e.toString());
                            }
                        }
                    }
                },AddWatchMode.PERSISTENT_RECURSIVE);
                LockSupport.park();
            }

        }
    }

    //解除写锁
    public boolean unlockWriteLock(String createZnode) throws Exception{
        zooKeeper.delete(createZnode, 0);
        System.out.println( Thread.currentThread().getName()+ "删除读锁");
        return true;
    }

    //加锁后干的事情
    public void doSomethingAfterLock(){
//        System.out.println(Thread.currentThread().getName()+": start");
        try {
            Thread.sleep(1*1000);
        }catch (Exception e){
            System.out.println(e.toString());
        }
//        System.out.println(Thread.currentThread().getName()+": end");
    }

为了更好地模拟,代码中出现多次Sleep来进行一定的停顿

三、结果

 由于线程调度问题,答案并不唯一,但只要满足读写锁的规律即可。

写锁4 加写锁 成功1648123105030
写锁4删除读锁
读锁6  Read Lock is ok
读锁1  Read Lock is ok
读锁4  Read Lock is ok
读锁0  Read Lock is ok
读锁2  Read Lock is ok
读锁9  Read Lock is ok
读锁5  Read Lock is ok
读锁6删除读锁
读锁1删除读锁
读锁8  Read Lock is ok
读锁4删除读锁
读锁3  Read Lock is ok
读锁0删除读锁
读锁7  Read Lock is ok
读锁2删除读锁
读锁9删除读锁
读锁5删除读锁
读锁8删除读锁
读锁3删除读锁
读锁7删除读锁
写锁7 加写锁 成功1648123109244
写锁7删除读锁
写锁1 加写锁 成功1648123110704
写锁1删除读锁
写锁9 加写锁 成功1648123112365
写锁9删除读锁
写锁8 加写锁 成功1648123113815
写锁8删除读锁
写锁6 加写锁 成功1648123115170
写锁6删除读锁
写锁2 加写锁 成功1648123116324
写锁2删除读锁
写锁5 加写锁 成功1648123117571
写锁5删除读锁
写锁3 加写锁 成功1648123118719
写锁3删除读锁
写锁0 加写锁 成功1648123119763
写锁0删除读锁

四、需要改进的地方

由于所有的 节点均为最早的锁节点绑定了Watcher事件,而当该锁解开后,会一下子通知多个等待的锁节点,会造成惊群效应。

改进方法:改为链式绑定,只绑定前面的一个锁节点。

你可能感兴趣的:(Zookeeper,java,zookeeper)