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<String> children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet<String> 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<String> children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet<String> 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<String> children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet<String> 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<String> children = zooKeeper.getChildren(lockPath, null);
            //3、排序
            TreeSet<String> 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事件,而当该锁解开后,会一下子通知多个等待的锁节点,会造成惊群效应。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Zookeeper实现分布式的一般步骤如下: 1. 在Zookeeper中创建一个临时节点,节点名称可以是的名称,节点数据可以是当前客户的ID,表示该客户获取了。 2. 客户获取时,先检查是否已经存在该,如果不存在,则创建该;如果已经存在,则等待。 3. 当客户释放时,删除该节点。 4. 其他客户在创建节点时,如果发现该已经存在,则设置Watcher,等待上一个持有客户释放之后,重新尝试获取。 以下是一个简单的Java代码示例,演示了如何使用Zookeeper实现分布式: ``` public class DistributedLock { private static final String LOCK_BASE_PATH = "/mylock"; private static final String LOCK_NAME_PREFIX = "lock_"; private ZooKeeper zk; private String lockPath; public DistributedLock(String zkUrl) throws IOException, InterruptedException, KeeperException { this.zk = new ZooKeeper(zkUrl, 5000, null); createLockBasePath(); } private void createLockBasePath() throws KeeperException, InterruptedException { if (zk.exists(LOCK_BASE_PATH, false) == null) { zk.create(LOCK_BASE_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } public void lock() throws KeeperException, InterruptedException { String path = zk.create(LOCK_BASE_PATH + "/" + LOCK_NAME_PREFIX, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); lockPath = path; while (true) { List<String> children = zk.getChildren(LOCK_BASE_PATH, false); String minChild = Collections.min(children); if (lockPath.endsWith(minChild)) { return; } else { String prevChild = children.get(children.indexOf(lockPath.substring(LOCK_BASE_PATH.length() + 1)) - 1); zk.exists(LOCK_BASE_PATH + "/" + prevChild, new LockWatcher()); } } } public void unlock() throws KeeperException, InterruptedException { zk.delete(lockPath, -1); } private class LockWatcher implements Watcher { @Override public void process(WatchedEvent event) { synchronized (this) { notifyAll(); } } } } ``` 在上述代码中,我们使用了ZooKeeper的EPHEMERAL_SEQUENTIAL节点类型来创建临时节点,并通过节点名称来实现。在获取时,会不断检查当前节点是否是最小的节点,如果不是,则等待上一个节点的Watcher通知,重新尝试获取。 需要注意的是,这只是一个简单的示例代码,实际应用中可能需要考虑更多的情况,比如节点的超时时间、异常处理等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值