从 paxos 到 zookeeper 总结(二)ZooKeeper入门

一、ZooKeeper是什么

ZooKeeper是一个开源的分布式协调服务,由雅虎公司创建,是Goole Chubby的开源实现。ZooKeepr的设计目标是将复杂易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,以一系列简单 易用的接口提供的用户使用。

zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、master选举、分布式锁和分布式队列等功能。ZooKeeper可以保证如下分布式一致性特性

  • 顺序一致性

同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中去

  • 原子性

所有事务的请求结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么在整个集群中所有机器上都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事物,而另外一部分没有应用的情况

  • 单一视图

无论客户端连接的是哪个Zookeeper服务器,其看到的服务端数据模型都是一致的。

  • 可靠性

一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。

  • 实时性

Zookeeper仅仅保证在一定的时间内,客户端最终一定能够从服务端上读到最终的数据状态

ZooKeeper的设计目标

Zookeeper致力于提供一个高性能、高可用,且具有严格的顺序访问控制能力的分布式协调服务。高性能使得Zookeeper能够应用于那些对系统吞吐有明确要求的大型分布式系统中。高可用使得分布式的单点问题得到很好的解决,而严格的顺序访问控制使得客户端能够基于Zookeeper实现一些复杂的同步原语。下面我们来具体看一下ZooKeeper的四个设计目标:

  • 简单的数据模型

Zookeeper使得分布式程序能够通过一个共享的,树型结构的名字空间进行相互协调。这里所说的树型结构的名字空间,是指Zookeeper服务器内存中的一个数据模型,尤其一系列被称为ZNode的数据节点组成,总的来说,其数据模型类似于一个文件系统,而ZNode之间的层级关系,就像文件系统的目录结构一样。不过和传统的磁盘文件系统不同的是,Zookeeper将全量数据存储在内存中,以此来实现提高服务器吞吐,减少延迟的目的。

  • 可以构建集群

一个ZooKeeper集群通常由一组机器组成,一般3~5台机器就可以组成一个可用的ZooKeeper集群了
图2-1
组成Zookeeper集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。值得一提的是,只要集群中存在超过一半的机器能够正常工作,那么整个集群就能够正常对外服务。
Zookeeper的客户端程序会选择和集群中任意一台机器共同来创建一个TCP连接,而一旦客户端和某台Zookeeper服务器之间的链接断开后,客户端会自动链接到集群中的其他机器。

ZooKeeper没有沿用传统的Master/Slave模式(主备模式),而是引入了Leader、Follower和Observer三种角色:
Leader:集群通过一个Leader选举过程从所有的机器中选举一台机器作为”Leader”,Leader能为客户端提供读和写服务Leader服务器是整个集群工作机制的核心,主要工作:

  1. 事务请求的唯一调度者和处理者,保证集群事务处理的顺序性
  2. 集群内部各服务器的调度者

Follower :追随者,主要工作:

  1. 参与Leader选举投票
  2. 处理客户端非事务请求 - 即读服务
  3. 转发事务请求给Leader服务器
  4. 参与事务请求Proposal的投票

Observer :ZooKeeper自3.3.0版本开始引入的一个全新的服务器角色,充当一个观察者角色,工作原理和Follower基本是一致的,和Follower唯一的区别是Observer**不参与任何形式的投票,**所以Observer可以在不影响写性能的情况下提升集群的读性能。

  • 顺序访问

对于来自客户端的每个更新请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序都可以使用ZooKeeper的这个特性来实现更高层次的同步原语。

  • 高性能

Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景

二、系统模型

ZNode

ZooKeeper的视图结构和标准的Unix文件系统非常类似,但没有引入传统文件系统中目录和文件等相关概念,而是使用了其特有的“数据节点”概念,我们称之为ZNode.ZNode是ZooKeeper中数据的最小单元,每个ZNode上都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,我们称之为树。

