Zookeeper典型应用场景介绍

博客转自:https://www.cnblogs.com/jian0110/p/10650396.html

引言

  ZooKeeper是中典型的pub/sub模式的分布式数据管理与协调框架,开发人员可以使用它进行分布式数据的发布与订阅。另外,其丰富的数据节点类型可以交叉使用,配合Watcher事件通知机制,可以应用于分布式都会涉及的一些核心功能:数据发布/订阅、Master选举、命名服务、分布式协调/通知、集群管理、分布式锁、分布式队列等。本博文主要介绍:发布/订阅、分布式锁、Master选举三种最常用的场景

  本文中的代码示例均是由Curator客户端编写的,已经对ZooKeeper原生API做好很多封装。参考资料《从Paxos到Zookeeper  分布式一致性原理与实践》(有需要电子PDF的朋友,可以评论私信我)


一、数据发布/订阅

1、基本概念

(1)数据发布/订阅系统即所谓的配置中心,也就是发布者将数据发布到ZooKeeper的一个节点或者一系列节点上,提供订阅者进行数据订阅,从而实现动态更新数据的目的,实现配置信息的集中式管理和数据的动态更新。ZooKeeper采用的是推拉相结合的方式:客户端向服务器注册自己需要关注的节点,一旦该节点的数据发生改变,那么服务端就会向相应的客户端发送Wacher事件通知,客户端接收到消息通知后,需要主动到服务端获取最新的数据。

(2)实际系统开发过程中:我们可以将初始化配置信息放到节点上集中管理,应用在启动时都会主动到ZooKeeper服务端进行一次配置读取,同时在指定节点注册Watcher监听,主要配置信息一旦变更,订阅者就可以获取读取最新的配置信息。通常系统中需要使用一些通用的配置信息,比如机器列表信息、运行时的开关配置、数据库配置信息等全局配置信息,这些都会有以下3点特性:

  1) 数据量通常比较小(通常是一些配置文件)

  2) 数据内容在运行时会经常发生动态变化(比如数据库的临时切换等)

  3) 集群中各机器共享,配置一致(比如数据库配置共享)。

(3)利用的ZooKeeper特性是:ZooKeeper对任何节点(包括子节点)的变更,只要注册Wacther事件(使用Curator等客户端工具已经被封装好)都可以被其它客户端监听

2、代码示例

package com.lijian.zookeeper.demo;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

import java.util.concurrent.CountDownLatch;

public class ZooKeeper_Subsciption {
    private static final String ADDRESS = "xxx.xxx.xxx.xxx:2181";
    private static final int SESSION_TIMEOUT = 5000;
    private static final String PATH = "/configs";
    private static RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    private static String config = "jdbc_configuration";
    private static CountDownLatch countDownLatch = new CountDownLatch(4);

    public static void main(String[] args) throws Exception {
        // 订阅该配置信息的集群节点(客户端):sub1-sub3
        for (int i = 0; i < 3; i++) {
            CuratorFramework consumerClient = getClient();
            subscribe(consumerClient, "sub" + String.valueOf(i));
        }
        // 更改配置信息的集群节点(客户端):pub
        CuratorFramework publisherClient = getClient();
        publish(publisherClient, "pub");

    }
    private static void init() throws Exception {
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(ADDRESS)
                .sessionTimeoutMs(SESSION_TIMEOUT)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        // 检查节点是否存在,不存在则初始化创建
        if (client.checkExists().forPath(PATH) == null) {
            client.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .forPath(PATH, config.getBytes());
        }
    }


    /**
     * 创建客户端并且初始化建立一个存储配置数据的节点
     *
     * @return
     * @throws Exception
     */
    private static CuratorFramework getClient() throws Exception {
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(ADDRESS)
                .sessionTimeoutMs(SESSION_TIMEOUT)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        if (client.checkExists().forPath(PATH) == null) {
            client.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .forPath(PATH, config.getBytes());
        }
        return client;
    }

