Zookeeper—客户端 API 操作

前提:保证 zk200、zk201、zk202服务器上 zookeeper集群服务端启动。

1、IDEA 环境搭建 

  1、创建一个工程:zookeeper

  2、添加 pom 文件

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.5.7</version>
        </dependency>
</dependencies>

   3、在项目 /resources 目录下,新建文件 log4j.properties 

log4j.rootLogger=INFO, stdout 
log4j.appender.stdout=org.apache.log4j.ConsoleAppender 
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c]- %m%n 
log4j.appender.logfile=org.apache.log4j.FileAppender 
log4j.appender.logfile.File=target/spring.log 
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout 
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c]- %m%n 

  4、创建测试类

  

  5、源码

/**
 * @description
 * @author: hq
 * @create: 2021-11-28 22:05
 **/
public class ZkClient {

    private static String connectionString = "192.168.31.200:2181,192.168.31.201:2181,192.168.31.202:2181";
//private static String connectionString = "192.168.0.200:2181,192.168.0.201:2181,192.168.0.202:2181";

    private static int sessionTimeout = 60000;

    private ZooKeeper zkClient = null;

    @Before
    public void init() throws IOException, KeeperException, InterruptedException {
        zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到时间通知后的回调函数(用户的业务逻辑)
                System.out.println(event.getType() + "--" + event.getPath());
                // 再次启动监听,获取子节点的变化
                try {
                    List<String> children = zkClient.getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("------------------------------");
    }

    @Test
    public void create() throws KeeperException, InterruptedException {
        // 参数1:要创建的节点的路径;参数2:节点数据;参数3:节点权限;参数4:节点的类型
        String nodeCreate = zkClient.create("/test01", "yi".getBytes()
                , ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }

    @Test
    public void esists() throws KeeperException, InterruptedException {
        Stat exists = zkClient.exists("/test", true);
        System.out.println("是否存在:" + (exists != null ? "是" : "否"));
    }
}

2、客户端向服务端写数据流程

  1、写入请求直接发给 Leader 节点

  

  2、写入请求发给 follower 节点

  

 3、服务器动态上下线监听案例

  1、需求

    某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端能实时感知到主节点服务器的上下线。

  2、需求分析

    

  3、具体实现

    1)现在集群上创建 /servers 节点

    

     2)服务器向 zookeeper 注册

/**
 * @description 服务器向 zookeeper 注册
 * @author: hq
 * @create: 2021-12-08 15:48
 **/
public class DistributeServer {

    private static String connectionString = "192.168.31.200:2181,192.168.31.201:2181,192.168.31.202:2181";

    private static int sessionTimeout = 60000;

    private ZooKeeper zkClient = null;

    private String parentNode = "/servers";

    /**
     * 创建 zk 客户端连接
     */
    public void getConnect() throws IOException {
        zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到时间通知后的回调函数(用户的业务逻辑)
                System.out.println(event.getType() + "--" + event.getPath());
                // 再次启动监听,获取子节点的变化
                try {
                    List<String> children = zkClient.getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * 注册服务器
     *
     * @param hostname
     * @throws KeeperException
     * @throws InterruptedException
     */
    public void registServer(String hostname) throws KeeperException, InterruptedException {
        String create = zkClient.create(parentNode + "/server", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostname + " is working ...");
    }

    /**
     * 业务公能
     *
     * @param hostname
     * @throws InterruptedException
     */
    public void business(String hostname) throws InterruptedException {
        System.out.println(hostname + " is working ....");
        TimeUnit.SECONDS.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        // 1、获取连接
        DistributeServer distributeServer = new DistributeServer();
        distributeServer.getConnect();
        // 2、利用 zk 连接注册服务器信息系
        distributeServer.registServer(args[0]);
        // 3、启动业务功能
        distributeServer.business(args[0]);
    }

}

    3)客户端代码

/**
 * @description 客户端
 * @author: hq
 * @create: 2021-12-08 16:08
 **/
public class DistributeClient {

    private static String connectionString = "192.168.31.200:2181,192.168.31.201:2181,192.168.31.202:2181";

