【ZooKeeper】 :原生Java客户端连接zookeeper服务实行操作(ACL,Session,Watcher,Znode)

zookeeper客户端:

  • zk原生客户端
  • Apache curator
    我这里采用原生客户端来连接zookeeper服务。

导入依赖

pom.xml文件添加如下依赖(我这里是zookeeper 3.7.0),我服务端也是3.7.0,最好保持一致。

<dependencies>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.7.0</version>
    </dependency>
</dependencies>

日志文件

在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”

log4j.rootLogger=INFO,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.encoding=UTF-8
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%l] - [%p] %m%n

zookeeper-3.7.0的项目依赖图如下所示,所以我这里可以不用导入log4j的依赖,因为zookeeper-3.7.0.pom文件里面已经引入了
在这里插入图片描述

zookeeper服务的连接

ZKConnect类:
如下实现了zookeeper服务的连接,重连和断开连接,代码有详细注释

public class ZKConnect{

	//日志
	static final Logger log = LoggerFactory.getLogger(ZKConnect.class);
	//server列表,以逗号分割,我这里是使用docker-compose 搭建的单机zookeeper集群
	static final String ZK_SERVER_CLUSTER = "xxxx:2181,xxxx:2181,xxxx:2181";
	// 用以主线程(主线程指这个程序的线程)和其他线程的同步,这里会产生连接zookeeper服务的其他线程,等zookeeper连接成功,主线程才得以继续运行
	static CountDownLatch countDownLatch;
	//连接的超时时间,单位ms
	public static final Integer SESSION_TIMEOUT = 30000;
	// 记录zookeeper的连接耗时
	private static long time;
    //zookeeper 实例,通过zk实现对zookeeper服务的一系列操作
	private ZooKeeper zk;

	/**
	 * @Author codingXT
	 * @Description
	 * @Date  2022/1/11 16:28
	 * @Param
	 * @return null
	 **/
	public class ZkConnectWatcher implements Watcher {
		@Override
		public void process(WatchedEvent event) {

			log.info("进入WatchedEvent");
			if (event.getState() == Event.KeeperState.SyncConnected) {
				//成功与zookeeper server建立连接
				// 放开闸门, wait在connect或reconnect方法上的主线程将被唤醒
				// 连接建立, 回调process接口时, 其event.getState()为Event.KeeperState.SyncConnected
				countDownLatch.countDown();
			}else if(event.getState().equals(Event.KeeperState.Closed)){
				//成功与zookeeper server 断开连接
				//放开闸门, wait在disconnect方法上的主线程将被唤醒
				// 连接断开, 回调process接口时, 其event.getState()为Event.KeeperState.Closed
				countDownLatch.countDown();
			}
//			log.info("事件类型为:{}",event.getType());
//			log.info("事件触发的路径为:{}",event.getPath());
//			log.info("此时zookeeper 的状态为:{}",event.getState().name());
			log.info("接收到watch通知:"+ event);
			log.info("退出WatchedEvent");
		}
	}