    /**
     * 集群中的某个节点机器更改了配置信息:即发布了更新了数据
     *
     * @param client
     * @throws Exception
     */
    private static void publish(CuratorFramework client, String znode) throws Exception {

        System.out.println("节点[" + znode + "]更改了配置数据...");
        client.setData().forPath(PATH, "configuration".getBytes());
        countDownLatch.await();
    }

    /**
     * 集群中订阅的节点客户端(机器)获得最新的配置数据
     *
     * @param client
     * @param znode
     * @throws Exception
     */
    private static void subscribe(CuratorFramework client, String znode) throws Exception {
        // NodeCache监听ZooKeeper数据节点本身的变化
        final NodeCache cache = new NodeCache(client, PATH);
        // 设置为true:NodeCache在第一次启动的时候就立刻从ZooKeeper上读取节点数据并保存到Cache中
        cache.start(true);
        System.out.println("节点["+ znode +"]已订阅当前配置数据:" + new String(cache.getCurrentData().getData()));
        // 节点监听
        countDownLatch.countDown();
        cache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() {
                System.out.println("配置数据已发生改变, 节点[" + znode + "]读取当前新配置数据: " + new String(cache.getCurrentData().getData()));
            }
        });
    }
}

运行结果:节点[pub]更改了配置数据为“configuration”,订阅"/configs"节点的sub1-sub3观测到配置被改变,立马读取当前最新的配置数据“configuration”

Zookeeper典型应用场景介绍_第1张图片

二、Master选举

1、基本概念

  (1)在一些读写分离的应用场景中,客户端写请求往往是由Master处理的,而另一些场景中,Master则常常负责处理一些复杂的逻辑,并将处理结果同步给集群中其它系统单元。比如一个广告投放系统后台与ZooKeeper交互,广告ID通常都是经过一系列海量数据处理中计算得到(非常消耗I/O和CPU资源的过程),那就可以只让集群中一台机器处理数据得到计算结果,之后就可以共享给整个集群中的其它所有客户端机器。

  (2)利用ZooKeeper的特性:利用ZooKeeper的强一致性,即能够很好地保证分布式高并发情况下节点的创建一定能够保证全局唯一性,ZooKeeper将会保证客户端无法重复创建一个已经存在的数据节点,也就是说如果多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功,这个客户端就是Master,而其它客户端注在该节点上注册子节点Wacther,用于监控当前Master是否存活,如果当前Master挂了,那么其余客户端立马重新进行Master选举。

  (3)竞争成为Master角色之后,创建的子节点都是临时顺序节点,比如:_c_862cf0ce-6712-4aef-a91d-fc4c1044d104-lock-0000000001,并且序号是递增的。需要注意的是这里有"lock"单词,这说明ZooKeeper这一特性,也可以运用于分布式锁。

Zookeeper典型应用场景介绍_第2张图片

2、代码示例

package com.lijian.zookeeper.demo;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.leader.LeaderSelector;
import org.apache.curator.framework.recipes.leader.LeaderSelectorListenerAdapter;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ZooKeeper_Master {

    private static final String ADDRESS="xxx.xxx.xxx.xxx:2181";
    private static final int SESSION_TIMEOUT=5000;
    private static final String MASTER_PATH = "/master_path";
    private static final int CLIENT_COUNT = 5;

    private static RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);


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

        ExecutorService service = Executors.newFixedThreadPool(CLIENT_COUNT);
        for (int i = 0; i < CLIENT_COUNT; i++) {
            final String index = String.valueOf(i);
            service.submit(() -> {
                masterSelect(index);
            });
        }
    }

    private static void  masterSelect(final String znode){
        // client成为master的次数统计
        AtomicInteger leaderCount = new AtomicInteger(1);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(ADDRESS)
                .sessionTimeoutMs(SESSION_TIMEOUT)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        // 一旦执行完takeLeadership,就会重新进行选举
        LeaderSelector selector = new LeaderSelector(client, MASTER_PATH, new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
                System.out.println("节点["+ znode +"]成为master");
                System.out.println("节点["+ znode +"]已经成为master次数:"+ leaderCount.getAndIncrement());
                // 睡眠5s模拟成为master后完成任务
                Thread.sleep(5000);
                System.out.println("节点["+ znode +"]释放master");
            }
        });
        // autoRequeue自动重新排队:使得上一次选举为master的节点还有可能再次成为master
        selector.autoRequeue();
        selector.start();
    }
}