    private static int sessionTimeout = 60000;

    private ZooKeeper zkClient = null;

    private String parentNode = "/servers";

    /**
     * 创建 zk 客户端连接
     */
    public void getConnect() throws IOException {
        zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 收到时间通知后的回调函数(用户的业务逻辑)
                System.out.println(event.getType() + "--" + event.getPath());
                // 再次启动监听,获取子节点的变化
                try {
                    getServerList();
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * 获取服务器列表信息
     *
     * @throws KeeperException
     * @throws InterruptedException
     */
    public void getServerList() throws KeeperException, InterruptedException {
        // 1、获取服务器子节点信息,并且对父节点进行监听
        List<String> children = zkClient.getChildren(parentNode, true);
        // 2、存储服务器信息列表
        ArrayList<String> list = new ArrayList<>();
        // 3、遍历所有节点
        for (String child : children) {
            byte[] data = zkClient.getData(parentNode + "/" + child, false, null);
            list.add(new String(data));
        }
        // 4、打印服务器列表信息
        System.out.println(list);
    }

    public void business() throws InterruptedException {
        System.out.println("client is working ...");
        TimeUnit.SECONDS.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        // 1、获取连接
        DistributeClient distributeClient = new DistributeClient();
        distributeClient.getConnect();
        // 2、获取 servers 的子节点信息,从中获取服务器信息列表
        distributeClient.getServerList();
        // 3、启动业务
        distributeClient.business();
    }
}

  4、测试

    1)在 服务器命令行操作增加减少服务器

      1 启动 DistributeClient 客户端

      2 在 虚拟机 zk200 上 zookeeper 的客户端 /servers 目录上创建临时带序号节点

      

       3 观察 IDEA 控制台变化

       

    2)在 IDEA 上操作增加减少服务器

      1 启动 DistributeClient 客户端(已经启动过,不需要启动)

      2 启动 DistributeServer 服务

        

         在弹出的窗口中 (Program arguments)输入想启动的主机。例如 zk200

        

         然后再执行 DistributeServer 服务的 main()

        观察 DistributeServer、DistributeClient  控制台 

4、zookeeper 分布式锁案列

  1、什么叫做分布式锁呢?

     比如说”进程1“在使用该资源的时候,会先去获得锁,”进程1“获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,”进程1“用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫做分布式锁。

  

 

  2、原生 zookeeper 实现分布式锁案例

    1)分布式锁实现

/**
 * @description
 * @author: hq
 * @create: 2021-12-10 14:32
 **/
public class DistributedLock {

    private static String connectionString = "192.168.31.200:2181,192.168.31.201:2181,192.168.31.202:2181";

    private static int sessionTimeout = 60000;

    private ZooKeeper zkClient = null;

    private static String rootNode = "locks";

    private static String subNode = "seq-";

    //当前 client 等待的子节点
    private String waitPath;

    // zookeeper 连接
    private CountDownLatch countDownLatch = new CountDownLatch(1);
    // zookeeper 节点等待
    private CountDownLatch waitLatch = new CountDownLatch(1);

    // 当前 client 创建的子节点
    private String currentNode;