ZooKeeper中,每一个数据节点都被称为-一个ZNode,所有ZNode按层次化结构进行组织,形成一棵树。ZNode 的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/) 进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。如图。
在这里插入图片描述

节点类型

在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型。在ZooKeeper中,节点类型可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)、顺序节点(SEQUENTIAL)三大类,具体在节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:

  • 持久节点(PERSISTENT)

持久节点是ZooKeeper中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。

  • 持久顺序节点(PERSISTENT_ SEQUENTIAL)

持久顺序节点的基本特性和持久节点是-致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper 会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。

  • 临时节点(EPHEMERAL)

和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外,ZooKeeper规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。

  • 临时顺序节点(EPHEMERAL_ SEQUENTIAL)

临时顺序节点的基本特性和临时节点也是一致的,同样是在临时节点的基础上,添加了顺序的特性。

事务ID(ZXID)

在《事务处理,感念与技术》一书中提到,事务是对物理和抽象的应用状态上的操作集合。在现在的计算机科学中,狭义上的事务通常指的是数据库事务,一 般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation) 和持久性(Durability)。

在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务 ID,用ZXID来表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。

状态信息

可以针对ZooKeeper上的数据节点进行数据的写人和子节点的创建。事实上,每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。下图所示使用客户端脚本get命令来获取一个数据节点的内容。
在这里插入图片描述
从图中返回结果中,我们可以看到,第一行是当前节点的数据内容,从第二行开始就是节点的状态信息了,这其实就是数据节点的Sata对象的格式化输出。

Stat类中包含了ZooKeeper上一个数据节点的所有状态信息,包括事务ID,版本信息,和只节点数等,下表对所有这些属性进行了说明。

在这里插入图片描述

版本—保证分布式数据原子性操作

ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。下面对三类版本信息进行说明

  • version 当前数据节点数据内容的版本号
  • cversion 当前数据节点总结点的版本号
  • aversion 当前数据节点ACL变更版本号

ZooKeeper中的版本概念和传统意义上的软件版本有很大的区别,它表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数,我们以其中的version这种版本类型为例来说明。在一个数据节点/zk-book被创建完毕之后,节点的version值是0,表示的含义是“当前节点自从创建之后,被更新过0次”。如果现在对该节点的数据内容进行更新操作,那么随后,version的值就会变成1。同时需要注意的是,在上文中提到的关于version的说明,其表示的是对数据节点数据内容的变更次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值依然会变更。

从上面的讲解中,我们其实可以把一个乐观锁控制的事务分成三个阶段:读取数据,写入效验,数据写入。其中写入效验阶段是整个悲观所控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其它事务对数据进行过更新,以确保数据更新的一致性。我们首先看下CAS(Compare and swap)理论的相关知识,通俗地讲,CAS的意思就是:“对一个值V,每次更新前都会对比其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B。”其中是否符合预期是乐观锁中的“写入校验”阶段。
好了,现在我们再回头来看ZooKeeper中版本的作用,实际上,在ZooKeeper中,version属性正是用来实现乐观锁机制中的“写入校验”的。这里我们来看下Zookeeper中的内部实现。在Zookeeper服务器的PrepRequestProcessor处理器类中,在处理没一个数据更新(setDataRequest)请求时,会进行如下的版本检查。

version = setDataRequest.getVersion();
currentVersion = nodeRecord.stat.getVersion();
if(version != -1 && version != currentVersion) {
	throw new BadVersionException(path);
}
version = currentVersion + 1;

从上面的执行逻辑中,我们可以看出,在进行一次setDataRequest请求处理时,首先进行了版本检查,ZooKeeper会从setDataRequest请求中获取当前请求的版本version,同时从数据记录nodeRecord中获取当前服务器上该数据的最新版本currentVersion。如果version为-1,那么说明客户端并不要求使用乐观锁,可以忽略版本对比。那么对比version和currentVersion,如果两个版本不匹配就会抛出BadVersionException异常。