运行结果:由于执行selector.autoRequeue()方法,被选举为master后的节点可能会再次获被选举为master,所以会一直循环执行,以下只截图部分。其中获取成为master的次数充分表明了Master选举的公平性。

 Zookeeper典型应用场景介绍_第3张图片

三、分布式锁

1、基本概念

(1)对于排他锁:ZooKeeper通过数据节点表示一个锁,例如/exclusive_lock/lock节点就可以定义一个锁,所有客户端都会调用create()接口,试图在/exclusive_lock下创建lock子节点,但是ZooKeeper的强一致性会保证所有客户端最终只有一个客户创建成功。也就可以认为获得了锁,其它线程Watcher监听子节点变化(等待释放锁,竞争获取资源)。

  对于共享锁:ZooKeeper同样可以通过数据节点表示一个锁,类似于/shared_lock/[Hostname]-请求类型(读/写)-序号的临时节点,比如/shared_lock/192.168.0.1-R-0000000000

2、代码示例

Curator提供的有四种锁,分别如下:

  (1)InterProcessMutex:分布式可重入排它锁

  (2)InterProcessSemaphoreMutex:分布式排它锁

  (3)InterProcessReadWriteLock:分布式读写锁

  (4)InterProcessMultiLock:将多个锁作为单个实体管理的容器

主要是以InterProcessMutex为例,编写示例:

package com.lijian.zookeeper.demo;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.leader.LeaderSelector;
import org.apache.curator.framework.recipes.leader.LeaderSelectorListenerAdapter;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ZooKeeper_Master {

    private static final String ADDRESS="xxx.xxx.xxx.xxx:2181";
    private static final int SESSION_TIMEOUT=5000;
    private static final String MASTER_PATH = "/master_path";
    private static final int CLIENT_COUNT = 5;

    private static RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);


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

        ExecutorService service = Executors.newFixedThreadPool(CLIENT_COUNT);
        for (int i = 0; i < CLIENT_COUNT; i++) {
            final String index = String.valueOf(i);
            service.submit(() -> {
                masterSelect(index);
            });
        }
    }

    private static void  masterSelect(final String znode){
        // client成为master的次数统计
        AtomicInteger leaderCount = new AtomicInteger(1);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(ADDRESS)
                .sessionTimeoutMs(SESSION_TIMEOUT)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        // 一旦执行完takeLeadership,就会重新进行选举
        LeaderSelector selector = new LeaderSelector(client, MASTER_PATH, new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
                System.out.println("节点["+ znode +"]成为master");
                System.out.println("节点["+ znode +"]已经成为master次数:"+ leaderCount.getAndIncrement());
                // 睡眠5s模拟成为master后完成任务
                Thread.sleep(5000);
                System.out.println("节点["+ znode +"]释放master");
            }
        });
        // autoRequeue自动重新排队:使得上一次选举为master的节点还有可能再次成为master
        selector.autoRequeue();
        selector.start();
    }
}

运行结果:加锁后可以从左图看到读取的都是最新的资源值。如果去掉锁的话读取的资源值不能保证是最新值看右图

Zookeeper典型应用场景介绍_第4张图片    Zookeeper典型应用场景介绍_第5张图片


博客转自:https://blog.csdn.net/u013468915/article/details/80955110

1.前言

之前自己写了一些关于Zookeeper的基础知识,Zookeeper作为一种协调分布式应用高性能的调度服务,实际的应用场景也非常的广泛,这里主要通过几个例子来具体的说明Zookeeper在特定场景下的使用方式(下面的这些功能估计consul和etcd也能实现,以后学到了再说吧)。