    // 和 zk 服务建立连接,并创建根节点
    public void getConnect() throws IOException, InterruptedException, KeeperException {
        zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 连接建立时,打开 latch,唤醒 wait 在该 latch 上的线程
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    countDownLatch.countDown();
                }
                // 发生了 waitPath 的删除事件
                if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });

        // 等待连接建立
        countDownLatch.await();

        // 获取根节点状态
        Stat stat = zkClient.exists("/" + rootNode, false);
        // 如果节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zkClient.create("/" + rootNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    /**
     * 加锁方法
     */
    public void zkLock() {

        try {
            // 在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zkClient.create("/" + rootNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // wait 小睡一会儿,让结果更清晰一些
            TimeUnit.MILLISECONDS.sleep(100);

            // 注意,没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zkClient.getChildren("/" + rootNode, false);

            // 列表中只有一个子节点,那肯定就是 currentNode,说明 client 获得锁
            if (childrenNodes.size() == 1) {
                return;
            } else {
                // 对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);

                // 当前节点名称
                String thisNode = currentNode.substring(("/" + rootNode + "/").length());
                // 获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);

                if (index == -1) {
                    System.out.println("异常信息");
                } else if (index == 0) {
                    // index == 0 ,说明 thisNode 在列表中最小,当前 client 获得锁
                    return;
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);

                    // 在 waitPath 上注册监听器,当 waitPath 被删除时,zookeeper 会回调监听器的 process 方法
                    zkClient.getData(waitPath, true, new Stat());
                    // 进入等待锁状态
                    waitLatch.await();

                    return;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 解锁方法
     */
    public void zkUnLock(){
        try {
            zkClient.delete(this.currentNode, -1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

}

    2)分布式锁测试

/**
 * @description 测试类
 * @author: hq
 * @create: 2021-12-10 15:19
 **/
public class DistributedLockTest {

    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {

        // 创建分布式锁1
        DistributedLock lock1 = new DistributedLock();
        lock1.getConnect();

        // 创建分布式锁2
        DistributedLock lock2 = new DistributedLock();
        lock2.getConnect();

        new Thread(() -> {
            // 获取锁对象
            try {
                lock1.zkLock();
                System.out.println("线程1 获取锁");
                Thread.sleep(5 * 1000);

                lock1.zkUnLock();
                System.out.println("线程1 释放锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            // 获取锁对象
            try {
                lock2.zkLock();
                System.out.println("线程2 获得锁");
                Thread.sleep(5 * 1000);

                lock2.zkUnLock();
                System.out.println("线程2 释放锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

   3、Curator 框架实现分布式锁案例

    1)原生的 Java API 开发存在的问题

      (1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch

      (2)Watch 需要重复注册,不然就不能生效

      (3)开发的复杂性还是比较高的

      (4)不支持多节点删除和创建,需要自己去递归

    2)Curator 是一个专门解决分布式锁的框架,解决了原生 Java API 开发分布式遇到的问题。

详情请查看官方文档:https://curator.apache.org/index.html

    3)Curator 案例

/**
 * @description
 * @author: hq
 * @create: 2021-12-10 16:20
 **/
public class CuratorLockTest {

    // zookeeper 连接服务器配置
    private static String connectionString = "192.168.31.200:2181,192.168.31.201:2181,192.168.31.202:2181";

    private static int sessionTimeout = 60000;

    private static int connectionTimeout = 60000;

    private ZooKeeper zkClient = null;

    private static String rootNode = "/locks";

    public CuratorFramework getCuratorFramework() {
        // 重试策略,初始时间 3秒,重试 3次
        RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);

        // 通过工厂创建 Curator
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(connectionString)
                .sessionTimeoutMs(sessionTimeout)
                .connectionTimeoutMs(connectionTimeout)
                .retryPolicy(policy).build();

        client.start();
        System.out.println("zookeeper 初始化完成。。。");
        return client;
    }

    public static void main(String[] args) {
        CuratorLockTest curatorLockTest = new CuratorLockTest();
        curatorLockTest.test();
    }

    public void test() {
        // 创建分布式锁1
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), rootNode);

        // 创建分布式锁2
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), rootNode);

        new Thread(() -> {
            // 获取锁对象
            try {
                lock1.acquire();
                System.out.println("线程1 获取锁");
                // 测试锁重入
                lock1.acquire();
                System.out.println("线程1 再次获取锁");
                TimeUnit.SECONDS.sleep(5);
                lock1.release();
                System.out.println("线程1 释放锁");
                lock1.release();
                System.out.println("线程1 再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
            // 获取锁对象
            try {
                lock2.acquire();
                System.out.println("线程2 获得锁");
                lock2.acquire();
                System.out.println("线程2 再次获得锁");
                TimeUnit.SECONDS.sleep(5);
                lock2.release();
                System.out.println("线程2 释放锁");
                lock2.release();
                System.out.println("线程2 再次释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

}

     运行结果:

    

 

posted @ 2021-11-28 21:48  放手解脱  阅读(220)  评论(0)    收藏  举报