Watcher

Watcher(时间监听器),是ZooKeeper中的一个很重要的特性,ZooKeeper允许用户在制定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式通知协调服务的重要特性。我们会在从 paxos 到 zookeeper 总结(三)Watcher——数据变更的通知中详细讲解。

ACL

ZooKeeper 采用 ACL(Access Control Lists)策略来进行权限控制。ZooKeeper 定义了如下5种权限。

CREATE: 创建子节点的权限。

READ: 获取节点数据和子节点列表的权限。

WRITE:更新节点数据的权限。

DELETE: 删除子节点的权限。

ADMIN: 设置节点ACL的权限。

CREATE 和 DELETE 都是针对子节点的权限控制。

五种权限简称

CREATE -> 增 -> c
READ -> 查 -> r
WRITE -> 改 -> w
DELETE -> 删 -> d
ADMIN -> 管理 -> a
这5种权限简写为crwda

鉴权模式

world:默认方式,相当于全世界都能访问
auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
ip:使用Ip地址认证

演示auth方式

 1)增加一个认证用户
addauth digest 用户名:密码明文
addauth digest linann:linann

 2)设置权限
setAcl /path auth:用户名:密码明文:权限
setAcl /zk_test auth:linann:linann:rw

 3)查看ACL设置
getAcl /zk_test

三、使用ZooKeeper

安装运行

官方地址下载并解压zookeeper

cd /usr/local
wget https://mirrors.bfsu.edu.cn/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0.tar.gz
tar -zxvf apache-zookeeper-3.7.0.tar.gz
cd apache-zookeeper-3.7.0

重命名配置文件zoo_sample.cfg

cp conf/zoo_sample.cfg conf/zoo.cfg

进入zookeeper的bin目录,启动zookeeper

cd bin
./zkServer.sh start

查看状态

 ./zkServer.sh status

出现以下信息代表启动成功

ZooKeeper JMX enabled by default
Using config: /usr/local/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: standalone

启动客户端脚本

sh zkCli.sh

连接其他客户端

sh zkCli.sh -server ip:port

zoo.cfp配置说明

tickTime= 2000
dataDir= /user/local/ZooKeeper
clientPort=2181
initLimit = 10
syncLimit= 5
server.1=IP1:2888:3888
server.2=IP2:2888:3888
server.3=IP3:2888:3888
  • tickTime:基本事件单元,以毫秒为单位。这个时间是作为 Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每隔 tickTime时间就会发送一个心跳。
  • dataDir:就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper将写数据的日志文件也保存在这个目录里。
  • clientPort:这个端口就是客户端连接Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
  • initLimit:这个配置项是用来配置 Zookeeper 接受客户端初始化连接时最长能忍受多少个心跳时间间隔数,当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒。
  • syncLimit:这个配置项标识 LeaderFollower 之间发送消息,请求和应答时间长度,最长不能超过多少个tickTime 的时间长度,总的时间长度就是 5*2000=10 秒
  • server.A = B:C:D A表示这个是第几号服务器,B 是这个服务器的 IP 地址,C 表示的是集群节点通讯端口,D 表示节点选举端口。
    集群模式下,集群中的每台机器都是需要感知到整个集群是由哪几台机器组成的,所以集群中的服务器都要做如上配置,单机模式与集群不同的是只配自己即可,也可以在一台机器上启动多个服务,每个服务配置不同的端口做伪集群

客户端脚本

上述命令启动客户端脚本以后,我们便可以通过客户端对ZooKeeper进行操作:

  • 创建

使用create命令,创建一个Zookeeper节点,用法如下:

create [-s] [-e] path data acl

其中,-s或-e分别指定节点特性:顺序或临时节点。默认情况下即不添加-s或-e参数的,创建的是持久节点。

[zk:localhost:2181(CONNECTED)0] create /zk-book 123
Created /zk-book

