前言

前篇文章主要针对 zk的基本使用,以及特性和基本使用点 的分析,本篇文章会继续分析 zk的集群  如何搭建一个zk集群 部署  以及监控,以及 leader选举, 协议 核心,崩溃恢复 数据同步 数据配置 中心,以及 我们 常在dubbo上结合使用zk 做为配置中 心  的分析和实现 ,分布式队列的分析 和实现。

ZK集群安装与搭建

zookeeper集群 机器下线 zookeeper集群使用_云原生

  • 可靠的ZooKeeper 服务
  • 只要集群的大多数都准备好了,就可以使用这项服务
  • 容错集群设置至少需要三台集群,强烈使用奇数个服务器
  • 建议都部署到不同的机器上

 配置

直接 配置 具体的ip地址 以及 配置具体的  数据 日志 目录  和事务日志   以及 心跳时间配置,包括 里面的 2开头 端口  zk间通信的 端口  以及  选举的端口  3开头的。


集群节点


server.id=host:port:port


id ,通过在各自的dataDir目录下创建一个名为myid的文件来为每 台机器赋予一个服务器id。


两个端口号 ,第一个跟随者用来连接到领导者,第二个用来选举领导者。


zookeeper集群 机器下线 zookeeper集群使用_zookeeper_02


initLimit


集群中的follower服务器(F)与leader服务器(L)之间完成初始化同 步连接时能容忍的最多心跳数(tickTime的数量)。如果zk集群 环境数量确实很大,同步数据的时间会变长,因此这种情况下 可以适当调大该参数。


syncLimit


集群中的follower服务器与leader服务器之间请求和应答之间能 容忍的最多心跳数(tickTime的数量)。


zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_03



每台服务创建 myid文件   需要在 dataDir目录下创建;一行只包含机器id的文本,id在集群中必须是唯一的,其值应在1到255之间 如服务id为 1

集群的所有的节点 都提供服务,客户端链接时,连接串可以指定多个或者全部的节点的链接地址,当一个节点不通时,客户端会自动切换另一个节点。例如

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_04

 集群的监控

第一种方式  telnet 去设置

zookeeper集群 机器下线 zookeeper集群使用_云原生_05

 第二种方式

 使用JMX的方式

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_06

根据配置信息去查询。

leader 

对于leader都在竞争选择  成为 具体的leader.

zookeeper集群 机器下线 zookeeper集群使用_zookeeper_07

 Paxos算法

Paxos算法是Lamport宗师提出的一种基于消息传递的分布式一致性算法,使其获得2013年图灵奖。

Paxos由Lamport于1998年在《The Part-Time Parliament》论文中首次公开,最初的描述使用希腊的一个小岛Paxos作为比喻,描述了Paxos小岛中通过决议的流程,并以此命名这个算法,但是这个描述理解起来比较有挑战性。后来在2001年,Lamport觉得同行不能理解他的幽默感,于是重新发表了朴实的算法描述版本《Paxos Made Simple》。

自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性。Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的ZooKeeper,以及MySQL 5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。

然而,Paxos的最大特点就是难,不仅难以理解,更难以实现。

zookeeper集群 机器下线 zookeeper集群使用_云原生_08

P1a:提议发起者选择一个提案编号N,并且给大多数接收者发送一个带有编号n的提案预请求。

P1b:如果接收者收到一个编号n提案预请求,请求编号n大于前面已经响应过的预发请求编号,这是接收者做出相应,承若不再接受比编号n小的请求,并且如果存在比自己接受过的最高编号提案,则相应中带上,

P2a:如果提议发起者接收到了大多数接受者对于编号n预发请求的相应,这时会给这些接收者的每一个服务发送接受请求,接受请求内容为,编号n的提案并带上value的v,v的取值从哪里来,如果接收者响应中,有其他提议的内容,则接受者相应中取最高编号对应的值,如果响应中没有其他提议的内容,则可以是任意的值。

P2b:如果接受者受到一个编号n的提案接受请求,它接受该提案,如果它已经准备对大于n的编号的预发请求做出响应。则不接受编号n的请求。