	/**
	 * @Author codingXT
	 * @Description 与zookeeper server 建立连接
	 * @Date  2022/1/11 16:27
	 * @Param
	 * @return
	 **/
	public boolean connect(){
		try {
			//连接zookeeper服务端
			//ZkConnectWatcher:通知事件,如果有对应的事件触发,则会收到一个通知;如果不需要,那就设置为null
			countDownLatch = new CountDownLatch(1);
			zk = new ZooKeeper(ZK_SERVER_CLUSTER, SESSION_TIMEOUT, new ZkConnectWatcher());
			log.info("客户端开始连接zookeeper服务器...");
			log.info("连接状态:{}", zk.getState());
			//等待zk连接成功的通知
			countDownLatch.await();
			log.info("连接状态:{}", zk.getState());
			log.info("连接成功");
			return true;
		} catch (InterruptedException | IOException e) {
			log.error("与zookeeper server 建立连接失败"+",errMsg:"+e.getMessage());
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * @Author codingXT
	 * @Description 与zookeeper server 重连,基于Session
	 * @Date  2022/1/11 16:26
	 * @Param
	 * @return
	 **/
	public boolean reconnect(){
		try{
			countDownLatch = new CountDownLatch(1);

			long sessionId = zk.getSessionId();
			String ssid = "0x" + Long.toHexString(sessionId);
			log.info("sessionId 为:{}",ssid);
			byte[] sessionPassword = zk.getSessionPasswd();
			// 开始会话重连
			log.info("开始会话重连...");

			ZooKeeper zkSession = new ZooKeeper(ZK_SERVER_CLUSTER,
					SESSION_TIMEOUT,
					new ZkConnectWatcher(),
					sessionId,
					sessionPassword);

			log.info("重新连接状态zkSession:{}", zkSession.getState());
			countDownLatch.await();
			log.info("重新连接状态zkSession:{}", zkSession.getState());
			log.info("重新连接成功");
			return true;
		} catch (InterruptedException | IOException e) {
			log.error("与zookeeper server 重新连接失败"+",errMsg:"+e.getMessage());
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * @Author codingXT
	 * @Description 与zookeeper server 断开连接
	 * @Date  2022/1/11 16:27
	 * @Param
	 * @return
	 **/
	public boolean disconnect(){
		try{
			countDownLatch  = new CountDownLatch(1);
			//断开与zookeeper服务端的连接
			zk.close();
			//阻塞主线程
			countDownLatch.await();
			return true;
		} catch (InterruptedException e) {
			log.error("与zookeeper server 断开连接失败"+",errMsg:"+e.getMessage());
			e.printStackTrace();
		}
		return false;
	}

	
	public static void main(String[] args) throws Exception {
		ZKConnect  zkConnect = new ZKConnect();

		//记录连接之前的时间
		time = System.currentTimeMillis();
		//连接zookeeper 集群
		if(zkConnect.connect()){
			System.out.printf("zookeeper连接耗时:%d (ms)\n",System.currentTimeMillis()-time);
		}else{
			System.out.println("zookeeper连接失败");
		}

		time = System.currentTimeMillis();
		//重新连接
		if(zkConnect.reconnect()){
			System.out.printf("zookeeper重连耗时:%d (ms)\n",System.currentTimeMillis() - time);
		}else{
			System.out.println("zookeeper重连失败");
		}

		time = System.currentTimeMillis();
		//断开连接
		if(zkConnect.disconnect()){
			System.out.printf("zookeeper断开连接耗时:%d (ms)\n",System.currentTimeMillis() - time);
		}else{
			System.out.println("与zookeeper断开连接失败");
		}
	}
}

ZooKeeper类

我们看到,我们只需要new一个ZooKeeper实例就可以实现到服务端的连接,并通过这个实例我们可以实现对服务端的一系列操作。所有的操作都将通过调用ZooKeeper类的方法来完成。
因此,连接ZooKeeper服务端,直接使用ZooKeeper类new一个实例。

有多种构造ZooKeeper实例的方法,获取到ZooKeeper实例后,我们就可以对zookeeper server进行一系列操作,要注入watcher,这个watcher作为整个zookeeper会话期间的默认watcher,将一直保存在客户端ZKWatcherManager的defaultWatcher中
在这里插入图片描述
ZooKeeper的构造函数参数如下:

  • connectString:逗号分隔的host:port,每个host:port对应一个ZooKeeper服务端,如果需要使用chroot后缀,直接将chroot后缀拼接在host:port的后面即可,如:“xxxx:2181/xt”,之后客户端将基于"/xt"进行操作(操作"/spring/service1"将导致操作在"/xt/spring/service1"上,这样方便进行资源隔离,某个客户端只操作zookeeper树文件系统上的一个树枝)
  • sessionTimeout:以毫秒为单位的Session超时时间
  • watcher:通知事件,如果有对应的事件触发,则会收到一个通知;如果不需要,那就设置为null
  • sessionId:重新连接时使用的Session ID
  • sessionPasswd:重新连接时该Session的密码
  • canBeReadOnly:可读,当这个节点断开后,还是可以读到数据的,只是不能写,此时数据被读取到的可能是旧数据,此处建议设置为false,不推荐使用

还封装了很多操作zookeeper服务端的方法
在这里插入图片描述

Session

而我们一旦与服务端建立连接,就会为客户端(ZooKeeper实例)分配一个Session ID(保存在一个ClientCnxn对象中)。客户端将定期向服务端发送心跳以保持会话有效。只要客户端的Session ID保持有效,客户端就可以调用ZooKeeper API实现一系列操作。

如果由于某种原因,客户端长时间未能向服务端发送心跳(例如超过sessionTimeout值),服务端将使会话过期,Session ID将失效。 客户端对象将不再可用。 要进行ZooKeeper API调用,应用程序必须创建一个新的客户端对象。如果客户端当前连接的ZooKeeper服务端出现故障或没有响应,客户端将在其Session ID到期之前自动尝试连接其他服务端(在集群里这样)。 如果成功,应用程序可以继续使用客户端。

Watcher

zookeeper允许客户端向服务端的Znode注册watcher监听,当服务端的指定事件触发之后,服务端会向注册监听的客户端发送事件通知,客户端可以根据watcher通知状态和事件类型做出业务调整应对

工作流程:

  1. 客户端注册watcher
  2. 服务端注册watcher
  3. 服务端处理watcher
  4. 客户端回调watcher
1.客户端注册watcher
  1. getData,getchildren,exist,三个API,均可以传入watcher对象(里面也有默认的watcher实现),我们也可以通过addWatch方法给指定Znode传入watcher对象
  2. 标记请求request(是否注册了watcher),封装watcher到WatchRegistration
  3. 封装成最小传输单位packet,序列化后,发送到outgoingQueue进行排队,然后网络传输发送到服务端(在Packet传输过程中,ClientCnxn.sendThread只会传输其中的ByteBuffer bb字段:包括header、request,而不包括具体的Watcher内容,以免Zookeeper的服务端内存溢出)
  4. ClientCnxn将packet传输给服务端之后,等待响应并将packet保存在客户端本地的pendingQueue队列
  5. ClientCnxn接收到服务端对这个的响应,如果是OK,则从WatchRegistion中取出对应的Watcher,注册到ZKWatcherManager中

客户端watcher管理者:ZKWatchManager

private final Map<String, Set<Watcher>> dataWatches = new HashMap<>();
private final Map<String, Set<Watcher>> existWatches = new HashMap<>();
private final Map<String, Set<Watcher>> childWatches = new HashMap<>();
private final Map<String, Set<Watcher>> persistentWatches = new HashMap<>();
private final Map<String, Set<Watcher>> persistentRecursiveWatches = new HashMap<>();

下面是客户端请求的发送流程
图片来自https://www.cnblogs.com/wuzhenzhao/p/9994450.html
在这里插入图片描述

2.服务端注册watcher
  1. 处理请求:服务端ServerCnxn接收客户端的请求request,调用FinalRequestProcessor.processRequest方法,查看其中的watcher标志位,看是否要对这个Znode注册watcher
  2. 注册watcher:如果要注册watcher,存储相关信息(保存到服务端的WatchManager,里面维护了两个hashmap,WatchTable和Watch2Paths),服务端的ServerCnxn是一个实现了Watcher接口的类。socket网络传输并没有将客户端具体的自定义watcher给传输过来,所以在服务端WatcherManager中存储的,实际上是ServerCnxn实例(在zookeeper中,NIOServerCnxn,继承了ServerCnxn)

服务端watcher管理者:WatchManager

private final Map<String, Set<Watcher>> watchTable = new HashMap<>();
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>();

如下是一张watcher注册流程图(来自https://blog.csdn.net/sun_tantan/article/details/120430313),图中的String就是zookeeper的节点path
在这里插入图片描述

3.服务端处理watcher
  1. watcher触发,服务端某个指定path(就是Znode的路径)接收到某个命令请求,触发watcher(triggerWatch方法),根据EventType(事件类型),KeeperState.SyncConnected(zookeeper的连接状态)和path(节点路径)构造一个WatchedEvent,构造一个新的Set< Watcher > 集合 watchers,通过遍历迭代器的方式来检查当前路径以及父路径是否有对此 path 感兴趣的
    Watcher,如果有那么将其加入到 watchers 集合中;(具体代码可以自己去WatchManager类里面查看)
  2. 遍历 watchers 容器内的所有 Watcher 实例的 process(WatchedEvent event) 方法,来处理WatchedEvent 事件;
  3. 执行Watcher.process(WatchEvent event)方法(服务端的watcher实现类是NIOServerCnxn)

如下是NIOServerCnxn的process方法

public void process(WatchedEvent event) {
   ReplyHeader h = new ReplyHeader(-1, -1L, 0);
   if (LOG.isTraceEnabled()) {
       ZooTrace.logTraceMessage(LOG, 64L, "Deliver event " + event + " to 0x" + Long.toHexString(this.sessionId) + " through " + this);
   }

   WatcherEvent e = event.getWrapper();
   int responseSize = this.sendResponse(h, e, "notification", (String)null, (Stat)null, -1);
   ServerMetrics.getMetrics().WATCH_BYTES.add((long)responseSize);
}

3.1将 WatchedEvent 转换为 WatcherEvent,前者用于程序内部处理,后者用于事件序列化的发送;
3.2序列化为 ByteBuffer 后加入到 outgoingBuffers 队列中,等待异步线程的消费与发送;

4.客户端回调Watcher

ClientCnxn的SendThread线程来接收服务端的响应。如果此响应带有标志xid=-1,则表示这是一个Event事件,交给EventThread线程去处理回调Watcher:

  1. 构造WatcherEvent实例、并转换为WatchedEvent实例
  2. 通过ZKWatchManager中的注册信息,通过path查找Set< Watcher > watchers。
  3. 将WatchedEvent、watchers、以及path、事件类型等封装为WatcherSetEventPair,加入到EvenrThread.waitingEvents队列。
  4. EvenrThread线程去读取队列中的事件,对每一个Watcher都执行process方法。

watcher特性:轻量级,告诉客户端发生了何种事件,但不会包含事件的具体内容(比如只告诉你事件发生的类型和事件发生的具体路径节点,也就是通知你什么地方发生了什么类型的事)。

如下是Watcher接口,我们下面客户端与服务端建立连接的时候也要注入实现了watcher接口的实例,我们只用实现其中的process方法就好。

@Public
public interface Watcher {
    //WatchedEvent表示Watcher能够响应的ZooKeeper上的更改
    //WatchedEvent包括事件类型、ZooKeeper的当前状态以及事件涉及的znode路径
    void process(WatchedEvent var1);
	
	//watcher类型
    @Public
    public static enum WatcherType {
        Children(1),
        Data(2),
        Any(3);
    }
	
	 事件类型
    @Public
    public interface Event {
        @Public
        public static enum EventType {
            None(-1),
            NodeCreated(1),
            NodeDeleted(2),
            NodeDataChanged(3),
            NodeChildrenChanged(4),
            DataWatchRemoved(5),
            ChildWatchRemoved(6),
            PersistentWatchRemoved(7); 
        }
	
		 ZooKeeper的当前状态
        @Public
        public static enum KeeperState {
            /** @deprecated */
            @Deprecated
            Unknown(-1),
            Disconnected(0),
            /** @deprecated */
            @Deprecated
            NoSyncConnected(1),
            SyncConnected(3),
            AuthFailed(4),
            ConnectedReadOnly(5),
            SaslAuthenticated(6),
            Expired(-112),
            Closed(7);    
        }
    }
}

ACL

在创建Znode的时候,我们需要设置这个Znode的ACL
在这里插入图片描述
在ZooKeeper类中,我们看见create方法的定义有三种实现方式,同时我们注意到传入的ACL是个list,这就说明可以为这个节点设置多个ACL,意思就是说IP1对这个节点有哪些权限,而IP2对这个节点有哪些权限(这里仅拿IP 模式举例,还有其他scheme)。

ACL类

ZooKeeper的原生Java客户端对ACL的定义体现在perms(授权权限,ACL类中是个int类型数据)和Id类(授权策略和授权对象)中。

@Public
public class ACL implements Record {
    private int perms;
    private Id id;
}

perms 的具体权限的定义在ZooDefs类的 Perms接口里面

    @Public
    public interface Perms {
        int READ = 1;
        int WRITE = 2;
        int CREATE = 4;
        int DELETE = 8;
        int ADMIN = 16;
        int ALL = 31;  //刚好是所有权限的相或
    }
权限位权限描述
cCREATE可以创建子节点
dDELETE可以删除子节点(仅直接子节点)
rREAD可以读取节点数据及显示子节点列表
wWRITE可以设置节点数据
aADMIN可以设置节点访问控制列表权限

Id 类

@Public
public class Id implements Record {
    private String scheme;
    private String id;
}

授权策略(Scheme):

  • world:开放模式,world表示任意客户端都可以访问(默认设置)。
  • ip:限定客户端IP防问。
  • auth:只有在会话中通过了认证才可以访问(通过addauth命令)。
  • digest:与auth类似,区别在于auth用明文密码,而digest用SHA1+base64加密后的密码(通过addauth命令,实际场景中digest更常见)。

授权对象(ID)就是指定的授权策略(Scheme)的内容:
比如world:anyone中的anyone、
ip:191.124.2.131中的191.124.2.131、
auth:username:password中的username:password(明文密码)、digest:username:password_digest中的username:password_digest(用SHA1+base64加密后的密码)。

ZooDefs类定义了很多信息,方便进行调用

ZooKeeper的Java客户端中ZooDefs类内置了一些ACL定义(Ids接口中):
除此之外,还内置了watchMode,操作权限Perms,操作码OpCode等种种定义。

public class ZooDefs {
    public static final String CONFIG_NODE = "/zookeeper/config";
    public static final String ZOOKEEPER_NODE_SUBTREE = "/zookeeper/";
    public static final String[] opNames = new String[]{"notification", "create", "delete", "exists", "getData", "setData", "getACL", "setACL", "getChildren", "getChildren2", "getMaxChildren", "setMaxChildren", "ping", "reconfig", "getConfig"};

    public ZooDefs() {
    }

    @Public
    public interface AddWatchModes {
        int persistent = 0;
        int persistentRecursive = 1;
    }

    @Public
    public interface Ids {
        Id ANYONE_ID_UNSAFE = new Id("world", "anyone");
        Id AUTH_IDS = new Id("auth", "");
        @SuppressFBWarnings(
            value = {"MS_MUTABLE_COLLECTION"},
            justification = "Cannot break API"
        )
        ArrayList<ACL> OPEN_ACL_UNSAFE = new ArrayList(Collections.singletonList(new ACL(31, ANYONE_ID_UNSAFE)));
        @SuppressFBWarnings(
            value = {"MS_MUTABLE_COLLECTION"},
            justification = "Cannot break API"
        )
        ArrayList<ACL> CREATOR_ALL_ACL = new ArrayList(Collections.singletonList(new ACL(31, AUTH_IDS)));
        @SuppressFBWarnings(
            value = {"MS_MUTABLE_COLLECTION"},
            justification = "Cannot break API"
        )
        ArrayList<ACL> READ_ACL_UNSAFE = new ArrayList(Collections.singletonList(new ACL(1, ANYONE_ID_UNSAFE)));
    }

    @Public
    public interface Perms {
        int READ = 1;
        int WRITE = 2;
        int CREATE = 4;
        int DELETE = 8;
        int ADMIN = 16;
        int ALL = 31;  //刚好是所有权限的相或
    }

    @Public
    public interface OpCode {
        int notification = 0;
        int create = 1;
        int delete = 2;
        int exists = 3;
        int getData = 4;
        int setData = 5;
        int getACL = 6;
        int setACL = 7;
        ....  省略
    }
}

这样方便给开发人员使用(基本的一些操作直接帮我们写好)
比如create 节点的时候,需要定义这个节点的ACL list,可以直接传这个
意思就是所有访问者都拥有所有权限

ZooDefs.Ids.OPEN_ACL_UNSAFE
实现如下,帮我们new好了
ArrayList<ACL> OPEN_ACL_UNSAFE = new ArrayList(Collections.singletonList(new ACL(31, ANYONE_ID_UNSAFE)));

ZooKeeper的ACL相关方法

ZooKeeper API方法有同步方法和异步方法。同步方法阻塞,直到服务端响应。异步方法只是将请求发送并立即返回。调用异步方法时,会将一个回调实例以参数的方式传给它,该实例将在请求执行成功或错误时被执行,并带有指示错误的状态码(回调方法的rc参数)以及其他和事件相关的其他信息。
基本ZooKeeper类里面所有的方法,都有同步和异步两个版本。setACL和getACL两个方法也是如此,setACL方法在设置节点的ACL后还会返回节点的状态信息,getACL方法返回给定路径下的节点的ACL和状态信息。

// 如果存在这样的节点并且给定的aclVersion与节点的aclVersion匹配
// 则为给定路径的节点设置ACL并且返回节点的状态信息
// 如果不存在给定路径的节点,将抛出错误代码为KeeperException.NoNode的KeeperException
// 如果给定的aclVersion与节点的aclVersion不匹配,则会抛出错误代码为 KeeperException.BadVersion的KeeperException
Stat setACL(final String path, List<ACL> acl, int aclVersion)
// setACL的异步版本
void setACL(final String path, List<ACL> acl, int version, StatCallback cb, Object ctx)
// 返回给定路径的节点的ACL和状态信息
// 如果不存在给定路径的节点,将抛出错误代码为KeeperException.NoNode的KeeperException
// 如果stat参数不为空,则会将节点的状态信息复制到此参数中,其他方法也是类似的方式来完成状态信息的返回
List<ACL> getACL(final String path, Stat stat)
// getACL的异步版本
void getACL(final String path, Stat stat, ACLCallback cb, Object ctx)

同步和异步实现

我们从这里看一看他们同步和异步实现方法的区别:
同步版本getACL:可以看到是直接调用这个方法,然后这个方法马上返回数据给他(从response里面)

    public List<ACL> getACL(String path, Stat stat) throws KeeperException, InterruptedException {
    	//删除了相同的代码部分
        ReplyHeader r = this.cnxn.submitRequest(h, request, response, (ZooKeeper.WatchRegistration)null);
        if (r.getErr() != 0) {
            throw KeeperException.create(Code.get(r.getErr()), path);
        } else {
            if (stat != null) {
                DataTree.copyStat(response.getStat(), stat);
            }

            return response.getAcl();
        }
    }

异步版本getACL:但是这个不一样,并没有直接返回数据给调用方。而是执行了一个其他的方法。

    public void getACL(String path, Stat stat, ACLCallback cb, Object ctx) {
       // 删除了相同的代码部分
        this.cnxn.queuePacket(h, new ReplyHeader(), request, response, cb, path, serverPath, ctx, (ZooKeeper.WatchRegistration)null);
    }

这里cnxn主要负责server和client的交互,也就是网络通信的部分,zookeeper的网络通信应该是使用的netty。cnxn是ClientCnxn的一个实例。原来一开始客户端连接服务端的时候,会生成一个ClientCnxn对象,同时ClientCnxn对象会创建SendThread和EventThread两个线程,用于对request/response以及watcher event进行管理。
queuePacket方法会把我们传入的参数全部打包,然后添加到outgoingQueue(这是一个LinkedBlockingDeque<ClientCnxn.Packet>,这里用来处理request请求)

如下图所示(图来自https://www.cnblogs.com/jing99/p/12722430.html)
在这里插入图片描述
上图的PendingQueue用于存储已经发送等待服务响应的请求,是一个Queue<ClientCnxn.Packet>,常规的ArrayQueue。waitingEventsQueue用处临时存放需要被触发的对象,通过队列的应用就实现了ZooKeeper的高性能

添加认证

addAuthInfo方法将指定的scheme:auth信息添加到客户端与ZooKeeper服务端建立的连接中(ClientCnxn实例中),使用方法如下所示,然后在这个会话里我们才能对需要指定认证的Znode进行操作:

zk.addAuthInfo("digest", "xt:codingxt".getBytes());
zk.addAuthInfo("auth", "xt:codingxt".getBytes());

给某个节点添加ACL之后,这里是digest,在这个连接session里面就需要添加认证,然后才能对这个节点进行操作

//new一个acl实例,只有通过了认证,才能进行操作
ACL acl = new ACL(
        ZooDefs.Perms.ALL,
        new Id("digest",
                DigestAuthenticationProvider.generateDigest("xt:codingXT"))
);

//同步方式添加ACL
zkOperation.zk.setACL("/xtSyn",new ArrayList<>(Collections.singletonList(acl)),-1);

//直接访问会抛异常   org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /xtSyn
//data = zkOperation.zk.getData("/xtSyn",false,stat);
//System.out.println("data: " + new String(data));

//给当前会话,添加认证信息  会添加到ClientCnxn里面
// ClientCnxn:是 Zookeeper 客户端和 Zookeeper 服务器端进行通信和事件通知处理的主要类,里面维护了很多信息
//ClientCnxn包含两个线程
//SendThread :负责客户端和服务器端的数据通信, 也包括事件信息的传输
//EventThread : 主要在客户端回调注册的 Watchers 进行通知处理
zkOperation.zk.addAuthInfo("digest", "xt:codingXT".getBytes());
data = zkOperation.zk.getData("/xtSyn",false,stat);
System.out.println("data: " + new String(data));

Znode的一系列操作

异步回调接口AsyncCallback

定义了各种回调接口,在Znode的一些方法的异步实现上,需要我们实现这些接口,重写其中的processResult方法。
注意:下面的代码之所以是这个样子,需要下载sources,下载之后就可以看见全部代码了(包含注释)
在这里插入图片描述

public interface AsyncCallback {
    public interface EphemeralsCallback extends AsyncCallback {
        void processResult(int var1, Object var2, List<String> var3);
    }

    @Public
    public interface MultiCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, List<OpResult> var4);
    }

    @Public
    public interface VoidCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3);
    }

    @Public
    public interface StringCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, String var4);
    }