2.具体应用

2.1.一致性配置管理

我们在开发的时候,有时候需要获取一些公共的配置,比如数据库连接信息等,并且偶然可能需要更新配置。如果我们的服务器有N多台的话,那修改起来会特别的麻烦,并且还需要重新启动。这里Zookeeper就可以很方便的实现类似的功能。

2.1.1.思路

1.将公共的配置存放在Zookeeper的节点中
2.应用程序可以连接到Zookeeper中并对Zookeeper中配置节点进行读取或者修改(对于写操作可以进行权限验证设置),下面是具体的流程图:

Zookeeper典型应用场景介绍_第6张图片
2.1.2.事例

数据库配置信息一致性的维护

配置类:

public class CommonConfig implements Serializable{
    // 数据库连接配置
    private String dbUrl;
    private String username;
    private String password;
    private String driverClass;
    
    public CommonConfig() {}
    
    public CommonConfig(String dbUrl, String username, String password, String driverClass) {
        super();
        this.dbUrl = dbUrl;
        this.username = username;
        this.password = password;
        this.driverClass = driverClass;
    }
 
    public String getDbUrl() {
        return dbUrl;
    }
 
    public void setDbUrl(String dbUrl) {
        this.dbUrl = dbUrl;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public String getPassword() {
        return password;
    }
 
    public void setPassword(String password) {
        this.password = password;
    }
 
    public String getDriverClass() {
        return driverClass;
    }
 
    public void setDriverClass(String driverClass) {
        this.driverClass = driverClass;
    }
    
    @Override
    public String toString() {
        return "CommonConfig:{dbUrl:" + this.dbUrl +
                ", username:" + this.username + 
                ", password:" + this.password + 
                ", driverClass:" + this.driverClass + "}";
    }
}

配置管理中心

  1. 获取本地配置信息
  2. 修改配置,并同步

同步配置信息到Zookeeper服务器

public class ZkConfigMng {
    private String nodePath = "/commConfig";
    private CommonConfig commonConfig;
    private ZkClient zkClient;
    
    public CommonConfig initConfig(CommonConfig commonConfig) {
        if(commonConfig == null) {
            this.commonConfig = new CommonConfig("jdbc:mysql://127.0.0.1:3306/mydata?useUnicode=true&characterEncoding=utf-8",
                    "root", "root", "com.mysql.jdbc.Driver");    
        } else {
            this.commonConfig = commonConfig;
        }
        return this.commonConfig;
    }
    
    /**
     * 更新配置
     * 
     * @param commonConfig
     * @return
     */
    public CommonConfig update(CommonConfig commonConfig) {
        if(commonConfig != null) {
            this.commonConfig = commonConfig;
        }
        syncConfigToZookeeper();
        return this.commonConfig;
    }
    
    public void syncConfigToZookeeper() {
        if(zkClient == null) {
            zkClient = new ZkClient("127.0.0.1:2181");
        }
        if(!zkClient.exists(nodePath)) {
            zkClient.createPersistent(nodePath);
        }
        zkClient.writeData(nodePath, commonConfig);
    }
}
以上是提供者,下面我们需要一个客户端获取这些配置

public class ZkConfigClient implements Runnable {
    
    private String nodePath = "/commConfig";
    
    private CommonConfig commonConfig;
 
    @Override
    public void run() {
        ZkClient zkClient = new ZkClient(new ZkConnection("127.0.0.1:2181", 5000));
        while (!zkClient.exists(nodePath)) {
            System.out.println("配置节点不存在!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 获取节点
        commonConfig = (CommonConfig)zkClient.readData(nodePath);
        System.out.println(commonConfig.toString());
        zkClient.subscribeDataChanges(nodePath, new IZkDataListener() {
            
            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                if(dataPath.equals(nodePath)) {
                    System.out.println("节点:" + dataPath + "被删除了!");
                }
            }
            
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
                if(dataPath.equals(nodePath)) {
                    System.out.println("节点:" + dataPath + ", 数据:" + data + " - 更新");
                    commonConfig = (CommonConfig) data;
                }
            }
        });
    }
 
}

下面启动Main函数

配置管理服务启动

public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(ZookeeperApiDemoApplication.class, args);
        