zookeeper集群 机器下线 zookeeper集群使用_分布式_09

 paxos的结束

  最终只有一个提议值会被选择,只有被选择的提议者才会被learner节点学习。 最终总有一个提议生成,paxos协议能够让properties 发送的提议朝着大多数acceptor接受那个提议靠拢,因此保证可终止性。

paxos的流程 

proposer会发送两种类型的消息 acceptors  prepare准备 和accept接收请求。

proposer发送的提议请求由两部分组成, n为序号,  v为提议值。

zookeeper集群 机器下线 zookeeper集群使用_zookeeper_10

 proposerA B 都可以发送prepare提议请求。 acceptor C D 先接受到 proposer A 请求  acceptor E 先接受到 proposer b的请求。  

paxos 的流程  prepare b 

如果 acceptor 接收到  prepare提议的请求(n1,v1),并且还未接受到任何请求,会发送一个提议请求的响应。设置当前提议者 为(n1,v1)  保证不再接受序号小于 v1的提议请求。

zookeeper集群 机器下线 zookeeper集群使用_服务器_11

paxos 的流程 accept a 

随后 acceptor C D 会接受到 proposer b的提议请求  (n=4,v=8) 先前的提议请求 (n=2,v=5)  由于 4>2  由此发送 (n=2,v=5) 的提议响应,设置当前接收到的提议(n=4,v=5) 并且保证不再接受序号小于4的提议请求; acceptor E 先收到 Proposer B 的提议请求 (n=4.v=8) 后收到proposer a 提议请求(n=2,v=8)  后收到proposer a 的提议请求 (n=2,v=5)  由于 2<4 由此抛弃该提议请求。

zookeeper集群 机器下线 zookeeper集群使用_分布式_12

 Paxos 的流程 accept b

一旦proposer 收到超过半数 acceptor 所发送 prepare 提议响应。 便会向所有acceptor 发送一个 accept提议请求。

zookeeper集群 机器下线 zookeeper集群使用_云原生_13

proper A 收到两个提议 响应  后,发送 accept提议请求  (n=2,v=5),会被所有acceptor 丢弃。 proposer b 收到两个提议响应 (n=2,v=5)  发送 acceptor   提议请求 , 需要注意  n=4  是最初 proposer b 使用的序号, v=5 不是初始值, 而是提议响应中最高的v值。

Paxox 的流程 learner 

Acceptor C D E 收到 acceptor 提议请求后,会通知 所有的 learner 

zookeeper集群 机器下线 zookeeper集群使用_zookeeper_14

zookeeper集群 机器下线 zookeeper集群使用_云原生_15

选举中的概念 

包括 在 选举过程中 涉及到的状态。

zookeeper集群 机器下线 zookeeper集群使用_服务器_16

选举算法 

zookeeper集群 机器下线 zookeeper集群使用_服务器_17

 胜出条件 就是 

 投票数赞成大于半数则胜出的逻辑。

数据一致性

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_18

zk根据自身的情况建立了一套 ZAB协议

ZAB协议

ZAB协议,全称 Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。它是专门为分布式协调服务——Zookeeper,设计的一种支持崩溃恢复和原子广播的协议。

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_19

 参考Paxos 算法实现的。

关注点数据一致性,无关数据准确性,权威性,实时性。

ZAB协议的过程

所有事务都转发给 leader节点 产生一个 zxid

zookeeper集群 机器下线 zookeeper集群使用_服务器_20

 leader分配全局递增事务id,广播事务提议

zookeeper集群 机器下线 zookeeper集群使用_云原生_21

 

 follwer处理提议,做出反馈

zookeeper集群 机器下线 zookeeper集群使用_服务器_22

 leader 收到半数的响应 广播commit

zookeeper集群 机器下线 zookeeper集群使用_云原生_23

 leader 做出响应

从这流程 就能看出 zk适合 写比较少 都比较多的场景。

并且有个重要特征是有序性  ,这里半数就可以提交了。 并不是 需要所有都同意才提交。

 leader里面有个队列,用来保证 执行顺序

Leader崩溃