    @Public
    public interface Create2Callback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, String var4, Stat var5);
    }

    @Public
    public interface Children2Callback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, List<String> var4, Stat var5);
    }

    @Public
    public interface ChildrenCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, List<String> var4);
    }

    @Public
    public interface ACLCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, List<ACL> var4, Stat var5);
    }

    @Public
    public interface DataCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, byte[] var4, Stat var5);
    }

    @Public
    public interface AllChildrenNumberCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, int var4);
    }

    @Public
    public interface StatCallback extends AsyncCallback {
        void processResult(int var1, String var2, Object var3, Stat var4);
    }
}

创建节点

在这里插入图片描述
使用create方法创建节点(异步创建,ZooKeeper的Java客户端方法大部分都有异步版本,所以需要使用CountDownLatch来协调多线程执行顺序,子线程执行完毕,主线程继续向下执行,避免出错),参数如下:

  • String path:该节点的路径。
  • byte[] data:该节点存储的数据。
  • List acl:该节点的ACL列表。
  • createMode:该节点的类型(持久类型、临时类型、容器类型等)。
  • StringCallback cb:创建该节点成功或者失败后的回调(可通过Lambda表达式指定)。
  • Object ctx:传递给异步调用的上下文对象。
  • long ttl:创建TTL节点时,用于指定TTL节点的TTL时间,创建其他类型的节点时,指定该参数会导致节点创建不成功(除非指定为-1)。