        ZkConfigMng zkConfigMng = new ZkConfigMng();
        zkConfigMng.initConfig(null);
        zkConfigMng.syncConfigToZookeeper();
        TimeUnit.SECONDS.sleep(10);
        
        // 修改值
        zkConfigMng.update(new CommonConfig("jdbc:mysql://192.168.1.122:3306/mydata?useUnicode=true&characterEncoding=utf-8",
                "root", "wxh", "com.mysql.jdbc.Driver"));
    }
}

客户端启动:

public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(ZookeeperApiDemoApplication.class, args);
 
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 模拟多个客户端获取配置
        executorService.submit(new ZkConfigClient());
        executorService.submit(new ZkConfigClient());
        executorService.submit(new ZkConfigClient());
    }
}

2.2.分布式锁

在我们日常的开发中,如果是单个进程中对共享资源的访问,我们只需要用synchronized或者lock就能实现互斥操作。但是对于跨进程、跨主机、跨网络的共享资源似乎就无能为力了。

2.1.1.思路

  1. 首先zookeeper中我们可以创建一个/distributed_lock持久化节点
  2. 然后再在/distributed_lock节点下创建自己的临时顺序节点,比如:/distributed_lock/task_00000000008
  3. 获取所有的/distributed_lock下的所有子节点,并排序
  4. 判读自己创建的节点是否最小值(第一位)
  5. 如果是,则获取得到锁,执行自己的业务逻辑,最后删除这个临时节点。
  6. 如果不是最小值,则需要监听自己创建节点前一位节点的数据变化,并阻塞。
  7. 当前一位节点被删除时,我们需要通过递归来判断自己创建的节点是否在是最小的,如果是则执行5);如果不是则执行6)(就是递归循环的判断)

下面是具体的流程图:

Zookeeper典型应用场景介绍_第7张图片
2.1.3.事例

public class DistributedLock {
    
    // 常亮
    static class Constant {
        private static final int SESSION_TIMEOUT = 10000;
        private static final String CONNECTION_STRING = "127.0.0.1:2181";
        private static final String LOCK_NODE = "/distributed_lock";
        private static final String CHILDREN_NODE = "/task_";
    }
    
    private ZkClient zkClient;
    
    public DistributedLock() {
        // 连接到Zookeeper
        zkClient = new ZkClient(new ZkConnection(Constant.CONNECTION_STRING));
        if(!zkClient.exists(Constant.LOCK_NODE)) {
            zkClient.create(Constant.LOCK_NODE, "分布式锁节点", CreateMode.PERSISTENT);
        }
    }
    