Leader 服务器 出现崩溃,或者说由于网络原因导致leader服务器失去了与过半Follwer 的联系,那么 就会进入崩溃恢复模式。

也就是在之前配置的时候 3开头的端口号。

ZAB协议规定一个事务Proposal在一台机器上处理成功,那么应该在所有机器上都被处理成功,哪怕机器出现故障崩溃。

ZAB协议确保那些已经在Leader服务器上提交事务最终被所有服务器都提交。

ZAB协议确保丢弃那些只在Leader服务器上被提出的事务。

zookeeper集群 机器下线 zookeeper集群使用_服务器_24

对于leader也好 还是 follower 都为了保证数据的正确性,都要zxid的最大个,进行恢复 同步。

崩溃的恢复 

ZAB协议需要设计的选举算法应该满足:确保提交已经被Leader提交事务Proposal.同时丢弃已经被跳过的事务 Proposal

  如果让Leader选举算法能够保证新选举出来的leader服务器拥有集群中所有机器最高ZXID的事务proposal 那么就可以保证这个新选举出来leader 一定有所有已经提交的提案。同时 如果让具有最高事务编号  的机器成为leader 就可以省去leader 服务器检查 proposal 的提交和丢弃工作这一步骤的。

数据同步

Leader选举出来后,需完成Followers与Leader的数据同步,当半数的Followers完成同步, 则可以开始提供服务。同步的过程如下。 Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被各Follower服 务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消 息后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower服务器将所 有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数据库中后, Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中,并开始之后的其他 流程。

丢弃事务的处理 

在ZAB协议的事务编号ZXID设计中,ZXID是一个64位的数字,低32位是一个简单的单调递增的计 数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数 器进行加1操作;高32位代表了 Leader周期纪元的编号,每当选举产生一个新的Leader服务器,就会从这 个Leader服务器上取出其本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出对应的纪元值,然后 对其进行加1操作,之后就会以此编号作为新的纪元,并将低32位置0来开始生成新的ZXIDo 基于这样的策略,当一个包含了上一个Leader周期中尚未提交过的事务

Proposal的服务器启动加入到集群 中,发现此时集群中已经存在leader,将自身以Follower角色连接上Leader服务器之后,Leader服务器会根据 自己服务器上最后被提交的Proposal来和Follower服务器的Proposal进行比对,发现follower中有上一个 leader周期的事务Proposal时,Leader会要求Follower进行一个回退操作一一回退到一个确实已经被集群中过 半机器提交的最新的事务Proposal 。

下面 zk的配置等等。

ZooKeeper: Because Coordinating Distributed Systems is a Zoo (apache.org)

 经典应用场景

1、数据发布订阅  2、命名服务、3 master 选举 4.集群管理  5.分布式队列 6.分布式锁

分布式队列的应用场景

利用 zk达到解耦 的场景。

zookeeper集群 机器下线 zookeeper集群使用_云原生_25

 

异步

在项目中 利用zk 去达到 订阅 等功能确实很少,但是也可以使用这种方式去解决

zookeeper集群 机器下线 zookeeper集群使用_服务器_26

 削峰填谷

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_27

并且可以利用 zk达到队列的效果 

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_28

实现方式

 主要还是利用 顺序节点 达到 实现队列的 

zookeeper集群 机器下线 zookeeper集群使用_分布式_29

 入队逻辑

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_30

 出队的逻辑

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_31

整体的代码逻辑 

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_32

  •  ZKDistributeQueue 分布式队列实现
  • 使用zk指定的目录作为队列,子节点作为任务。
  • 生产者能往队列中添加任务,消费者能往队列中消费任务。 
  • 持久节点作为队列,持久顺序节点作为任务。 