执行完上面的命令,就在ZooKeeper的根节点下创建了一个叫做/zk-book的节点,并且节点的数据内容是“123”。另外,create命令的最后一个参数是acl,它是用来进行全线控制的,缺省情况下,不做任何权限控制。关于ZooKeeper权限控制,后面我们会介绍。

  • 读取

与读取相关的命令包括ls命令和set命令

ls

使用ls命令,可以列出ZooKeeper指定节点下的所有子节点。当然,这个命令只能考到指定节点下第一级的所有子节点。用法如下:

ls path [watch]

其中 path表示的是指定数据节点的节点路径。

执行如下命令:

[zk:localhost:2181(CONNECTED)0] ls /
[zookeeper]

第一次部署的ZooKeeper集群,默认在根节点"/"下面有一个叫做/zookeeper的保留节点。

get

使用get命令,可以获取ZooKeeper指定节点的数据内容和属性信息。用法如下:

get path [watch]

执行如下命令:

[zk:localhost:2181(CONNECTED)0] get /zk-book
123
cZxid=0xa
ctime=Thu Jun 20 10:35:00 UTC 2013
mZxid=0xa
mtime=Thu Jun 20 10:35:00 UTC 2013
pZxid=0xa
cversion=0
dataVersion=0
aclVersion=0
ephemeralOwner=0x0
dataLength=3
numChildren=0

从上面的输出信息中,我们可以看到,第一行是节点/zk-book的数据内容,其他几行则是创建该节点的事务ID(cZxid),最后一次更新该节点的事务ID(mZxid)和最后一次更新该节点的时间(mtime)等属性信息。关于ZooKeeper节点的数据结构,在后面讲解。

更新

使用set命令可以更新指定节点的数据内容。用法如下:

set path data [version]

其中,data就是要更新的内容。注意,set命令后面还有一个version参数。在ZooKeeeper中,节点的数据是由版本概念的,这个用于指定本次更新操作时基于ZNode的哪一个版本进行的。

执行如下命令:

[zk:localhost:2181(CONNECTTED)3] set /zk-book 456
cZxid = 0xa
ctime = Thu jun 20 10:35:00 UTC 2013
mZxid = 0x11
mtime = Fri Jun 21 01:05:49 UTC 2013
pZxid = 0xa
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

执行完以上命令后,节点/zk-book的数据内容就已经被更新成“456”了。细心的读者还会发现,在输出信息中,dataVersion的值由原来的0变成了1,这是因为刚才的更新操作导致该节点的数据版本也发生了变更。关于ZNode的数据版本,将在后面做详细解释,这里只是演示简单操作,不再详细展开。

删除

使用delete命令,可以删除ZooKeeper上的指定节点。用法如下:

delete path [version]

此命令中的version参数和set命令中的version参数的作用是一致的。
执行如下命令:

[zk:localhost:2181(CONNECTED)2] delete /zk-book

执行完以上命令后,就可以把/zi-book这个节点成功删除了。但是这里要注意的一点是:要想删除某一个指定节点,该节点必须没有子节点存在。这可以通过执行如下命令来进行验证:

[zk:localhost:2181(CONNECTED) 7] create /zk-book 123
Create /zk-book
[zk:localhost:2181(CONNECTED) 8] create /zk-book/child 12345
Created /zk-book/child
[zk:localhost:2181(CONNECTED) 9] delete /zk-book
Node not empty:..zk-book

上面的这个输出结果已经很清晰地表明了删除失败,通过Node not empty这个出错信息,可以看出无法删除一个包含子节点的节点

Java客户端API使用

ZooKeeper作为一个分布式服务框架,主要用来解决分布式数据一致性问题,它提供了简单的分布式原语,并且对多种编程语言提供了API。下面我们重点来看下ZooKeeper的Java客户端API使用方式。

创建会话