节点类型 CreateMode枚举类,有7种(可参考Zookeeper 重要概念以及基本命令使用Znode章节):

public enum CreateMode {
    PERSISTENT(0, false, false, false, false),
    PERSISTENT_SEQUENTIAL(2, false, true, false, false),
    EPHEMERAL(1, true, false, false, false),
    EPHEMERAL_SEQUENTIAL(3, true, true, false, false),
    CONTAINER(4, false, false, true, false),
    PERSISTENT_WITH_TTL(5, false, false, false, true),
    PERSISTENT_SEQUENTIAL_WITH_TTL(6, false, true, false, true);
}

这里create有两种异步回调实例,一个是StringCallback接口,另一个是Create2Callback接口,我们只需要重写这些接口的processResult方法。
服务端执行完毕后,会调用这些实例的processResult方法来告诉客户端,服务端执行的结果

Create2Callback 会比StringCallback多一个Stat对象

    @InterfaceAudience.Public
    interface StringCallback extends AsyncCallback {
        void processResult(int rc, String path, Object ctx, String name);
    }

    @InterfaceAudience.Public
    interface Create2Callback extends AsyncCallback {
        void processResult(int rc, String path, Object ctx, String name, Stat stat);
    }
  • rc:返回码或调用结果。
  • path:传递给异步调用的路径。
  • ctx:传递给异步调用的上下文对象。
  • name:创建的节点的名称。 节点创建成功时,name和path通常相等,除非创建顺序节点。
  • stat :给定path上的节点的Stat对象(Stat对象就是节点的状态信息)。

和终端命令上获取到的stat信息基本一致。

@Public
public class Stat implements Record {
    private long czxid;
    private long mzxid;
    private long ctime;
    private long mtime;
    private int version;
    private int cversion;
    private int aversion;
    private long ephemeralOwner;
    private int dataLength;
    private int numChildren;
    private long pzxid;
    ...
}

调用ZooKeeper原生create方法
注意:zookeeper的原生java api是不会自动递归创建节点的,即当父节点不存在时,不可以创建子节点
同步方式代码
因为create会直接返回结果,线程这里会被阻塞(不会引发并发问题),所以我没加多线程工具类CountDownLatch