    public String getLock() {
        try {
            // 1。在Zookeeper指定节点下创建临时顺序节点
            String lockName = zkClient.createEphemeralSequential(Constant.LOCK_NODE + Constant.CHILDREN_NODE, "");
            // 尝试获取锁
            acquireLock(lockName);
            return lockName;
        } catch(Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
    /**
     * 获取锁
     * @throws InterruptedException 
     */
    public Boolean acquireLock(String lockName) throws InterruptedException {
        // 2.获取lock节点下的所有子节点
        List childrenList = zkClient.getChildren(Constant.LOCK_NODE);
        // 3.对子节点进行排序,获取最小值
        Collections.sort(childrenList, new Comparator() {
            @Override
            public int compare(String o1, String o2) {
                return Integer.parseInt(o1.split("_")[1]) - Integer.parseInt(o2.split("_")[1]);
            }
            
        });
        // 4.判断当前创建的节点是否在第一位
        int lockPostion = childrenList.indexOf(lockName.split("/")[lockName.split("/").length - 1]);
        if(lockPostion < 0) {
            // 不存在该节点
            throw new ZkNodeExistsException("不存在的节点:" + lockName);
        } else if (lockPostion == 0) {
            // 获取到锁
            System.out.println("获取到锁:" + lockName);
            return true;
        } else if (lockPostion > 0) {
            // 未获取到锁,阻塞
            System.out.println("...... 未获取到锁,阻塞等待 。。。。。。");
            // 5.如果未获取得到锁,监听当前创建的节点前一位的节点
            final CountDownLatch latch = new CountDownLatch(1);
            IZkDataListener listener = new IZkDataListener() {
                
                @Override
                public void handleDataDeleted(String dataPath) throws Exception {
                    // 6.前一个节点被删除,当不保证轮到自己
                    System.out.println("。。。。。。前一个节点被删除  。。。。。。");
                    acquireLock(lockName);
                    latch.countDown();
                }
                
                @Override
                public void handleDataChange(String dataPath, Object data) throws Exception {
                    // 不用理会
                }
            };
            try {
                zkClient.subscribeDataChanges(Constant.LOCK_NODE + "/" + childrenList.get(lockPostion - 1), listener);
                latch.await();
            } finally {
                zkClient.unsubscribeDataChanges(Constant.LOCK_NODE + "/" + childrenList.get(lockPostion - 1), listener);
            }
        }
        return false;
    }
    
    /**
     * 释放锁(删除节点)
     * 
     * @param lockName
     */
    public void releaseLock(String lockName) {
        zkClient.delete(lockName);
    }
    
    public void closeZkClient() {
        zkClient.close();
    }
}
 
@SpringBootApplication
public class ZookeeperDemoApplication {
 
    public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(ZookeeperDemoApplication.class, args);
        
        DistributedLock lock = new DistributedLock();
        String lockName = lock.getLock();
        /** 
         * 执行我们的业务逻辑
         */
        if(lockName != null) {
            lock.releaseLock(lockName);
        }
        
        lock.closeZkClient();
    }
}

2.3.分布式队列

在日常使用中,特别是像生产者消费者模式中,经常会使用BlockingQueue来充当缓冲区的角色。但是在分布式系统中这种方式就不能使用BlockingQueue来实现了,但是Zookeeper可以实现。

2.1.1.思路

  1. 首先利用Zookeeper中临时顺序节点的特点
  2. 当生产者创建节点生产时,需要判断父节点下临时顺序子节点的个数,如果达到了上限,则阻塞等待;如果没有达到,就创建节点。
  3. 当消费者获取节点时,如果父节点中不存在临时顺序子节点,则阻塞等待;如果有子节点,则获取执行自己的业务,执行完毕后删除该节点即可。
  4. 获取时获取最小值,保证FIFO特性。

2.1.2.事例

这个是一个消费者对一个生产者,如果是多个消费者对多个生产者,对代码需要调整。

public interface AppConstant {
    static String ZK_CONNECT_STR = "127.0.0.1:2181";
    static String NODE_PATH = "/mailbox";
    static String CHILD_NODE_PATH = "/mail_";
    static int MAILBOX_SIZE = 10;
}
 
public class MailConsumer implements Runnable, AppConstant{
 
    private ZkClient zkClient;
    private Lock lock;
    private Condition condition;
    
    public MailConsumer() {
        lock = new ReentrantLock();
        condition = lock.newCondition();
        zkClient = new ZkClient(new ZkConnection(ZK_CONNECT_STR));
        System.out.println("sucess connected to zookeeper server!");
        // 不存在就创建mailbox节点
        if(!zkClient.exists(NODE_PATH)) {
            zkClient.create(NODE_PATH, "this is mailbox", CreateMode.PERSISTENT);
        }
    }
 
