解读 zookeeper 分布式锁应用

1、zookeeper简介
  1. zookeeper是啥?

    ​ zookeeper是为分布式应用而生的,是协调多应用分布式部署共同完成任务的应用程序。管理协同数据的程序。例如访问某个应用接口的数据的过程,你只需要关心应用数据,对于这个数据时从那台服务器上提供的协同数据时有zookeeper来协调处理的。让开发人员更注重应用本身的逻辑关系,而不是应用之间的协同工作。

    ​ zookeeper可以在分布式系统中协助多个任务,任务可以理解成多个线程或者多个应用;这些任务可是协助或者为了管理竞争。

  2. zookeeper能干啥?

    • 用于存储集群中的共享数据(非海量数据)
    • 集群中选取主节点(主从节点选举)
    • 检测监控系统的崩溃情况(监听节点,当系统崩溃后,及时通知监控系统)
    • 服务发现,数据分片,故障恢复等功能(服务节点监听,新增服务信息,通知服务监控系统 dubbo框架)
  3. zookeeper有哪些特性?

    可靠性:一旦Zookeeper成功的应用了一个事务,并完成对client的响应,那么Zookeeper内部集群的所有服务器的状态都会是一致的保留下来,Zookeeper集群节点之间实现数据的同步,主节点会将接受的数据,同步到从节点上,当大部分节点都已经完成同步后,主节点将数据返回调用者,保证了主从节点之间的数据一致性。

    顺序一致性:从一个client发起的请求,最终会严格的按照发起的顺序被应用到Zookeeper中去。实质上,ZK会对每一个client的每一个请求,进行编号,说白了,就是分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。

    实时性:通常意义下的实时性是指一旦事务被成功应用,那么client会立刻从服务器端获取到变更后的新数据。ZK仅仅能够保证在一定时间内,client最终一定会能从服务器上获取到最新的数据。

    高可用:在ZK集群内部,会有一个Leader,多个Follower。一旦Leader挂掉,那么ZK会通过Paxos算法选举出新的Leader,只要ZK集群内部的服务器有一半以上正常工作,那么ZK就能对外正常提供服务!

    简单的数据结构:类似于Linux文件系统的树形结构,简单,实用!(树形层次空间)

在这里插入图片描述

2、分布式锁

​ 在单体的应用开发场景中,涉及并发同步的时候,大家往往采用synchronized或者Lock的方式来解决多线程间的同步问题。但在分布式集群工作的开发场景中,那么就需要一种更加高级的锁机制,来处理种跨JVM进程之间的数据同步问题,这就是分布式锁。

单体应用应用同步分布式应用同步
在这里插入图片描述在这里插入图片描述
3、zookeeper功能介绍
  1. 创建对话

    public ZkClient(String serverstring)

    public ZkClient(String zkServers, int connectionTimeout)

    public ZkClient(String zkServers, int sessionTimeout, int connectionTimeout)

    public ZkClient(String zkServers, int sessionTimeout, int connectionTimeout, ZkSerializer zkSerializer)

    public ZkClient(final String zkServers, final int sessionTimeout, final int connectionTimeout, final ZkSerializer zkSerializer, final long operationRetryTimeout)

    public ZkClient(IZkConnection connection)

    public ZkClient(IZkConnection connection, int connectionTimeout)

    public ZkClient(IZkConnection zkConnection, int connectionTimeout, ZkSerializer zkSerializer)

    public ZkClient(final IZkConnection zkConnection, final int connectionTimeout, final ZkSerializer zkSerializer, final long operationRetryTimeout)

  2. 创建节点

    • 永久节点

    public void createPersistent(String path)

    public void createPersistent(String path, boolean createParents)

    public void createPersistent(String path, boolean createParents, List acl)

    public void createPersistent(String path, Object data)

    public void createPersistent(String path, Object data, List acl)

    • 永久有序节点

    public String createPersistentSequential(String path, Object data)

    public String createPersistentSequential(String path, Object data, List acl)

    • 临时节点

    public void createEphemeral(final String path)

    public void createEphemeral(final String path, final List acl)

    public void createEphemeral(final String path, final Object data)

    public void createEphemeral(final String path, final Object data, final List acl)

    • 临时有序节点

    public String createEphemeralSequential(final String path, final Object data)

    public String createEphemeralSequential(final String path, final Object data, final List acl)

  3. 删除节点

    public boolean delete(final String path)

    public boolean delete(final String path, final int version)

    public boolean deleteRecursive(String path)

  4. 读取节点

    • 读取子节点的相对路径列表

    public List getChildren(String path)

    • 获取节点内容

      public T readData(String path)

      public T readData(String path, boolean returnNullIfPathNotExists)

      public T readData(String path, Stat stat)

  5. 更新数据

    public void writeData(String path, Object object)

    public void writeData(final String path, Object datat, final int expectedVersion)

    public Stat writeDataReturnStat(final String path, Object datat, final int expectedVersion)

  6. 检测是否存在

    protected boolean exists(final String path, final boolean watch)

  7. 注册监听

    接口类注册监听方法解除监听方法
    IZkChildListenerZkClient的subscribeChildChanges方法ZkClient的unsubscribeChildChanges方法
    IZkDataListenerZkClient的subscribeDataChanges方法ZkClient的subscribeChildChanges方法
    IZkStateListenerZkClient的subscribeStateChanges方法ZkClient的unsubscribeStateChanges方法