//同步方式   ACL 权限可以自己定义,我这里图方便就直接使用的ZooDefs里面定义好的  这里是创建持久节点
String result = "";
result = zkOperation.zk.create("/xtSyn", "I am SynCreate".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
System.out.println("创建节点:\t" + result + "\t成功..."); //同步方式可以直接获取结果 create方法会被阻塞,直到获取结果

异步方式代码
create为void方法,里面调用了其他方法继续执行(将网络通信包添加到了阻塞队列里面),所以会引发并发安全问题,所以需要CountDownLatch进行协调,直到异步回调方法被执行完毕(说明已经创建节点完毕),主线程(create 节点的线程)方可继续执行

 //异步方式
 String ctx;
 //ctx 是object类型,所以可以传入任何类型
 ctx="{'create':'success'}";
 countDownLatch = new CountDownLatch(1);
 zkOperation.zk.create("/xtAsyn", "hello,I am AsynCreate".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,new CreateCallBack(countDownLatch),ctx);
 //阻塞这个线程,直到异步回调方法被执行
 countDownLatch.await();
 System.out.println("成功创建节点");

如上代码所示,需要new一个回调接口(create方法是上面介绍的两种回调接口StringCallback和Create2Callback)的实例.
我的实现如下,重写processResult()方法,服务端创建完毕后,这个方法会被执行。
CreateCallBack(创建节点的异步回调接口实现类)

public class CreateCallBack implements StringCallback {

	private CountDownLatch countDownLatch;

	public CreateCallBack(CountDownLatch countDownLatch){
		super();
		this.countDownLatch = countDownLatch;
	}

	@Override
	public void processResult(int rc, String path, Object ctx, String name) {

		System.out.println("-------------创建节点的异步回调-------------------");
		System.out.println("返回码为:"+rc);
		System.out.println("创建节点: " + path);
		System.out.println("传递给异步调用的上下文对象:"+(String)ctx);
		System.out.println("创建的节点的名称:"+name);
		System.out.println("-------------创建节点的异步回调-------------------");
		//拿到结果之后,就不用阻塞create 节点的线程了。 这个processResult方法里面,可以写其他的方法,来执行创建成功或者失败之后的代码
		countDownLatch.countDown();
	}

}

也可以实现Create2Callback接口,这样就可以多拿一个Stat的数据

更新节点数据

在这里插入图片描述
setData:给指定节点设置数据,就和更新一样。一样的有同步方式和异步方式,我就不全展示了,自己实现对应回调接口,重写processResult方法就可以了
同步方式代码
更新指定path的节点的数据,注意版本号要匹配(每个节点都拥有的属性,保存在stat里面,版本号类似于乐观锁,如果给定的版本为-1,则它与节点的任何版本匹配,你可以理解为耍赖皮)

//更新节点数据
//setData 就是更新数据,这里也有同步和异步的实现方式   这里version要对应zookeeper 该节点的dataversion,相当于一个乐观锁,version号不对,会报错,设置-1也可以访问
Stat status  = zkOperation.zk.setData("/xtSyn", "hello,xt,nice to meet you".getBytes(), -1);
//status 是修改这个节点之后返回的这个节点的相关信息
System.out.println(status.toString());
System.out.println("更新节点数据成功");

获取节点数据

在这里插入图片描述

getData:方法返回给定路径的节点的数据和状态信息(类似ZooKeeper客户端的get -s命令)。如果watch为true并且调用成功(也可以传入一个Watcher实例),则watch将留在给定路径的节点上。watch将由在节点上设置数据或删除节点的成功操作触发。
同步方式代码:

 //获取节点数据 同步方式(也有异步方式)
 stat = new Stat();
 byte[] data = zkOperation.zk.getData("/xtSyn",false,stat);
 System.out.println("data: " + new String(data));

是否存在一个节点

在这里插入图片描述
exists:方法返回给定路径的节点的状态(类似ZooKeeper客户端的stat命令)。如果不存在这样的节点,则返回null。如果watch为 true并且调用成功(也可以传入一个Watcher实例),则watch将留在给定路径的节点上。 watch将由创建、删除节点或在节点上设置数据的成功操作触发。
同步方式代码:

  //判断某个节点是否存在 同步方式
  stat = new Stat();
  stat = zkOperation.zk.exists("/xtSyn",false);
  System.out.println(stat.toString());

删除节点

在这里插入图片描述
delete:方法删除给定路径的节点。 同样的有版本号。
同步方式的delete方法是个void方法,不好判断是否删除
所以我这里展示异步方法(文章末尾会放出全部代码,这里回调接口实现类就不贴了)
异步方式代码:

 ctx = "{'delete':'success'}";
 countDownLatch = new CountDownLatch(1);
 zkOperation.zk.delete("/xtSyn", -1, new DeleteCallBack(countDownLatch), ctx);
 countDownLatch.await();
 System.out.println("成功删除节点");

获取子节点

在这里插入图片描述
getChildren:返回给定路径的节点的子节点列表。如果watch为true并且调用成功(也可以传入一个Watcher实例),则watch将留在给定路径的节点上。 删除给定路径的节点或在节点下创建、删除子节点的成功操作将触发watch。返回的子节点列表未排序。
同步方式代码:

 //获取子节点
 //同步方式
 List<String> children =  zkOperation.zk.getChildren("/",false);
 for(String ch:children){
     System.out.println(ch);
 }

获取子节点数量

在这里插入图片描述
getAllChildrenNumber:同步或者异步获取特定路径下所有子节点的数量。

同步方式代码:

 //同步或者异步获取特定路径下所有子节点的数量
 //同步方式
 int number = zkOperation.zk.getAllChildrenNumber("/");
 System.out.printf("该路径下有%d个节点",number);

获取临时节点(此会话)

在这里插入图片描述
getEphemerals:同步或者异步获取所有在此会话创建的与prefixPath路径匹配的临时节点(不使用prefixPath路径参数就是获取此会话创建的所有临时节点 )。 如果prefixPath 是"/"那么它会返回所有的临时节点(不包括其他客户端上创建的临时节点)。

同步方式代码:

//获取此会话创建的临时节点,可以指定前缀路径prefixPath,不指定前缀路径,就是所有临时节点(只包含此会话创建的)
List<String> ephemerals = zkOperation.zk.getEphemerals();
for(String ephemeral : ephemerals){
    System.out.println(ephemeral);
}

给节点添加watcher

我们通过上面的操作,知道getData,exists,getChildren方法里面可以在这个节点上传入watcher,或者设置watcher的标志位(true则添加默认实现的watcher实例来进行监听事件的触发回调),不过这些watcher都是一次性的,被成功触发完之后就没有了。(这也是为了减轻开销,如果所有节点的watcher都是永久的话,服务端会不停的向客户端发送事件通知,对于网络和服务端客户端来说都会造成不小的开销)

这种方式添加的watcher只能触发一次,但是通过addWatch方法添加到节点上的Watcher可以永久存在。

addWatch:给指定节点添加永久watcher(对于特殊节点来说确实会比较方便)
在这里插入图片描述

// 使用给定的模式向给定的ZNode添加Watcher
// 此方法只能设置AddWatchMode中的模式
void addWatch(String basePath, Watcher watcher, AddWatchMode mode)
// 使用给定的模式向给定的ZNode添加Watcher
// 此方法只能设置AddWatchMode中的模式
// 使用了默认的Watcher(创建ZooKeeper实例时传入的Watcher),其他方法类似
void addWatch(String basePath, AddWatchMode mode)
// addWatch(String, Watcher, AddWatchMode)异步版本
void addWatch(String basePath, Watcher watcher, AddWatchMode mode, VoidCallback cb, Object ctx)
// addWatch(String, AddWatchMode)异步版本
void addWatch(String basePath, AddWatchMode mode, VoidCallback cb, Object ctx)

AddWatchMode枚举类:

public enum AddWatchMode {

    // 在给定的路径上设置一个在触发时不会被移除的Watcher(即它保持活动状态直到被移除)
    // 该Watcher对data和child两类事件进行触发
    // 该Watcher的行为就像在给定路径的ZNode上放置一个exists() Watcher和一个getChildren() Watcher一样
    // 要移除该Watcher,需要使用removeWatches()和WatcherType.Any
    PERSISTENT(0),

    // 在给定的路径上设置一个Watcher
    // a) 触发时不会被移除(即它保持活动状态直到被移除)
    // b) 不仅适用于注册路径,而且递归地适用于所有子路径
    // 该Watcher对data和child两类事件进行触发
    // 该Watcher的行为就像在给定路径的ZNode上放置一个exists() Watcher和一个getChildren() Watcher一样
    // 要删除该Watcher,需要使用removeWatches()和WatcherType.Any
    // 注意:当有递归监听时,性能会略有下降,因为必须检查ZNode路径的所有子路径以进行事件监听
    PERSISTENT_RECURSIVE(1);
}

AddWatchMode.PERSISTENT类型的Watcher只会监听注册节点的相关事件(节点数据更改NodeDataChanged、子节点列表更改NodeChildrenChanged以及节点删除NodeDeleted等),而不会监听注册节点的子节点的相关事件。

AddWatchMode.PERSISTENT_RECURSIVE类型的Watcher不仅监听注册节点的相关事件,还会递归地监听注册节点的所有子节点的相关事件。

客户端向服务端注册watcher的时候,并不会把客户端真实的watcher对象通过网络传输到服务端,而只是在request中做一个标记

request.setWatch(watcher != null);

给/xt节点添加永久watcher,如下示例只监听他自己,自己更换AddWatchMode.PERSISTENT_RECURSIVE再次尝试
代码:

//给某个节点添加watcher
//这里是永久的watcher
countDownLatch = new CountDownLatch(1);
AtomicBoolean isOk = new AtomicBoolean(false);
//还一种模式AddWatchMode.PERSISTENT_RECURSIVE  监听这个节点的所有子节点的事件(包括他自己)
ctx="{'addWatch':'success'}";
//这里没传入VoidCallback 回调接口的实现类,传入的是lambda
zkOperation.zk.addWatch("/xt", new ZkWatcher("添加watch的watcher"), AddWatchMode.PERSISTENT,
        (rc, path, ctxCallBack) -> {
            System.out.println("-----------------addWatch的异步回调------------------");
            System.out.println(rc);
            System.out.println("返回码为:"+rc);
            System.out.println("创建节点: " + path);
            System.out.println("传递给异步调用的上下文对象:"+(String)ctxCallBack);

            if(rc == KeeperException.Code.OK.intValue()) {
                System.out.println("添加watch成功");
                isOk.set(true);
            }
            System.out.println("-----------------addWatch的异步回调------------------");
            countDownLatch.countDown();
        },
        ctx);

//如果给/xt 节点添加watch成功
if(isOk.get()) {
    //全部为同步方式
    zkOperation.zk.setData("/xt", "new data".getBytes(), -1);
    zkOperation.zk.create("/xt/son1", "son1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    zkOperation.zk.getData("/xt/son1", true, null);
    zkOperation.zk.create("/xt/son1/grandson1", "grandson1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
    zkOperation.zk.setData("/xt/son1", "new son1".getBytes(), -1);
    zkOperation.zk.setData("/xt/son1", "new son2".getBytes(), -1);
    zkOperation.zk.setData("/xt/son1/grandson1", "new grandson1".getBytes(), -1);
    zkOperation.zk.delete("/xt/son1/grandson1", -1);
    zkOperation.zk.delete("/xt/son1", -1);
    zkOperation.zk.delete("/xt", -1);
}

删除节点上的watcher

与上面添加永久watcher相对应的是,我们需要手动删除这些节点上的watcher(当我们不需要在这个节点上有watcher的时候)

// 对于给定的ZNode路径(path参数),删除指定类型(watcherType参数)的指定Watcher(watcher参数,因此watcher参数不能为空)
void removeWatches(String path, Watcher watcher, WatcherType watcherType, boolean local)
// 异步版本
void removeWatches(String path, Watcher watcher, WatcherType watcherType, boolean local, VoidCallback cb,Object ctx)
// 对于给定的ZNode路径(path参数),删除指定类型(watcherType参数)的所有Watcher(没有Watcher的限制)
void removeAllWatches(String path, WatcherType watcherType, boolean local)
// 异步版本
void removeAllWatches(String path, WatcherType watcherType, boolean local, VoidCallback cb, Object ctx)

path:节点的路径。
watcher:一个具体的Watcher,
watcherType:要移除的Watcher类型。removeWatches要传入zookeeper的默认watcher实例(需要同一个实例,以前在zookeeper连接传入的watcher实例,或者是使用register方法替换掉的默认watcher实例)
local:没有服务端连接时,是否可以在本地移除Watcher。
VoidCallback:异步回调实例。

WatcherType枚举类:
定义在Watcher接口中,里面还封装了KeeperState(zookeeper状态的枚举类)和Event(事件的枚举类)

 enum WatcherType {
     Children(1),
     Data(2),
     Any(3);
 }

getData、getChildren以及exists三个方法都可以在节点上留下一次性Watcher,而这些Watcher的类型分别是Data、Children和Data,而通过addWatch方法可以在节点上添加持久Watcher(PERSISTENT和PERSISTENT_RECURSIVE),这些Watcher的类型是Any。删除类型为Any的Watcher,也会一起删除类型为Children和Data的Watcher。
代码:

zkOperation.zk.getData("/xt",true,null);
//同步方式
//getData 留下的是Data类型的Watcher  传入的必须是同一个watcher实例,不然会报错,里面会WatchDeregistration,所以要传入以前注册的watcher实例
zkOperation.zk.removeWatches("/xt",zkOperation.connectWatcher, Watcher.WatcherType.Data,false);

zkOperation.zk.getChildren("/xt",true);
//getChildren 留下的是Children类型的Watcher
zkOperation.zk.removeWatches("/xt", zkOperation.connectWatcher, Watcher.WatcherType.Children,false);

zkOperation.zk.exists("/xt",true);
//exists 留下的是Data类型的Watcher
zkOperation.zk.removeWatches("/xt", zkOperation.connectWatcher, Watcher.WatcherType.Any,false);

//这行命令,把addwatch添加的newWatcher删掉了,下一行命令就会报错
//zkOperation.zk.removeWatches("/xt",newWatcher, Watcher.WatcherType.Any,false);

//移除在/xt 上面使用addWatch 添加的watcher , 类型分别是PersistentWatch和PERSISTENT_RECURSIVE
// false表示没有服务端连接时,不可以在本地移除Watcher,因为服务端和客户端都会注册watcher实例
zkOperation.zk.removeAllWatches("/xt", Watcher.WatcherType.Any,false);

这些都最终会交给ZKWatchManager的removeWatcher方法去处理,我们从如下代码可以看出,是怎么根据watcherType来删除watcher的

switch(watcherType) {
case Children:
    synchronized(this.childWatches) {
        removedWatcher = this.removeWatches(this.childWatches, watcher, clientPath, local, rc, childWatchersToRem);
        break;
    }
case Data:
    synchronized(this.dataWatches) {
        removedWatcher = this.removeWatches(this.dataWatches, watcher, clientPath, local, rc, dataWatchersToRem);
    }

    synchronized(this.existWatches) {
        removedPersistentRecursiveWatcher = this.removeWatches(this.existWatches, watcher, clientPath, local, rc, dataWatchersToRem);
        removedWatcher |= removedPersistentRecursiveWatcher;
        break;
    }
case Any:
    synchronized(this.childWatches) {
        removedWatcher = this.removeWatches(this.childWatches, watcher, clientPath, local, rc, childWatchersToRem);
    }

    synchronized(this.dataWatches) {
        removedPersistentRecursiveWatcher = this.removeWatches(this.dataWatches, watcher, clientPath, local, rc, dataWatchersToRem);
        removedWatcher |= removedPersistentRecursiveWatcher;
    }

    synchronized(this.existWatches) {
        removedPersistentRecursiveWatcher = this.removeWatches(this.existWatches, watcher, clientPath, local, rc, dataWatchersToRem);
        removedWatcher |= removedPersistentRecursiveWatcher;
    }

    synchronized(this.persistentWatches) {
        removedPersistentRecursiveWatcher = this.removeWatches(this.persistentWatches, watcher, clientPath, local, rc, persistentWatchersToRem);
        removedWatcher |= removedPersistentRecursiveWatcher;
    }

    synchronized(this.persistentRecursiveWatches) {
        removedPersistentRecursiveWatcher = this.removeWatches(this.persistentRecursiveWatches, watcher, clientPath, local, rc, persistentWatchersToRem);
        removedWatcher |= removedPersistentRecursiveWatcher;
    }
}

注册默认watcher

调用ZooKeeper的getData、getChildren以及exists时,可以通过布尔参数boolean watch来判断是否给指定节点注册Watcher,而不是传入自己new的Watcher实例,而是使用默认Watcher实例来进行监听事件的触发回调(在创建ZooKeeper实例时被传入,这就是默认的Watcher实例)

但是默认的Watcher实例也可以替换,register方法(synchronized修饰)用于注册最新的默认Watcher,会替换以前的Watcher。但是注意到这个操作可能会有多个线程来同时执行,所以要注意并发安全问题。

ZooKeeper类中的register方法。

 public synchronized void register(Watcher watcher) {
     this.getWatchManager().setDefaultWatcher(watcher);
 }

defaultWatcher是ZKWatchManage的字段,所有的客户端Watcher实例由ZKWatchManage统一管理,ZKWatchManager对象注入到ClientCnxn对象中,而ClientCnxn对象注入到ZooKeeper对象,所以我们可以通过ZooKeeper对象更换默认的Watcher实例

volatile修饰符保证了defaultWatcher属性的可见性(在多个线程使用的时候,看到的都是同样的defaultWatcher),因此,只需要保证register方法的原子性即可,而register方法被synchronized修饰(具有原子性)。

private volatile Watcher defaultWatcher;

zookeeper操作代码汇总

创建节点的异步回调接口实现类CreateCallBack

public class CreateCallBack implements StringCallback {

	private CountDownLatch countDownLatch;

	public CreateCallBack(CountDownLatch countDownLatch){
		super();
		this.countDownLatch = countDownLatch;
	}

	@Override
	public void processResult(int rc, String path, Object ctx, String name) {

		System.out.println("-------------创建节点的异步回调-------------------");
		System.out.println("返回码为:"+rc);
		System.out.println("创建节点: " + path);
		System.out.println("传递给异步调用的上下文对象:"+(String)ctx);
		System.out.println("创建的节点的名称:"+name);
		System.out.println("-------------创建节点的异步回调-------------------");
		//拿到结果之后,就不用阻塞create 节点的线程了。 这个processResult方法里面,可以写其他的方法,来执行创建成功或者失败之后的代码
		countDownLatch.countDown();
	}

}

删除节点的异步回调接口实现类DeleteCallBack

public class DeleteCallBack implements VoidCallback {


	private CountDownLatch countDownLatch;

	public DeleteCallBack(CountDownLatch countDownLatch){
		super();
		this.countDownLatch = countDownLatch;
	}

	@Override
	public void processResult(int rc, String path, Object ctx) {
		System.out.println("-------------删除节点的异步回调-------------------");
		System.out.println("删除节点" + path);
		System.out.println((String)ctx);
		System.out.println("-------------删除节点的异步回调-------------------");
		countDownLatch.countDown();
	}
}

实现一系列zookeeper操作的ZKOperation类

public class ZKOperation {

    //日志
    static final Logger log = LoggerFactory.getLogger(ZKOperation.class);
    //server列表,以逗号分割,我这里是使用docker-compose 搭建的单机zookeeper集群
    static final String ZK_SERVER_CLUSTER = "81.68.82.48:2181,81.68.82.48:2182,81.68.82.48:2183";
    // 用以主线程(主线程指这个程序的线程)和其他线程的同步,这里会产生连接zookeeper服务的其他线程,等zookeeper连接成功,主线程才得以继续运行
    static CountDownLatch countDownLatch;
    //连接的超时时间,单位ms
    private static final Integer SESSION_TIMEOUT = 30000;
    // 记录zookeeper的连接耗时
    private static long time;
    //zookeeper 实例,通过zk实现对zookeeper服务的一系列操作
    private ZooKeeper zk;
    // 保存节点信息的类
    private static Stat stat;
    // 连接zookeeper的watcher,后面会作为他的默认watcher
    private ZkWatcher connectWatcher;



    /**
     * @Author codingXT
     * @Description 实现了Watcher接口,通过process方法,可以监听WatchedEvent的触发
     * @Date  2022/1/11 16:28
     * @Param
     * @return null
     **/
    static class ZkWatcher implements Watcher {

        private String watcherName;

        public ZkWatcher(String watcherName){
            super();
            this.watcherName = watcherName;
        }

        @Override
        public void process(WatchedEvent event) {
            System.out.println("----------------------发生了watcher----------------------");
            System.out.println("watcher的名字为:"+watcherName);
            System.out.println("事件类型:"+event.getType());
            log.info("进入WatchedEvent");
            if(event.getType() == Event.EventType.NodeDataChanged){
                log.warn("zookeeper节点数据发生了变化:{}",event.getPath());
            } else if(event.getType() == Event.EventType.NodeCreated) {
                log.warn("创建了新的zookeeper节点:{}",event.getPath());
            } else if(event.getType() == Event.EventType.NodeChildrenChanged) {
                log.warn("子节点发生了变化:{}",event.getPath());
            } else if(event.getType() == Event.EventType.NodeDeleted) {
                log.warn("zookeeper节点被删除:{}",event.getPath());
            } else if (event.getState() == Event.KeeperState.SyncConnected) {
                // 放开闸门, wait在connect或reconnect方法上的主线程将被唤醒
                // 连接建立, 回调process接口时, 其event.getState()为Event.KeeperState.SyncConnected
                log.info("成功与zookeeper server建立连接");
            }else if(event.getState().equals(Event.KeeperState.Closed)){
                //放开闸门, wait在disconnect方法上的主线程将被唤醒
                // 连接断开, 回调process接口时, 其event.getState()为Event.KeeperState.Closed
                log.info("成功与zookeeper server 断开连接");
            }
//			log.info("事件类型为:{}",event.getType());
//			log.info("事件触发的路径为:{}",event.getPath());
//			log.info("此时zookeeper 的状态为:{}",event.getState().name());
            log.info("接收到watch通知:"+ event);
            countDownLatch.countDown();
            log.info("退出WatchedEvent");

            System.out.println("----------------------watcher完毕----------------------");
        }

    }


    /**
     * @Author codingXT
     * @Description 与zookeeper server 建立连接
     * @Date  2022/1/11 16:27
     * @Param
     * @return
     **/
    private boolean connect(){
        try {
            //连接zookeeper服务端
            //ZkWatcher:通知事件,如果有对应的事件触发,则会收到一个通知;如果不需要,那就设置为null
            countDownLatch = new CountDownLatch(1);

            zk = new ZooKeeper(ZK_SERVER_CLUSTER, SESSION_TIMEOUT, connectWatcher);
            log.info("客户端开始连接zookeeper服务器...");
            log.info("连接状态:{}", zk.getState());
            //等待zk连接成功的通知
            countDownLatch.await();
            log.info("连接状态:{}", zk.getState());
            log.info("连接成功");
            return true;
        } catch (InterruptedException | IOException e) {
            log.error("与zookeeper server 建立连接失败"+",errMsg:"+e.getMessage());
            e.printStackTrace();
        }
        return false;
    }



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

        ZKOperation  zkOperation = new ZKOperation();
        zkOperation.connectWatcher =new ZkWatcher("zookeeper连接watcher");

        time = System.currentTimeMillis();
        //连接zookeeper 集群
        if(zkOperation.connect()){
            System.out.printf("zookeeper连接耗时:%d (ms)\n",System.currentTimeMillis()-time);
        }else{
            System.out.println("zookeeper连接失败");
        }


        //创建节点  有同步方式和异步方式  同步方式直接返回数据,异步方式为void,会执行new CreateCallBack()的processResult方法来告知成功
        //同步方式   ACL 权限可以自己定义,我这里图方便就直接使用的ZooDefs里面定义好的  这里是创建持久节点
        String result = "";
        result = zkOperation.zk.create("/xtSyn", "I am SynCreate".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println("创建节点:\t" + result + "\t成功..."); //同步方式可以直接获取结果 create方法会被阻塞,直到获取结果

        //创建节点异步方式
        String ctx;
        //ctx 是object类型,所以可以传入任何类型
        ctx="{'create':'success'}";
        countDownLatch = new CountDownLatch(1);
        zkOperation.zk.create("/xtAsyn", "hello,I am AsynCreate".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT,new CreateCallBack(countDownLatch),ctx);
        //阻塞这个线程,直到异步回调方法被执行
        countDownLatch.await();
        System.out.println("成功创建节点");


        //更新节点数据
        //setData 就是更新数据,这里也有同步和异步的实现方式   这里version要对应zookeeper 该节点的dataversion,相当于一个乐观锁,version号不对,会报错,设置-1也可以访问
        stat  = zkOperation.zk.setData("/xtSyn", "hello,xt,nice to meet you".getBytes(), -1);
        //status 是修改这个节点之后返回的这个节点的相关信息
        System.out.println("/xtSyn的stat数据为:"+stat.toString());
        System.out.println("更新节点数据成功");

        //获取节点数据 同步方式(也有异步方式)
        //可以添加watcher,或者设置true,里面添加默认实现的watcher,这个watcher是一次性的,被触发之后就没了
        stat = new Stat();
        byte[] data = zkOperation.zk.getData("/xtSyn",false,stat);
        System.out.println("data: " + new String(data));


        //判断某个节点是否存在 同步方式
        //可以添加watcher,或者设置true,里面添加默认实现的watcher,这个watcher是一次性的,被触发之后就没了
        stat = new Stat();
        stat = zkOperation.zk.exists("/xtSyn",false);
        if(stat!=null){
            System.out.println("存在/xtSyn节点");
            System.out.println("/xtSyn的stat数据为:"+stat.toString());
        }

        //获取子节点
        //同步方式
        //可以添加watcher,或者设置true,里面添加默认实现的watcher,这个watcher是一次性的,被触发之后就没了
        List<String> children =  zkOperation.zk.getChildren("/",false);
        for(String ch:children){
            System.out.println(ch);
        }

        //获取特定路径下所有子节点的数量(同步或者异步)
        //同步方式
        int number = zkOperation.zk.getAllChildrenNumber("/");
        System.out.printf("该路径下有%d个节点",number);


        //获取此会话创建的临时节点,可以指定前缀路径prefixPath,不指定前缀路径,就是所有临时节点(只包含此会话创建的)
        List<String> ephemerals = zkOperation.zk.getEphemerals();
        for(String ephemeral : ephemerals){
            System.out.println(ephemeral);
        }

        //new一个acl实例,只有通过了认证,才能进行操作
        ACL acl = new ACL(
                ZooDefs.Perms.ALL,
                new Id("digest",
                        DigestAuthenticationProvider.generateDigest("xt:codingXT"))
        );

        //同步方式添加ACL
        zkOperation.zk.setACL("/xtSyn",new ArrayList<>(Collections.singletonList(acl)),-1);

        //直接访问会抛异常   org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /xtSyn
//        data = zkOperation.zk.getData("/xtSyn",false,stat);
//        System.out.println("data: " + new String(data));

        //给当前会话,添加认证信息  会添加到ClientCnxn里面
        // ClientCnxn:是 Zookeeper 客户端和 Zookeeper 服务器端进行通信和事件通知处理的主要类,里面维护了很多信息
        //ClientCnxn包含两个线程
        //SendThread :负责客户端和服务器端的数据通信, 也包括事件信息的传输
        //EventThread : 主要在客户端回调注册的 Watchers 进行通知处理
        zkOperation.zk.addAuthInfo("digest", "xt:codingXT".getBytes());
        data = zkOperation.zk.getData("/xtSyn",false,stat);
        System.out.println("data: " + new String(data));



        //创建节点异步方式
        //ctx 是object类型,所以可以传入任何类型
        // ACL 权限里面可以仿照着ZooDefs.Ids里面的定义来写
        ctx="{'create':'success'}";
        countDownLatch = new CountDownLatch(1);
        zkOperation.zk.create("/xt", "hello,I am xt".getBytes(),new ArrayList<>(Collections.singletonList(acl)), CreateMode.PERSISTENT,new CreateCallBack(countDownLatch),ctx);
        //阻塞这个线程,直到异步回调方法被执行
        countDownLatch.await();
        System.out.println("成功创建节点");

        //zkOperation.zk.addAuthInfo("digest", "xt:codingXT".getBytes());  上面已经给这个会话添加认证了

        //给某个节点添加watcher
        //这里是永久的watcher
        countDownLatch = new CountDownLatch(1);
        AtomicBoolean isOk = new AtomicBoolean(false);
        //还一种模式AddWatchMode.PERSISTENT_RECURSIVE  监听这个节点的所有子节点的事件(包括他自己)
        ctx="{'addWatch':'success'}";
        ZkWatcher newWatcher = new ZkWatcher("添加watch的watcher");
        //这里没传入VoidCallback 回调接口的实现类,传入的是lambda
        zkOperation.zk.addWatch("/xt", newWatcher, AddWatchMode.PERSISTENT,
                (rc, path, ctxCallBack) -> {
                    System.out.println("-----------------addWatch的异步回调------------------");
                    System.out.println(rc);
                    System.out.println("返回码为:"+rc);
                    System.out.println("创建节点: " + path);
                    System.out.println("传递给异步调用的上下文对象:"+(String)ctxCallBack);

                    if(rc == KeeperException.Code.OK.intValue()) {
                        System.out.println("添加watch成功");
                        isOk.set(true);
                    }
                    System.out.println("-----------------addWatch的异步回调------------------");
                    countDownLatch.countDown();
                },
                ctx);


        //如果给/xt 节点添加watch成功
        if(isOk.get()) {
            //全部为同步方式
            zkOperation.zk.setData("/xt", "new data".getBytes(), -1);
            zkOperation.zk.create("/xt/son1", "son1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zkOperation.zk.getData("/xt/son1", true, null);
            zkOperation.zk.create("/xt/son1/grandson1", "grandson1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            zkOperation.zk.setData("/xt/son1", "new son1".getBytes(), -1);
            zkOperation.zk.setData("/xt/son1", "new son2".getBytes(), -1);
            zkOperation.zk.setData("/xt/son1/grandson1", "new grandson1".getBytes(), -1);
            zkOperation.zk.delete("/xt/son1/grandson1", -1);
            zkOperation.zk.delete("/xt/son1", -1);
            zkOperation.zk.delete("/xt", -1);
        }


        zkOperation.zk.getData("/xt",true,null);
        //同步方式
        //getData 留下的是Data类型的Watcher  传入的必须是同一个watcher实例,不然会报错,里面会WatchDeregistration,所以要传入以前注册的watcher实例
        zkOperation.zk.removeWatches("/xt",zkOperation.connectWatcher, Watcher.WatcherType.Data,false);

        zkOperation.zk.getChildren("/xt",true);
        //getChildren 留下的是Children类型的Watcher
        zkOperation.zk.removeWatches("/xt", zkOperation.connectWatcher, Watcher.WatcherType.Children,false);

        zkOperation.zk.exists("/xt",true);
        //exists 留下的是Data类型的Watcher
        zkOperation.zk.removeWatches("/xt", zkOperation.connectWatcher, Watcher.WatcherType.Any,false);

        //这行命令,把addwatch添加的newWatcher删掉了,下一行命令就会报错
//        zkOperation.zk.removeWatches("/xt",newWatcher, Watcher.WatcherType.Any,false);

        //移除在/xt 上面使用addWatch 添加的watcher , 类型分别是PersistentWatch和PERSISTENT_RECURSIVE
        // false表示没有服务端连接时,不可以在本地移除Watcher,因为服务端和客户端都会注册watcher实例
        zkOperation.zk.removeAllWatches("/xt", Watcher.WatcherType.Any,false);


        //替换默认watcher
        zkOperation.zk.register(new ZkWatcher("替换的watcher"));
        zkOperation.zk.exists("/xtSyn",true);
        zkOperation.zk.setData("/xtSyn","我更换默认watcher啦".getBytes(),-1);
        zkOperation.zk.getData("/xtSyn",true,stat);

        
        
        //delete 节点数据
        //同步方式  void 方法,没有返回什么东西
        zkOperation.zk.delete("/xtSyn",-1);
        //异步方式
        ctx = "{'delete':'success'}";
        countDownLatch = new CountDownLatch(1);
        zkOperation.zk.delete("/xtAsyn", -1, new DeleteCallBack(countDownLatch), ctx);
        countDownLatch.await();
        System.out.println("成功删除节点");

        zkOperation.zk.delete("/xt",-1);

    }
}

代码运行结果(没注意啥美观,见笑,代码在上面,可以自己试着玩)
在这里插入图片描述

zookeeper设计模式

zookeeper从设计模式来看,采用了观察者的设计模式,zookeeper存储分布式所需要的各种配置数据等,然后接受观察者的注册,一旦某个节点数据发生变动,zookeeper就会通知每个已经注册某个节点的观察者(发布与订阅)

zookeeper应用场景:

  • 统一命名服务:通过指定的名称Znode可以获取服务地址(ip:port),
  • 集群管理:某个服务下面有很多服务端提供同样的服务(每个服务端是一个Znode),zookeeper因为可以实时监控节点状态变化而可以监听服务的情况,比如服务的动态上下线(通过Watcher实现)
  • 负载均衡:每个Znode下面可以统计信息,因此可以记录服务器的访问次数,让访问次数少的Znode(对应着一个服务端)去接受新的客户端请求
  • 配置管理:分布式的环境下,配置文件最好是统一放在一个地方,比如各种服务集群等,配置文件修改后,需要通知到每一个服务端并且能快速同步过去。而zookeeper可以利用watcher通知客户端他们的配置(存储在Znode上)发生了变化
  • 分布式锁:因为zookeeper是一个一致性的文件系统,并且创建删除Znode保证了原子性,所以独占锁的实现:将一个Znode看成一个独占锁,所有客户端都去创建一个指定Znode,创建成功的客户端等于抢到了这个锁,用完删除这个Znode等于释放独占锁。控制多个线程的执行顺序:在指定节点下面/Znode ,所有客户端在这个Znode下面创建临时顺序的节点,编号最小的获取锁,执行任务(获取这个Znode下面的所有节点,如果发现自己编号最小,获取锁,如果不是,就会向比自己小的节点注册事件监听器,方便通知自己),用完这个临时顺序节点删除,轮到次小的上
  • 分布式队列: 在指定的/Znode下面,创建持久的顺序节点,这样任务入队时会获得编号,出队时按照编号出队(比如最小的节点的任务先执行),所以就是在一个/Znode下面创建持久顺序节点,因为是持久节点,所以不用担心消息会丢失
  • Master选举:Zookeeper有一个非常重要的特性即强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行Master选举了。
  • 分布式协调通知:基于watcher实现订阅通知,可以做到分布式之间的协调通知工作

References:

  • https://kaven.blog.csdn.net/article/details/121445222
  • https://blog.csdn.net/u012291108/article/details/54381975?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&utm_relevant_index=2
  • https://blog.csdn.net/formelo/article/details/79373548?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242.1&utm_relevant_index=3
  • https://blog.csdn.net/LXYDSF/article/details/121085765?utm_source=app&app_version=4.21.0&code=app_1562916241&uLinkId=usr1mkqgl919blen
  • https://www.cnblogs.com/jing99/p/12722430.html
  • https://www.cnblogs.com/yewy/p/13111829.html
  • https://blog.csdn.net/sun_tantan/article/details/120430313
  • https://www.cnblogs.com/wuzhenzhao/p/9994450.html
  • https://www.cnblogs.com/yewy/p/13111829.html
  • https://blog.csdn.net/wwwdc1012/article/details/86603643

(写博客主要是对自己学习的归纳整理,资料大部分来源于书籍、网络资料和自己的实践,整理不易,但是难免有不足之处,如有错误,请大家评论区批评指正。同时感谢广大博主和广大作者辛苦整理出来的资源和分享的知识。)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值