客户端可以通过创建一个Zookeeper(org.apache.zookeeper.ZooKeeper)实例来连接ZooKeeper服务器。ZooKeeper的4种构造方法如下。

ZooKeeper的构造方法:

ZooKeeper(String connectString, int sesssionTimeout, Watcher watcher);

ZooKeeper(String connectString,int sessionTimeout,Watcher watcher,boolean canBeReadOnly);

ZooKeeper(String connectString,int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd);

ZooKeeper(String connectString,int sessionTimeout,Watcher watcher,long sessionId,byte[] sessionPasswd,boolean canBeReadOnly);

使用任意一个构造方法都可以顺利完成与ZooKeeper服务器的会话(Session)创建,下面列出了对每个参数的说明。

  • connectString :指ZooKeeper服务器列表,由英文状态逗号host:port字符串组成,每一个都代表一台ZooKeeper机器,例如,192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181,这样就为客户端指定了三台服务器地址。另外,也可以在connectString中设置客户端连接上ZooKeeper后的根目录,方法是在host:port字符串之后添加上这个根目录,例如:192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181/zk-book,这样就指定了该客户端连接上ZooKeeper服务器之后,所有对ZooKeeper的操作,都会基于这个根目录。例如,客户端对/foo/bar的操作,都会指向节点/zk-book/foo/bar——这个目录也叫Chroot,即客户端隔离命名空间,关于ZooKeeper中Chrooot的用法和左右,将在后面详细解释
  • sessionTimeout 指会话的超时时间,是一个以“毫秒”为单位的整型值。
  • watcher ZooKeeper允许客户端在构造方法中传入一个接口Watcher(org.apache.zookeeper.Watcher)的实现类对象来作为默认的Watcher事件通知处理器。当然,该参数可以设置为null以表明不需要设置默认的Watcher处理器。

注意,ZooKeeper客户端和服务端会话的建立是一个异步的过程,也就是说在程序中,构造方法会在处理客户端初始化工作后立即返回,在大多说情况下,此时并没有真正建立好一个可用的会话,在会话的生命周期中处于“CONNECTING”的状态。
当该会话真正创建完毕后,ZooKeeper服务端会向会话对应客户端发送一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话。
该构造方法内部实现了与ZooKeeper服务器之间的TCP连接创建,负责维护客户端会话的生命周期,现在暂不对这些细节做更多详解,关于ZooKeeper客户端与服务端之间的连接创建过程及其内部原理,将在后面详细介绍。

创建节点

客户端可以通过ZooKeeper的API来创建一个数据节点,有如下两个接口:

String create(final String path,byte data[],List<ACL> acl,CreateMode createMode)

void create(final String path,byte date[],List<ACL> acl,CreateMode createMode,StringCallback cb,Object ctx)

这两个接口分别以同步和异步方式创建节点,API方法的参数说明如表5-3所示。

  • path 需要创建的数据节点的节点路径,录入,/zk-book/foo

  • data[] 一个字节数组,是节点创建后的初始内容

  • acl 节点的ACL策略

  • createMode 节点类型,是一个枚举类型,通常有4种可选的节点类型:

    • 持久(PERSISTENT)
    • 持久顺序(PERSISTENT_SEQUENTIAL)
    • 临时(EPHEMERAL)
    • 临时顺序(EPHEMERAL_SEQUENTIAL)
  • cb 注册一个异步回调函数。开发人员需要实现StringCallBack接口,主要是对下面这个方法的重写: void processResult(int rc,String path,Object ctx,String name);
    当服务端节点创建完毕后,ZooKeeper客户端就会自动调用这个方法,这样就可以处理相关的业务逻辑了

  • ctx 用于传递一个对象,可以在毁掉方法执行的时候使用,通常是一个上下文(Context)信息