4、zookeeper分布式锁流程图
  1. 流程总图

在这里插入图片描述

  1. 流程分布说明

    应用A访问应用程序需要获取共享锁,首先在zookeeper的永久节点lockRoot下创建临时有序节点“00001”,
    创建成功后获取根节点下的所有子节点,对其进行排序,由于当前是最小节点,进入同步代码中执行,加锁成功。如下图:
    

在这里插入图片描述

 应用B访问应用程序需要获取共享锁,首先在zookeeper的永久节点lockRoot下创建临时有序节点“00002”,
 创建成功后获取根节点下的所有子节点,对其进行排序。此时节点“00002”不是最小节点,无法进入同步代码块中,线程阻塞。

在这里插入图片描述

应用B无法获取到锁以后,在获取到的节点中取其前一个节点“00001”,监听其节点变化。

在这里插入图片描述

 应用程序A执行完业务以后,删除临时节点“00001”,此时应用程序B监听节点“00001”,应用B收到通知后,
 重新获取节点lockRoot下面的节点信息。  此时应用B创建的节点“00002"为最小节点,将获取锁,将阻塞的线程唤醒,执行同步代码;

在这里插入图片描述

5、zookeeper分布式锁代码实现
  1. 初始化zookeeper客户端,创建监听节点

        //zookeeper 服务地址
        private String server = "127.0.0.1:2181";
        //客户端
        private ZkClient zkClient;
        //永久根节点
        private static final String rootPath = "/zookeeperLock";
        public ZookeeperLock() {
            zkClient = new ZkClient(server, 5000, 20000);
            buildRoot();
        }
        // 构建根节点
        public void buildRoot() {
            if (!zkClient.exists(rootPath)) {
                zkClient.createPersistent(rootPath);
            }
        }
    
  2. 创建临时有序节点

        public Lock createLockNode(String lockId) {
            String nodePath = zkClient.createEphemeralSequential(rootPath + "/" + lockId, "w");
            return new Lock(lockId, nodePath);
        }
    
  3. 激活锁:即获取根节点下面的所有子节点,进行排序获取最小节点有当前节点的值是否一致,如一致则激活成功,否则进入等待中,同时监听当前节点的前一节点。

      // 获取锁
        public Lock lock(String lockId, long timeout) {
            // 创建临时节点
            Lock lockNode = createLockNode(lockId);
            lockNode = tryActiveLock(lockNode);// 尝试激活锁
            if (!lockNode.isActive()) {
                try {
                    synchronized (lockNode) {
                        lockNode.wait(timeout); // 线程锁住
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            if (!lockNode.isActive()) {
                throw new RuntimeException(" lock  timeout");
            }
            return lockNode;
        }
    // 尝试激活锁
     private Lock tryActiveLock(Lock lockNode) {
            // 获取根节点下面所有的子节点
            List<String> list = zkClient.getChildren(rootPath)
                    .stream()
                    .sorted()
                    .map(p -> rootPath + "/" + p)
                    .collect(Collectors.toList());      // 判断当前是否为最小节点
            String firstNodePath = list.get(0);
            // 最小节点是不是当前节点
            if (firstNodePath.equals(lockNode.getPath())) {
                lockNode.setActive(true);
            } else {
                String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
                zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
                    @Override
                    public void handleDataChange(String dataPath, Object data) throws Exception {
    
                    }
    
                    @Override
                    public void handleDataDeleted(String dataPath) throws Exception {
                        // 事件处理 与心跳 在同一个线程,如果Debug时占用太多时间,将导致本节点被删除,从而影响锁逻辑。
                        System.out.println("" + dataPath);
                        //重新尝试获取锁
                         Lock lock = tryActiveLock(lockNode);
                        synchronized (lockNode) {
                            if (lock.isActive()) {
                                lockNode.notify(); // 释放了
                            }
                        }
                        //解除监听
                        zkClient.unsubscribeDataChanges(upNodePath, this);
                    }
                });
            }
            return lockNode;
        }
    
  4. 释放锁

     // 释放锁
        public void unlock(Lock lock) {
            if (lock.isActive()) {
                //删除临时节点
                zkClient.delete(lock.getPath());
            }
        }
    
  5. 实体类Lock

    public class Lock {
        private String lockId;
        private String path;
        private boolean active;
        public Lock(String lockId, String path) {
            this.lockId = lockId;
            this.path = path;
        }
    
        public Lock() {
        }
    
        public String getLockId() {
            return lockId;
        }
    
        public void setLockId(String lockId) {
            this.lockId = lockId;
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        public boolean isActive() {
            return active;
        }
    
        public void setActive(boolean active) {
            this.active = active;
        }
    }
    
6、总结
  1. client调用create()方法创建“/root/lock_”节点,注意节点类型是EPHEMERAL_SEQUENTIAL。
  2. client调用getChildren("/root/lock_")来获取所有已经创建的子节点,这里并不注册任何Watcher。
  3. 客户端获取到所有子节点Path后,如果发现自己在步骤1中创建的节点是所有节点中最小的,那么就认为这个客户端获得了锁。
  4. 如果在步骤3中,发现不是最小的,那么找到比自己小的那个节点,然后对其调用subscribeDataChanges方法注册事件监听。
  5. 之后一旦这个被关注的节点移除,客户端会收到相应的通知,这个时候客户端需要再次调用getChildren("/root/lock_")来确保自己是最小的节点,然后进入步骤3。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秉烛解毒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值