public class ZkDistributeQueue extends AbstractQueue<String> implements BlockingQueue<String> , java.io.Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	/**
	 * zookeeper客户端操作实例
	 */
	private ZkClient zkClient;
	
	/**
	 *  定义在zk上的znode,作为分布式队列的根目录。
	 */
	private String queueRootNode;
	private static final String default_queueRootNode = "/distributeQueue";
	
	/**队列写锁节点*/
	private String queueWriteLockNode;
	/**队列读锁节点*/
	private String queueReadLockNode;
	/**
	 * 子目录存放队列下的元素,用顺序节点作为子节点。
	 */
	private String queueElementNode;
	
	/**
	 * ZK服务的连接字符串,hostname:port形式的字符串
	 */
	private String zkConnUrl;
	
	private static final String default_zkConnUrl = "localhost:2181";
	
	/** 
	 * 队列容量大小,默认Integer.MAX_VALUE,无界队列。
	 **/
	private static final int default_capacity = Integer.MAX_VALUE;
	private int capacity;
	
	/**
	 * 控制进程访问的分布式锁
	 */
	final Lock distributeWriteLock;
    final Lock distributeReadLock;
    
	public ZkDistributeQueue() {
		this(default_zkConnUrl, default_queueRootNode, default_capacity);
	}
	
	public ZkDistributeQueue(String zkServerUrl, String rootNodeName, int initCapacity) {
		if (zkServerUrl == null) throw new IllegalArgumentException("zkServerUrl");
		if (rootNodeName == null) throw new IllegalArgumentException("rootNodeName");
		if (initCapacity <= 0) throw new IllegalArgumentException("initCapacity");
		this.zkConnUrl = zkServerUrl;
		this.queueRootNode = rootNodeName;
		this.capacity = initCapacity;
		init();
		distributeWriteLock = new ZkDistributeImproveLock(queueWriteLockNode);
		distributeReadLock = new ZkDistributeImproveLock(queueReadLockNode);
	}
	
	/**
	 * 初始化队列信息
	 */
	private void init() {
		queueWriteLockNode = queueRootNode+"/writeLock";
		queueReadLockNode = queueRootNode+"/readLock";
		queueElementNode = queueRootNode+"/element";
		zkClient = new ZkClient(zkConnUrl);
		zkClient.setZkSerializer(new MyZkSerializer());
		if (!this.zkClient.exists(queueElementNode)) {
			try {
				this.zkClient.createPersistent(queueElementNode, true);
			} catch (ZkNodeExistsException e) {
				
			}
		}
	}
	
	/**
	 * 获取队列头部元素,但不执行删除,如果队列为空则返回null。
	 * 
	 */
	@Override
	public String peek() {
		List<String> children = zkClient.getChildren(queueElementNode);
		if(children != null && !children.isEmpty()) {
			children = children.stream().sorted().collect(Collectors.toList());
			String firstChild = children.get(0);
			String elementData = zkClient.readData(queueElementNode+"/"+firstChild);
			return elementData;
		}
		return null;
	}
	/**
	 * 往zk中添加元素
	 * @param e
	 */
	private void enqueue(String e) {
		zkClient.createPersistentSequential(queueElementNode+"/", e);
	}
	/**
	 * 从zk中删除元素
	 * @param e
	 * @return
	 */
	private boolean dequeue(String e) {
		boolean result = zkClient.delete(e);
		return result;
	}
	
	// ==============================特殊值操作============================
	/**
	 * 获取并删除头部元素,如果队列为空则返回null
	 */
	@Override
	public String poll() {
		String firstChild = peek();
		distributeReadLock.lock();
		try {
			if (firstChild == null) {
				return null;
			}
			boolean result = dequeue(firstChild);
			if (!result) {
				return null;
			} 
		} finally {
			distributeReadLock.unlock();
		}
		return firstChild;
	}
	
	/**
	 * 容量足够立即将指定的元素插入到队列中,成功时返回true,容量不够,则返回false。
	 * 
	 */
	@Override
	public boolean offer(String e) {
		checkElement(e);
		// 判断是否可以添加任务,不能则抛出异常
		distributeWriteLock.lock();
		try {
			if(size() < capacity) {
				enqueue(e);
				return true;
			}
		} finally {
			distributeWriteLock.unlock();
		}
		return false;
	}
	
	// ==============================抛出异常操作============================
	/**
	 * 获取并删除队列头部元素,当队列为空时抛出异常
	 */
	@Override
	public String remove() {
		if(size() <= 0) {
			throw new IllegalDistributeQueueStateException(
					IllegalDistributeQueueStateException.State.empty);
		}
		distributeReadLock.lock();
		String firstChild;
		try {
			firstChild = poll();
			if (firstChild == null) {
				throw new IllegalDistributeQueueStateException("移除失败");
			} 
		} finally {
			distributeReadLock.unlock();
		}
		return firstChild;
	}
	
	/**
	 * 容量足够的情况下,将指定的元素加入队列,插入成功返回true,
	 * 容量不足够的情况下,抛出IllegalDistributeQueueStateException异常。
	 */
	@Override
	public boolean add(String e) {
		checkElement(e);
		distributeWriteLock.lock();
		try {
			// 判断是否可以添加任务,不能则抛出异常
			if(size() >= capacity) {
				throw new IllegalDistributeQueueStateException(
						IllegalDistributeQueueStateException.State.full);
				
			}
			return offer(e);
		} catch (Exception e1) {
			e1.printStackTrace();
		} finally {
			distributeWriteLock.unlock();
		}
		return false;
	}
	
	// 阻塞操作
	@Override
	public void put(String e) throws InterruptedException {
		checkElement(e);
		distributeWriteLock.lock();
		try {
			if(size() < capacity) {	// 容量足够
				enqueue(e);
				System.out.println(Thread.currentThread().getName() + "-----往队列放入了元素");
			}else { // 容量不够,阻塞,监听元素出队
				waitForRemove();
				put(e);
			}
		} finally {
			distributeWriteLock.unlock();
		}
	}
	
	/**
	 * 队列容量满了,不能再插入元素,阻塞等待队列移除元素。
	 */
	private void waitForRemove() {
		CountDownLatch cdl = new CountDownLatch(1);
		// 注册watcher
		IZkChildListener listener = new IZkChildListener() {
			@Override
			public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
				if(currentChilds.size() < capacity) {	// 有任务移除,激活等待的添加操作
					cdl.countDown();
					System.out.println(Thread.currentThread().getName() + "-----监听到队列有元素移除,唤醒阻塞生产者线程");
				}
			}
		};
		zkClient.subscribeChildChanges(queueElementNode, listener);
		
		try {
			// 确保队列是满的
			if(size() >= capacity) {
				System.out.println(Thread.currentThread().getName() + "-----队列已满,阻塞等待队列元素释放");
				cdl.await();	// 阻塞等待元素被移除
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		zkClient.unsubscribeChildChanges(queueElementNode, listener);
	}
	
	
	
	@Override
	public String take() throws InterruptedException {
		distributeReadLock.lock();
		try {
			List<String> children = zkClient.getChildren(queueElementNode);
			if(children != null && !children.isEmpty()) {
				children = children.stream().sorted().collect(Collectors.toList());
				String takeChild = children.get(0);
				String childNode = queueElementNode+"/"+takeChild;
				String elementData = zkClient.readData(childNode);
				dequeue(childNode);
				System.out.println(Thread.currentThread().getName() + "-----移除队列元素");
				return elementData;
			}else {
				waitForAdd();		// 阻塞等待队列有元素加入
				return take();
			} 
		} finally {
			distributeReadLock.unlock();
		}
	}
	
	/**
	 * 队列空了,没有元素可以移除,阻塞等待元素添加到队列中。
	 */
	private void waitForAdd() {
		CountDownLatch cdl = new CountDownLatch(1);
		// 注册watcher
		IZkChildListener listener = new IZkChildListener() {
			@Override
			public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
				if(currentChilds.size() > 0) {	// 有任务了,激活等待的移除操作
					cdl.countDown();
					System.out.println(Thread.currentThread().getName() + "-----监听到队列有元素加入,唤醒阻塞消费者线程");
				}
			}
		};
		zkClient.subscribeChildChanges(queueElementNode, listener);
		
		try {
			// 确保队列是空的
			if(size() <= 0) {
				System.out.println(Thread.currentThread().getName() + "-----队列已空,等待元素加入队列");
				cdl.await();	// 阻塞等待
				System.out.println(Thread.currentThread().getName() + "-----队列已有元素,线程被唤醒");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		zkClient.unsubscribeChildChanges(queueElementNode, listener);
	}
	
	private static void checkElement(String v) {
        if (v == null) throw new NullPointerException();
        if("".equals(v.trim())) {
        	throw new IllegalArgumentException("不能使用空格");
        }
        if(v.startsWith(" ") || v.endsWith(" ")) {
        	throw new IllegalArgumentException("前后不能包含空格");
        }
    }
	
	/**
	 * 返回队列中已存在的元素数量
	 */
	@Override
	public int size() {
		int size = zkClient.countChildren(queueElementNode);
		return size;
	}
	
	/**
	 * 暂不支持迭代子
	 */
	@Override
	public Iterator<String> iterator() {
		throw new UnsupportedOperationException();
	}

	// 暂不支持
	@Override
	public boolean offer(String e, long timeout, TimeUnit unit) throws InterruptedException {
		return false;
	}
	

	@Override
	public String poll(long timeout, TimeUnit unit) throws InterruptedException {
		return null;
	}
	
	/**
	 * 返回剩余容量,没有限制返回Integer.MAX_value。
	 */
	@Override
	public int remainingCapacity() {
		int remaining = capacity - size();
		return remaining;
	}

	@Override
	public int drainTo(Collection<? super String> c) {
		return drainTo(c, size());
	}
	
	/**
	 * 从队列移除指定大小的元素,并将移除的元素添加到指定的集合中。
	 */
	@Override
	public int drainTo(Collection<? super String> c, int maxElements) {
		if(c == null) {
			throw new NullPointerException();
		}
		int transferredSize = 0;
		List<String> children = zkClient.getChildren(queueElementNode);
		if(children != null && !children.isEmpty()) {
			List<String> removeElements = children.stream().sorted().limit(maxElements).collect(Collectors.toList());
			transferredSize = removeElements.size();
			c.addAll(removeElements);
			removeElements.forEach((e)->{
				zkClient.delete(e);
			});
		}
		return transferredSize;
	}
	
	
}

zk配置中心

参数配置以及动态修改 

zookeeper集群 机器下线 zookeeper集群使用_zookeeper集群 机器下线_33

 就像dubbo中 利用 zk来实现 配置中心。

zookeeper集群 机器下线 zookeeper集群使用_云原生_34

 将所有的配置项都放到 zk上面

zookeeper集群 机器下线 zookeeper集群使用_云原生_35

 

 

public interface ConfigureWriter {
	/**
	 * 创建一个新的配置文件
	 * @param fileName 文件名称
	 * @param items 配置项
	 * @return 新文件的在zk上的路径
	 */
	String createCnfFile(String fileName, Properties items);
	/**
	 * 删除一个配置文件
	 * @param fileName
	 */
	void deleteCnfFile(String fileName);
	/**
	 * 修改一个配置文件
	 * @param fileName
	 * @param items
	 */
	void modifyCnfItem(String fileName, Properties items);
	/**
	 * 加载配置文件
	 * @param fileName
	 * @return
	 */
	Properties loadCnfFile(String fileName);
}

包括读的部分

public interface ConfigureReader {
	/**
	 * 读取配置文件
	 * @param fileName 配置文件名称
	 * @param ChangeHandler 配置发生变化的处理器
	 * @return 如果存在文件配置,则返回Properties对象,不存在返回null
	 */
	Properties loadCnfFile(String fileName);
	/**
	 * 监听配置文件变化,此操作只需要调用一次。
	 * @param fileName
	 * @param changeHandler
	 */
	void watchCnfFile(String fileName, ChangeHandler changeHandler);
	
	/**
	 * 配置文件变化处理器
	 * ChangeHandler
	 */
	interface ChangeHandler {
		/**
		 * 配置文件发生变化后给一个完整的属性对象
		 * @param newProp
		 */
		void itemChange(Properties newProp);
	}
}

具体的实现

zookeeper集群 机器下线 zookeeper集群使用_分布式_36

 这些功能 基本 在客户端  已经实现了。 可以自己 进行升级修改。