ZooKeeper之(六)应用实例

6.1 Java API

客户端要连接 Zookeeper服务器可以通过创建 org.apache.zookeeper.ZooKeeper 的一个实例对象,然后调用这个类提供的接口来和服务器交互。
ZooKeeper 主要是用来维护和监控一个目录节点树中存储的数据的状态,所有我们能够操作 ZooKeeper 和操作目录节点树大体一样,如创建一个目录节点,给某个目录节点设置数据,获取某个目录节点的所有子目录节点,给某个目录节点设置权限和监控这个目录节点的状态变化。
下面通过代码实例,来熟悉一下JavaAPI的常用方法。

import java.util.List;  

import org.apache.zookeeper.CreateMode;  
import org.apache.zookeeper.WatchedEvent;  
import org.apache.zookeeper.Watcher;  
import org.apache.zookeeper.ZooDefs.Ids;  
import org.apache.zookeeper.ZooKeeper;  
import org.apache.zookeeper.data.Stat;  

public class ZkTest {  

    private static final String CONNECT_STRING = "127.0.0.1:2181";  
    private static final int SESSION_TIMEOUT = 3000;  

    public static void main(String[] args) throws Exception {  

        // 定义一个监控所有节点变化的Watcher  
        Watcher allChangeWatcher = new Watcher() {  
            @Override  
            public void process(WatchedEvent event) {  
                System.out.println("**watcher receive WatchedEvent** changed path: " + event.getPath()  
                        + "; changed type: " + event.getType().name());  
            }  
        };  

        // 初始化一个与ZK连接。三个参数:  
        // 1、要连接的服务器地址,"IP:port"格式;  
        // 2、会话超时时间  
        // 3、节点变化监视器  
        ZooKeeper zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, allChangeWatcher);  

        // 新建节点。四个参数:1、节点路径;2、节点数据;3、节点权限;4、创建模式  
        zk.create("/myName", "chenlongfei".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);  
        System.out.println("create new node '/myName'");  

        // 判断某路径是否存在。两个参数:1、节点路径;2、是否监控(Watcher即初始化ZooKeeper时传入的Watcher)  
        Stat beforSstat = zk.exists("/myName", true);  
        System.out.println("Stat of '/myName' before change : " + beforSstat.toString());  

        // 修改节点数据。三个参数:1、节点路径;2、新数据;3、版本,如果为-1,则匹配任何版本  
        Stat afterStat = zk.setData("/myName", "clf".getBytes(), -1);  
        System.out.println("Stat of '/myName' after change: " + afterStat.toString());  

        // 获取所有子节点。两个参数:1、节点路径;2、是否监控该节点  
        List<String> children = zk.getChildren("/", true);  
        System.out.println("children of path '/': " + children.toString());  

        // 获取节点数据。三个参数:1、节点路径;2、书否监控该节点;3、版本等信息可以通过一个Stat对象来指定  
        byte[] nameByte = zk.getData("/myName", true, null);  
        String name = new String(nameByte, "UTF-8");  
        System.out.println("get data from '/myName': " + name);  

        // 删除节点。两个参数:1、节点路径;2、 版本,-1可以匹配任何版本,会删除所有数据  
        zk.delete("/myName", -1);  
        System.out.println("delete '/myName'");  

        zk.close();  
    }  

运行程序,打印结果如下:
这里写图片描述
Zookeeper 从设计模式角度来看,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。
下面通过两个ZooKeeper的典型用用场景来体会下ZooKeeper的特性与使用方法。

6.2 分布式锁

共享锁在同一个进程中很容易实现,可以靠Java本身提供的同步机制解决,但是在跨进程或者在不同 Server 之间就不好实现了,这时候就需要一个中间人来协调多个Server之间的各种问题,比如如何获得锁/释放锁、谁先获得锁、谁后获得锁等。
借助Zookeeper 可以实现这种分布式锁:需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren()方法获取列表中最小的目录节点,如果最小节点就是自己创建的目录节点,那么它就获得了这个锁,如果不是那么它就调用 exists() 方法并监控前一节点的变化,一直到自己创建的节点成为列表中最小编号的目录节点,从而获得锁。释放锁很简单,只要删除它自己所创建的目录节点就行了。

import java.util.Collections;  
import java.util.List;  

import org.apache.zookeeper.CreateMode;  
import org.apache.zookeeper.KeeperException;  
import org.apache.zookeeper.WatchedEvent;  
import org.apache.zookeeper.Watcher;  
import org.apache.zookeeper.ZooDefs.Ids;  
import org.apache.zookeeper.ZooKeeper;  
import org.apache.zookeeper.data.Stat;  

public class ZkDistributedLock {  

    // 以一个静态变量来模拟公共资源  
    private static int counter = 0;  