需要注意几点,无论是同步还是异步接口,ZooKeeper都不支持递归创建,即无法在父节点不存在的情况下创建一个子节点。另外,如果一个节点已经存在了,那么创建同名节点的时候,会抛出NodeExistException异常,这个特性可以用来做分布式锁,后面我们会讲到。
目前ZooKeeper的节点内容只支持字节数组(byte[])类型,也就是说,ZooKeeper不负责为节点内容进行序列化,开发人员需要自己使用序列化工具将节点内容进行序列化和反序列化。对于字符串,可以简单地使用“String”.getBytes()来生成一个字节数组;对于其他复杂对象,可以使用Hessian或是Kryo等专门的序列化工具来进行序列化。

使用同步API创建一个节点

public class ZooKeeper_Constructor_Usage_With_SID_PASSWD implements Watcher {
	private static CountDownLatch connectedSemphore = new CountDownLatch(1);
	
	public static void main(String[] args) {
		ZooKeeper zookeeper = new ZooKeeper("domain1.book.zookeeper:2181",5000,//
				new ZooKeeper_Create_API_Sync_Usage());
		connectedSemphore.await();
		String path1 = zookeeper.create("/zk-test-ephemeral-","".getBytes(),Ids.OPEN_ACL_UNSAFE,
				CreateMode.EPHEMERAL);
		System.out.println("Success create znode:"+path1);
		
		String path2 = zookeeper.create("/zk-test-ephemeral-","".getBytes(),
				Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
		System.out.println("Success create znode:"+path2);
	}
	public void process(WatchedEvent event){
		if(KeeperState.SyncConnected == event.getState()){
			connectedSemphore.countDown();
		}
	}
}

运行程序,输出结果如下:

Receive watched event:WatchedEvent state:SyncConnected type:None path:null
Success create znode:/zk-test-ephemeral-
Success create znode:/zk-test-ephemeral-0001975508

在上面这个程序片段中,使用了同步的节点创建接口:String create(final String path,byte data[],List acl,CreateMode createMode)。在接口使用中,我们分别创建了两种类型的节点:临时节点和临时顺序节点。从返回的结果可以看出,如果创建了临时节点,那么API的返回值就是当时传入的path参数:如果创建了临时顺序节点,那么ZooKeeper会自动在节点后加上一个数字,并且在API接口的返回值中返回该数据节点的一个完整的节点路径。

使用异步API创建一个节点

//ZooKeeper API创建节点,使用异步(async)接口
public class ZooKeeper_Create_API-ASync_Usage implements Watcher{
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
	public static void main(String[] args) {
		ZooKeeper zookeeper = new ZooKeeper("domain1.book.zookeepr:2181",5000,//
				new ZooKeeper_Create_API_ASync_Usage());
		connectedSemaphore.await();
		
		zookeeper.create("/zk-test-ephemeral-","".getBytes(),Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL,
				new IStringCallBack(),"I am contest.");
		zookeeper.create("/zk-test-ephemeral-","".getBytes(),Ids.OPEN_ACL_UNSAFE,
				CreateMode.EPHEMERAL,new IStringCallback(),"I am context.");
		zookeeper.create("/zk-test-ephemeral-","".getBytes(),Ids.OPEN_ACL_UNSAFE,
				CreateMode.EPHEMERAL_SEQUENTIAL,new ISstringCallback(),"I am context.");
		
		Thread.sleep(Integer.MAX_VALUE);
	}
	
