一、zookeeper是什么
集中式存储数据服务,通过监听通知机制来实现来实现分布式应用的协调。

1、目前比较常见应用场景有:
分布式锁(临时节点)
服务注册与订阅(共用节点)
分布式通知(监听znode)
服务命名(znode特性)
数据订阅、发布(watcher)

2、zookeeper数据模型

zookeeper 默认数据存储结构 zookeeper的znode_System

zookeeper 维护了一个类似文件系统的数据结构,每个子目录(/znode1、/znode1/leaf)都被称作为 znode 即节点。和文件系统一样,我们可以很轻松的对 znode 节点进行增加、删除等操作,而且还可以在一个znode下增加、删除子znode,区别在于文件系统的是,znode可以存储数据(严格说是必须存放数据,默认是个空字符)。

这些节点分为4中类型:

1、持久化节点(zk断开节点还在)

2、持久化顺序编号目录节点

3、临时目录节点(客户端断开后节点就删除了)

4、临时目录编号目录节点

zkClient链接zookeeper,首先执行如下命令,连接到zookeeper server
./zkCli.sh -server ip:2181

./zkCli.sh -server 192.168.25.128:2181

zookeeper 默认数据存储结构 zookeeper的znode_模型_02

创建持久化节点

create /zk_test

创建临时节点

create -e /zk_test/e_node

创建临时顺序化节点

create -e -s /zk_test/es_node

创建顺序化节点

create -s /zk_test/s_node

zookeeper 默认数据存储结构 zookeeper的znode_模型_03


退出重新进入,临时节点消失。

zookeeper 默认数据存储结构 zookeeper的znode_System_04


zookeeper 默认数据存储结构 zookeeper的znode_模型_05

1、分布式锁

zookeeper基于watcher机制和znode的有序节点。首先创建一个/zk_test/lock父节点作为一把锁,尽量是持久节点(PERSISTENT类型),每个尝试获取这把锁的客户端,在/zk_test/lock父节点下创建临时顺序子节点。

由于序号的递增性,我们规定序号最小的节点即获得锁。例如:客户端来获取锁,在/zk_test/lock节点下创建节点为/zk_test/lock/seq-00000001,它是最小的所以它优先拿到了锁,其它节点等待通知再次获取锁。/test/lock/seq-00000001执行完自己的逻辑后删除节点释放锁。

那么节点/test/lock/seq-00000002想要获取锁等谁的通知呢?

这里我们让/test/lock/seq-00000002节点监听/test/lock/seq-00000001节点,一旦/test/lock/seq-00000001节点删除,则通知/test/lock/seq-00000002节点,让它再次判断自己是不是最小的节点,是则拿到锁,不是继续等通知。

以此类推/test/lock/seq-00000003节点监听/test/lock/seq-00000002节点,总是让后一个节点监听前一个节点,不用让所有节点都监听最小的节点,避免设置不必要的监听,以免造成大量无效的通知,形成“羊群效应”。

zookeeper分布式锁和redis分布式锁相比,因为大量的创建、删除节点性能上比较差,并不是很推荐。

public class Zk implements Lock,Runnable {


    private static final String IP_PORT = "192.168.25.128:2181";
    private static final String Z_NODE = "/lock1";

    // private volatile String beforePath;
    private static ThreadLocal<String> beforePathLocal = new ThreadLocal<>();
    private volatile String path;

    private ZkClient zkClient = new ZkClient(IP_PORT);

    public Zk() {
        if (!zkClient.exists(Z_NODE)) {
            zkClient.createPersistent(Z_NODE);
        }
    }

    @Override
    public void lock() {
        if (tryLock()) {
            System.out.println("获得锁");
        } else {
            // 尝试加锁
            // 进入等待 监听
            waitForLock();
            // 再次尝试
            lock();
        }

    }
    @Override
    public synchronized boolean tryLock() {
        // 第一次就进来创建自己的临时节点,(如果已经有其他节点创建,下面获取节点更大)
        if (StringUtils.isBlank(path)) {
            path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock");
        }

        // 对节点排序
        List<String> children = zkClient.getChildren(Z_NODE);
        //节点排序
        Collections.sort(children);

        // 当前的是最小节点说明当前节点,
        if (path.equals(Z_NODE + "/" + children.get(0))) {
            System.out.println("当前路径是创建最早,锁这个节点");
            return true;
        } else {
            // 不是最小节点 就找到自己的前一个 ,用于监听上一个节点,而不是最小节点,防止羊群效应。
            // 依次类推 释放也是一样
            int i = Collections.binarySearch(children, path.substring(Z_NODE.length() + 1));
            //获取当前节点上一个节点地址
            String beforePath = Z_NODE + "/" + children.get(i - 1);
            beforePathLocal.set(beforePath);
        }
        return false;
    }

    @Override
    public void unlock() {
        //防止内存泄漏
        beforePathLocal.remove();
        zkClient.delete(path);
    }

    public void waitForLock() {
         CountDownLatch cdl = new CountDownLatch(1);

        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
            }
            @Override
            public void handleDataDeleted(String s) throws Exception {
                System.out.println(Thread.currentThread().getName() + ":监听到节点被删(由于这里监听是该路径的上一个节点,所以是上一个节点被删)");
                cdl.countDown();
            }
        };
        // 监听
        String beforePath = beforePathLocal.get();
        this.zkClient.subscribeDataChanges(beforePath, listener);
        if (zkClient.exists(beforePath)) {
            try {
                System.out.println("加锁失败 等待");
                //一直等到 该节点监听上一个节点被删
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 释放监听
        zkClient.unsubscribeDataChanges(beforePath, listener);
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public Condition newCondition() {
        return null;
    }
    static volatile int inventory = 10;

    @Override
    public void run() {
        Zk zk = new Zk();
        try {
            zk.lock();
            if (inventory > 0) {
                inventory--;
            }
            System.out.println(inventory);
            return;
        } finally {
            zk.unlock();
            System.out.println("释放锁");
        }
    }

    private static final int NUM = 5;
    public static void main(String[] args) {
        try {

            for (int i = 1; i <= NUM; i++) {
                new Thread(new Zk()).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

2、配置管理

现在有很多开源项目都在使用Zookeeper来维护配置,像消息队列Kafka中,就使用Zookeeper来维护broker的信息;dubbo中管理服务的配置信息。原理也是基于watcher机制,例如:创建一个/config节点存放一些配置,客户端监听这个节点,一点修改/config节点的配置信息,通知各个客户端数据变更重新拉取配置信息。

可以看到dubbo在注册provider信息,任然是创建节点
AbstractZookeeperClient

public void create(String path, boolean ephemeral) {
        if (!ephemeral) {
            if (this.persistentExistNodePath.contains(path)) {
                return;
            }

            if (this.checkExists(path)) {
                this.persistentExistNodePath.add(path);
                return;
            }
        }

        int i = path.lastIndexOf(47);
        if (i > 0) {
            this.create(path.substring(0, i), false);
        }

        if (ephemeral) {
            this.createEphemeral(path);
        } else {
            this.createPersistent(path);
            this.persistentExistNodePath.add(path);
        }

    }