    public static void plus() {  

        // 计数器加一  
        counter++;  

        // 线程随机休眠数毫秒,模拟现实中的费时操作  
        int sleepMillis = (int) (Math.random() * 100);  
        try {  
            Thread.sleep(sleepMillis);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  

    // 线程实现类  
    static class CountPlus extends Thread {  

        private static final String LOCK_ROOT_PATH = "/Locks";  
        private static final String LOCK_NODE_NAME = "Lock_";  

        // 每个线程持有一个zk客户端,负责获取锁与释放锁  
        ZooKeeper zkClient;  

        @Override  
        public void run() {  

            for (int i = 0; i < 20; i++) {  

                // 访问计数器之前需要先获取锁  
                String path = getLock();  

                // 执行任务  
                plus();  

                // 执行完任务后释放锁  
                releaseLock(path);  
            }  

            closeZkClient();  
            System.out.println(Thread.currentThread().getName() + "执行完毕:" + counter);  
        }  

        /** 
         * 获取锁,即创建子节点,当该节点成为序号最小的节点时则获取锁 
         */  
        private String getLock() {  
            try {  
                // 创建EPHEMERAL_SEQUENTIAL类型节点  
                String lockPath = zkClient.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME,  
                        Thread.currentThread().getName().getBytes(), Ids.OPEN_ACL_UNSAFE,  
                        CreateMode.EPHEMERAL_SEQUENTIAL);  
                System.out.println(Thread.currentThread().getName() + " create path : " + lockPath);  

                // 尝试获取锁  
                tryLock(lockPath);  

                return lockPath;  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  

        /** 
         * 该函数是一个递归函数 如果获得锁,直接返回;否则,阻塞线程,等待上一个节点释放锁的消息,然后重新tryLock 
         */  
        private boolean tryLock(String lockPath) throws KeeperException, InterruptedException {  

            // 获取LOCK_ROOT_PATH下所有的子节点,并按照节点序号排序  
            List<String> lockPaths = zkClient.getChildren(LOCK_ROOT_PATH, false);  
            Collections.sort(lockPaths);  

            int index = lockPaths.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));  
            if (index == 0) { // lockPath是序号最小的节点,则获取锁  
                System.out.println(Thread.currentThread().getName() + " get lock, lockPath: " + lockPath);  
                return true;  
            } else { // lockPath不是序号最小的节点  

                // 创建Watcher,监控lockPath的前一个节点  
                Watcher watcher = new Watcher() {  
                    @Override  
                    public void process(WatchedEvent event) {  
                        System.out.println(event.getPath() + " has been deleted");  
                        synchronized (this) {  
                            notifyAll();  
                        }  
                    }  
                };  
                String preLockPath = lockPaths.get(index - 1);  
                Stat stat = zkClient.exists(LOCK_ROOT_PATH + "/" + preLockPath, watcher);  

                if (stat == null) { // 由于某种原因,前一个节点不存在了(比如连接断开),重新tryLock  
                    return tryLock(lockPath);  
                } else { // 阻塞当前进程,直到preLockPath释放锁,重新tryLock  
                    System.out.println(Thread.currentThread().getName() + " wait for " + preLockPath);  
                    synchronized (watcher) {  
                        watcher.wait();  
                    }  
                    return tryLock(lockPath);  
                }  
            }  

        }  

        /** 
         * 释放锁,即删除lockPath节点 
         */  
        private void releaseLock(String lockPath) {  
            try {  
                zkClient.delete(lockPath, -1);  
            } catch (InterruptedException | KeeperException e) {  
                e.printStackTrace();  
            }  
        }  

        public void setZkClient(ZooKeeper zkClient) {  
            this.zkClient = zkClient;  
        }  

        public void closeZkClient(){  
            try {  
                zkClient.close();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  

        public CountPlus(String threadName) {  
            super(threadName);  
        }  
    }  

    public static void main(String[] args) throws Exception {  

        // 开启五个线程  
        CountPlus threadA = new CountPlus("threadA");  
        setZkClient(threadA);  
        threadA.start();  

        CountPlus threadB = new CountPlus("threadB");  
        setZkClient(threadB);  
        threadB.start();  

        CountPlus threadC = new CountPlus("threadC");  
        setZkClient(threadC);  
        threadC.start();  

        CountPlus threadD = new CountPlus("threadD");  
        setZkClient(threadD);  
        threadD.start();  

        CountPlus threadE = new CountPlus("threadE");  
        setZkClient(threadE);  
        threadE.start();  
    }  

    public static void setZkClient(CountPlus thread) throws Exception {  
        ZooKeeper zkClient = new ZooKeeper("127.0.0.1:2181", 3000, null);  
        thread.setZkClient(zkClient);  
    }  

}  

注意:运行程序之前需要创建“/Locks”作为存放锁信息的根节点。
一旦某个Server想要获得锁,就会在/Locks”下创建一个EPHEMERAL_SEQUENTIAL类型的名为“Lock_”子节点,ZooKeeper会自动为每个子节点附加一个递增的编号,该编号为int类型,长度为10,左端以0补全。“/Locks”下会维持着这样一系列的节点:
Lock_0000000001,Lock_0000000002, Lock_0000000003, Lock_0000000004…
一旦这些创建这些节点的Server断开连接,该节点就会被清除(当然也可以主动清除)。
由于节点的编号是递增的,创建越晚排名越靠后。遵循先到先得的原则,Server创建完节点之后会检查自己的节点是不是最小的,如果是,那就获得锁,如果不是,排队等待。执行完任务之后,Server清除自己创建的节点,这样后面的节点会依次获得锁。
程序的运行结果如下:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值