	public void process(WatchedEvent event){
		if(KeeperState.SyncConnected == event.getState()){
			connectedSemaphore.countDown();
		}
	}
}
 
class IStringCallback implements AsyncCallback.StringCallback{
	public void proceResult(int rc,String path,Object ctx,String name){
		System.out.println("Create path result:【"+rc+","+path+","+","
	+ ctx+", real path name:"+ name);
	}
}

运行程序,输出结果如下:

Create path result:[0,/zk-test-ephemeral-,I am context, real path name:/zk-test-ephemeral-
Create path result:[-110,/zk-test-ephemeral-,I am context.,real path name: null
Create path result:[0,/zk-test-ephemeral-,I am context., real path name:/zk-test-ephemeral-0001975736

从这个程序片段中可以看出,使用异步方式创建接口也很简单。用户仅仅需要实现**AsyncCallback.StringCallback()**接口即可。AsyncCallback包含了StatCallback、DataCalback、ACLCallback、ChildrenCallback、Children2Callback、StringCallback和VoidCallback七种不同的回调接口,用户可以在不同的异步接口中实现不同的接口。

和同步接口最大的区别在于,节点的创建过程(包括网络通信和服务端的节点创建过程)是异步的。并且,在同步接口调用过程中,我们需要关注接口抛出异常的可能;但是在异步接口中,接口本身是不会抛出异常的、所有的异常都会在回调函数中通过Result Code(响应吗)来实现。

下面来重点看下回调方法:void processResult(int rc,String path,Object ctx,String name)。这个方法的几个参数

  • rc Result Code,服务端响应码。客户端可以从这个响应码中识别出API调用的结果,常见的响应码如下:
    • 0(OK):接口调用成功
    • -4(ConnectionLoss):客户端和服务端连接已断开。
    • -110(NodeExists):指定节点已存在。
    • -112(SessionExpired):会话已过期
  • path 接口调用时传入API的数据节点路径参数值
  • ctx 接口调用时传入API的ctx参数值
  • name 实际在服务端创建的节点名。在上述代码中,第三次创建节点时,由于创建的节点类型是顺序节点,因此在服务端没有真正创建好顺序节点之前,客户端无法知道节点的完整节点路径。于是,在回调方法中,服务端会返回这个数据节点的完整节点路径。

读取数据 getData

客户端可以通过ZooKeeper的API来获取一个节点的数据内容,有如下4个接口:

byte[] getData(final String path, Watcher watcher, Stat stat)
byte[] getData(String path, boolean watch, Stat stat)
void getData(final String path, Watcher watcher,DataCallback cb, Object ctx)
void getData(String path, boolean watch, DataCallback cb, Object ctx)

这里列出的4个API包含了同步和异步的接口,API方法的参数说明如下表所示。

  • path 指定数据节点的节点路径,即API调用的目的是获取该节点的数据内容
  • watcher 注册的Watcher。一旦之后节点内容有变更,就会向客户端发送通知。该参数允许传入null
  • stat 指定数据节点的节点状态信息。用法是在接口中传入一个旧的stat变量,该stat变量会在方法执行过程中,被来自服务端响应的新stat对象替换。
  • watch 表明是否需要注册一个Watcher。这里使用到默认Watcher。如果这个参数是true,那么ZooKeeper客户端会自动使用上文中提到的那个默认Watcher:如果是false,表明不需要注册Watcher
  • cb 注册一个异步回调函数
  • ctx 用于传递上下文信息的对象

getData接口和上文中的getChildren接口的用法基本相同,这里主要看一看注册的Watcher有什么不同之处。客户端在获取一个节点的数据内容的时候,是可以进行Watcher注册的,这样一来,一旦该节点的状态发生变更,那么ZooKeeper服务端就会向客户端发送一个NodeDataChanged(EventType.NodeDataChanged) 的事件通知。
另外,API返回结果的类型是byte[],目前ZooKeeper只支持这种类型的数据存储,所以在获取数据的时候也是返回此类型。

使用同步API获取节点数据内容

// ZooKeeper API 获取节点数据内容,使用同步(sync)接口
public class ZooKeeper_GetData_API_Sync_Usage implements Watcher {
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);

	pirvate static ZooKeeper zk = null;
	
	public static void main(String[] args) throws Exception {
		String path = "/zk-book";
	
		zk = new ZooKeeper("domain1.book.zookeeper:2181", 5000, new ZooKeeper_GetData_API_Sync_Usage());
		connectedSemaphore.await();
	
		zk.create(path, "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
	
		System.out.println(new String(zk.getData(path, true, stat)));
		System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
		zk.setData(path, "123".Bytes(), -1);
		Thread.sleep(Integer.MAX_VALUE);
	}
	
	public void process(WatchedEvent event) {
		if (KeeperState.SyncConnected == event.getState()) {
			if (EventType.None == event.getType() && null == event.getPath) {
				connectedSemaphore.countDown();
			} else if (event.getType() == EventType.NodeChildrenChanged) {
				try {
					System.out.println(new String(zk.getData(event.getPath(), true, stat)));	
					System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
				} catch (Exception e) {}
			}
		}
	}
}

运行程序,输出结果如下:

123
253404961568,253404961568,0
123
253404961568,253404961576,1

在上面这个程序中,我们首先创建了一个节点/zk-book,并初始化其数据内容为“123”.然后调用getData的同步接口来获取/zk-book节点的数据内容,调用的同时注册了一个Watcher。之后,我们同样以“123”去更新将该节点的数据内容,此时,由于我们之前在该节点上注册了一个Watcher,因此,一旦该节点的数据发生变化,ZooKeeper服务端就会向客户端发出一个“数据变更”的事件通知,于是,客户端可以收到这个事件通知后,再次调用getData接口来获取新的数据内容。
另外,在调用getData接口的同时,我们传入了一个stat变量,在ZooKeeper客户端的内部实现中,会从服务端的响应中获取到数据节点的最新节点状态信息,来替换这个客户端的旧状态。
我们重点再来看下运行上面这个程序的输出结果中,前后两次调用getData接口的返回值。第一次的输出结果如下:

123
253404961568,253404961568,0

第二次的输出结果如下:

123
253404961568,253404961576,1

第一次是客户端主动调用getData接口来获取数据;第二次则是节点数据变更后,服务端发送Watcher事件通知给客户端后,客户端再次调用getData接口来获取数据。两次调用的输出结果中,节点数据内容的值并没有变化。既然节点的数据内容并没有变化,那么ZooKeeper服务端为什么会向客户端发送Watcher事件通知呢。这里,我们必须明确一个概念:节点的数据内容或是节点的数据版本变化,都被看作是ZooKeeper节点的变化。明白这个概念后,再回过头看上面的结果输出,可以看出,该节点在Zxid为“253404961568”时被创建,在Zxid为“253404961576”时被更新,于是节点的数据版本从“0”变化到“1”.所以,这里我们要明确的一点是,数据内容或是数据版本变化,都会触发服务端的NodeDataChanged通知。

如下接口与上面类似不再赘述,需要注意的是getDate,getChildren,exists三个方法支持Watcher的传入。

删除节点 delete

public void delete(final String path,int version)
pulbic void delete(final String path,int version,VoidCallback cb,Object ctx)

获取子节点 getChildren

List<String> getChildren(final String path, Watcher watcher)
List<String> getChildren(String path, boolean watch)
void getChildren(final String path, Watcher watcher,ChildrenCallback cb, Object ctx)
void getChildren(String path, boolean watch, ChildrenCallback cb,Object ctx)
List<String> getChildren(final String path, Watcher watcher,Stat stat)
List<String> getChildren(String path, boolean watch, Stat stat)
void getChildren(final String path, Watcher watcher,Children2Callback cb, Object ctx)
void getChildren(String path, boolean watch, Children2Callback cb,Object ctx)

更新数据 setData

Stat setData(final String path, byte data[], int version)
void setData(final String path, byte data[], int version,StatCallback cb, Object ctx)

检测节点是否存在 exists

public Stat exists(final String path, Watcher watcher)
public Stat exists(String path, boolean watch)
public void exists(final String path, Watcher watcher,StatCallback cb,Object ctx)
public void exists(String path, boolean watch, StatCallback cb, Object ctx)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值