    @Override
    public void run() {
        IZkChildListener listener = new IZkChildListener() {        
            @Override
            public void handleChildChange(String parentPath, List currentChilds) throws Exception {
                System.out.println("Znode["+parentPath + "] size:" + currentChilds.size());
                // 还是要判断邮箱是否为空
                if(currentChilds.size() > 0) {
                    // 唤醒等待的线程
                    try {
                        lock.lock();
                        condition.signal();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
        // 监视子节点的改变,不用放用while循环中,监听一次就行了,不需要重复绑定
        zkClient.subscribeChildChanges(NODE_PATH, listener);
        try {
            //循环随机发送邮件模拟真是情况
            while(true) {
                // 判断是否可以发送邮件
                checkMailReceive();
                // 接受邮件
                List mailList = zkClient.getChildren(NODE_PATH);
                // 如果mailsize==0,也没有关系;可以直接循环获取就行了
                if(mailList.size() > 0) {
                    Collections.sort(mailList, new Comparator() {
                        @Override
                        public int compare(String o1, String o2) {
                            return Integer.parseInt(o1.split("_")[1]) - Integer.parseInt(o2.split("_")[1]);
                        }
                    });
                    // 模拟邮件处理(0-1S)
                    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
                    zkClient.delete(NODE_PATH + "/" + mailList.get(0));
                    System.out.println("mail has been received:" + NODE_PATH + "/" + mailList.get(0));
                }
            } 
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            zkClient.unsubscribeChildChanges(NODE_PATH, listener);
        }
    }
 
    private void checkMailReceive() {
        try {
            lock.lock();
            // 判断邮箱是为空
            List mailList = zkClient.getChildren(NODE_PATH);
            System.out.println("mailbox size: " + mailList.size());
            if(mailList.size() == 0) {
                // 邮箱为空,阻塞消费者,直到邮箱有邮件
                System.out.println("mailbox is empty, please wait 。。。");
                condition.await();
                // checkMailReceive();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
 
public class MailProducer implements Runnable, AppConstant{
    
    private ZkClient zkClient;
    private Lock lock;
    private Condition condition;
    
    /**
     * 初始化状态
     */
    public MailProducer() {
        lock = new ReentrantLock();
        condition = lock.newCondition();
        zkClient = new ZkClient(new ZkConnection(ZK_CONNECT_STR));
        System.out.println("sucess connected to zookeeper server!");
        // 不存在就创建mailbox节点
        if(!zkClient.exists(NODE_PATH)) {
            zkClient.create(NODE_PATH, "this is mailbox", CreateMode.PERSISTENT);
        }
    }
 
    @Override
    public void run() {
        IZkChildListener listener = new IZkChildListener() {        
            @Override
            public void handleChildChange(String parentPath, List currentChilds) throws Exception {
                System.out.println("Znode["+parentPath + "] size:" + currentChilds.size());
                // 还是要判断邮箱是否已满
                if(currentChilds.size() < MAILBOX_SIZE) {
                    // 唤醒等待的线程
                    try {
                        lock.lock();
                        condition.signal();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
        // 监视子节点的改变,不用放用while循环中,监听一次就行了,不需要重复绑定
        zkClient.subscribeChildChanges(NODE_PATH, listener);
        try {
            //循环随机发送邮件模拟真是情况
            while(true) {
                // 判断是否可以发送邮件
                checkMailSend();
                // 发送邮件
                String cretePath = zkClient.createEphemeralSequential(NODE_PATH + CHILD_NODE_PATH, "your mail");
                System.out.println("your mail has been send:" + cretePath);
                // 模拟随机间隔的发送邮件(0-10S)
                TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
            } 
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            zkClient.unsubscribeChildChanges(NODE_PATH, listener);
        }
    }
    
    private void checkMailSend() {
        try {
            lock.lock();
            // 判断邮箱是否已满
            List mailList = zkClient.getChildren(NODE_PATH);
            System.out.println("mailbox size: " + mailList.size());
            if(mailList.size() >= MAILBOX_SIZE) {
                // 邮箱已满,阻塞生产者,直到邮箱有空间
                System.out.println("mailbox is full, please wait 。。。");
                condition.await();
                checkMailSend();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

2.4.均衡负载

首先我们需要简单的理解分布式和集群,通俗点说:分布式就是将一个系统拆分到多个独立运行的应用中(有可能在同一台主机也有可能在不同的主机上),集群就是将单个独立的应用复制多分放在不同的主机上来减轻服务器的压力。而Zookeeper不仅仅可以作为分布式集群的服务注册调度中心(例如dubbo),也可以实现集群的负载均衡。

2.4.1.思路

1.首先我们要理解,如果是一个集群,那么他就会有多台主机。所以,他在Zookeeper中信息的存在应该是如下所示:

Zookeeper典型应用场景介绍_第8张图片
2.如上的结构,当服务调用方调用服务时,就可以根据特定的均衡负载算法来实现对服务的调用(调用前需要监听/service/serviceXXX节点,以更新列表数据)

2.4.2.事例

/**
 * 服务提供者
 * 
 * @author Administrator
 *
 */
public class ServiceProvider {
    // 静态常量
    static String ZK_CONNECT_STR = "127.0.0.1:2181";
    static String NODE_PATH = "/service";
    static String SERIVCE_NAME = "/myService";
    
    private ZkClient zkClient;
    
    public ServiceProvider() {
        zkClient = new ZkClient(new ZkConnection(ZK_CONNECT_STR));
        System.out.println("sucess connected to zookeeper server!");
        // 不存在就创建NODE_PATH节点
        if(!zkClient.exists(NODE_PATH)) {
            zkClient.create(NODE_PATH, "this is mailbox", CreateMode.PERSISTENT);
        }
    }
    
    public void registryService(String localIp, Object obj) {
        if(!zkClient.exists(NODE_PATH + SERIVCE_NAME)) {
            zkClient.create(NODE_PATH + SERIVCE_NAME, "provider services list", CreateMode.PERSISTENT);
        }
        // 对自己的服务进行注册
        zkClient.createEphemeral(NODE_PATH + SERIVCE_NAME + "/" + localIp, obj);
        System.out.println("注册成功![" + localIp + "]");
    }    
}
 
/**
 * 消费者,通过某种均衡负载算法选择某一个提供者
 * 
 * @author Administrator
 *
 */
public class ServiceConsumer {
    // 静态常量
    static String ZK_CONNECT_STR = "127.0.0.1:2181";
    static String NODE_PATH = "/service";
    static String SERIVCE_NAME = "/myService";
    
    private List serviceList = new ArrayList();
    
    private ZkClient zkClient;
    
    public ServiceConsumer() {
        zkClient = new ZkClient(new ZkConnection(ZK_CONNECT_STR));
        System.out.println("sucess connected to zookeeper server!");
        // 不存在就创建NODE_PATH节点
        if(!zkClient.exists(NODE_PATH)) {
            zkClient.create(NODE_PATH, "this is mailbox", CreateMode.PERSISTENT);
        }
    }
    
    /**
     * 订阅服务
     */
    public void subscribeSerivce() {
        serviceList = zkClient.getChildren(NODE_PATH + SERIVCE_NAME);
        zkClient.subscribeChildChanges(NODE_PATH + SERIVCE_NAME, new IZkChildListener() {
            @Override
            public void handleChildChange(String parentPath, List currentChilds) throws Exception {
                serviceList = currentChilds;
            }
        });
    }
    
    /**
     * 模拟调用服务
     */
    public void consume() {
        //负载均衡算法获取某台机器调用服务
        int index = new Random().nextInt(serviceList.size());
        System.out.println("调用[" + NODE_PATH + SERIVCE_NAME + "]服务:" + serviceList.get(index));
    }
}

3.总结

Zookeeper是一个功能非常强大的应用,除了上面几种应用外,还有命名服务、分布式协调通知等也是常用的场景。

你可能感兴趣的:(ZooKeeper)