ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper包含一个简单的原语集,提供Java和C的接口
ZooKeeper是以Fast Paxos算法为基础的,Paxos 算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos做了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解。
ZooKeeper的基本运转流程:
1、选举Leader。
2、同步数据。
3、选举Leader过程中算法有很多,但要达到的选举标准是一致的。
4、Leader要具有最高的执行ID,类似root权限。
5、集群中大多数的机器得到响应并接受选出的Leader。
在Zookeeper中,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据。如果在创建znode时Flag设置为EPHEMERAL,那么当创建这个znode的节点和Zookeeper失去连接后,这个znode将不再存在在Zookeeper里,Zookeeper使用Watcher察觉事件信息。当客户端接收到事件信息,比如连接超时、节点数据改变、子节点改变,可以调用相应的行为来处理数据。Zookeeper的Wiki页面展示了如何使用Zookeeper来处理事件通知,队列,优先队列,锁,共享锁,可撤销的共享锁,两阶段提交。
那么Zookeeper能做什么事情呢,简单的例子:假设我们有20个搜索引擎的服务器(每个负责总索引中的一部分的搜索任务)和一个总服务器(负责向这20个搜索引擎的服务器发出搜索请求并合并结果集),一个备用的总服务器(负责当总服务器宕机时替换总服务器),一个web的cgi(向总服务器发出搜索请求)。搜索引擎的服务器中的15个服务器提供搜索服务,5个服务器正在生成索引。这20个搜索引擎的服务器经常要让正在提供搜索服务的服务器停止提供服务开始生成索引,或生成索引的服务器已经把索引生成完成可以提供搜索服务了。使用Zookeeper可以保证总服务器自动感知有多少提供搜索引擎的服务器并向这些服务器发出搜索请求,当总服务器宕机时自动启用备用的总服务器。
Zookeeper的官网:https://zookeeper.apache.org/
Zookeeper 3.6.2版本:https://zookeeper.apache.org/doc/r3.6.2/index.html
Zookeeper 所有版本的下载地址:https://zookeeper.apache.org/releases.html
Zookeeper 3.6.2版本的下载地址:https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz
下载过后进行解压
tar -zxvf apache-zookeeper-3.6.2-bin.tar.gz
最后,我的zookeeper的安装路径为:/usr/zookeeper/zookeeper3.6.2
在安装路径下的bin目录下,就是zookeeper提供的执行命令:
常用的是:
zkCli.sh:linux客户端
zkCli.cmd:windows客户端
zkServer.sh:linux服务端
zkServer.cmd:windows服务端
建议把bin目录添加到操作系统的环境变量中去,编辑 ~/.bash_profile
vi ~/.bash_profile
这样可以比较方便的来使用zookeeper
启动服务端
在启动服务端之前,得准备一份zookeeper要用的配置文件,在zookeeper的安装路径下有conf目录:
我这里已经将zoo_sample.cfg复制了一份改名为zoo.cfg,并且修改了base.path目录,可以正常使用了
有了配置文件后可以启动zookeeper(以下简称zk)服务端了
注意:以下都是基于linux的,如果你想在windows上使用,执行对应的**.cmd脚本
这样zk server就启动好了
控制台显示STARTED,但是不一定真的成功了,具体还要看对应的启动日志,默认日志目录和日志文件为:
cat zookeeper-root-server-localhost.localdomain.out 就能查看日志
ookeeper也是用Java实现的,所以如果没有在日志中看到异常信息,那基本上就启动成功了。
请记住这个日志文件,这是排查Zookeeper问题的关键。
当然,还有一种方式就是可以利用jps命令查看运行中的java进程
QuorumPeerMain其实是Zookeeper服务端的启动类,16899表示进程号,只要有这个信息,基本Zookeeper服务端就是正常运行中的。
启动客户端
直接运行zkCli.sh脚本就能启动客户端或者远程连接就使用zkCli.sh -server ip:port
一旦连接成功,此处就会显示CONNECTED,如果是连接中,就会显示CONNECTING(表示服务端没启动成功,客户端连接不上,一直在重试)。
Zookeeper可以存取数据,Mysql存的数据就做“行”,Redis中存的数据叫“kv对”,Zookeeper中存的数据叫“znode节点”。一个znode有节点名字、节点内容、子节点。我们可以自由的增加、删除znode,在一个znode下增加、删除子znode。
有四种类型的znode:
PERSISTENT(持久化节点)
客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在
PERSISTENT_SEQUENTIAL(持久化顺序节点)
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
EPHEMERAL(临时节点)
客户端与zookeeper断开连接后,该节点被删除。
临时节点下不能拥有子节点
其他客户端也能看到临时节点
EPHEMERAL_SEQUENTIAL(临时顺序节点)
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
Container(容器节点)
3.5.3 版本新增,如果Container节点下面没有子节点,则Container节点在未来会被Zookeeper自动清除,定时任务默认60s 检查一次
PERSISTENT_WITH_TTL(持久化TTL节点)
3.5.3 版本新增,默认禁用,只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启,拥有一个过期时间
PERSISTENT_SEQUENTIAL_WITH_TTL(持久化TTL顺序节点)
3.5.3 版本新增,默认禁用,只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启,在顺序节点的基础上增加了过期时间概念
创建节点(znode)
语法:
create /path data
示例:
create /FirstNode first
输出:
[zk: localhost:2181(CONNECTED) 3] create /FirstNode first
Created /FirstNode
要创建顺序节点,请添加flag:-s,如下所示。
语法:
create -s /path data
示例:
create -s /FirstNode second
输出:
[zk: localhost:2181(CONNECTED) 4] create -s /FirstNode second
Created /FirstNode0000000018
要创建临时节点,请添加flag:-e ,如下所示。
语法:
create -e /path data
示例:
create -e /FirstNode-ephemeral ephemeral
输出:
[zk: localhost:2181(CONNECTED) 6] create -e /FirstNode-ephemeral ephemeral
Created /FirstNode-ephemeral
记住当客户端断开连接时,临时节点将被删除。你可以通过退出ZooKeeper CLI,然后重新打开CLI来尝试。
创建容器节点
create -c /path data
只有添加过子节点,容器节点的特性才会生效,容器节点的特性是:节点的最后一个子节点被删除过,该节点会自动删除(可能延迟一段时间)
创建TTL节点
create -t 3000 /path data
TTL节点创建后,如果3秒内没有数据修改,并且没有子节点,则会自动删除
前提是,服务端支持了TTL节点,默认没有开启,通过-Dzookeeper.extendedTypesEnabled=true可以开启
[zk: localhost:2181(CONNECTED) 7] get /FirstNode
first
cZxid = 0xa2
ctime = Wed Dec 12 13:29:14 CST 2018
mZxid = 0xa2
mtime = Wed Dec 12 13:29:14 CST 2018
pZxid = 0xa2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
获取数据
语法:
get /path
示例:
get /FirstNode
输出:
要访问顺序节点,必须输入znode的完整路径。
示例:
get /FirstNode0000000018
输出:
[zk: localhost:2181(CONNECTED) 9] get /FirstNode0000000018
second
cZxid = 0xa3
ctime = Wed Dec 12 13:30:44 CST 2018
mZxid = 0xa3
mtime = Wed Dec 12 13:30:44 CST 2018
pZxid = 0xa3
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0
设置数据
语法:
set /path /data
示例:
set /FirstNode first_update
输出:
[zk: localhost:2181(CONNECTED) 12] set /FirstNode first_update
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/FirstNode
cZxid = 0xa2
ctime = Wed Dec 12 13:29:14 CST 2018
mZxid = 0xa6
mtime = Wed Dec 12 13:51:39 CST 2018
pZxid = 0xa2
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 0
创建子节点
语法:
create /parent/path/subnode/path data
示例:
create /FirstNode/Child firstchildren
输出:
[zk: localhost:2181(CONNECTED) 13] create /FirstNode/Child firstchildren
Created /FirstNode/Child
[zk: localhost:2181(CONNECTED) 14] create /FirstNode/Child2 secondchildren
Created /FirstNode/Child2
列出子节点
语法:
ls /path
实例:
ls /FirstNode
输出:
[zk: localhost:2181(CONNECTED) 15] ls /FirstNode
[Child2, Child]
检查状态
语法:
stat /path
示例:
stat /FirstNode
输出:
[zk: localhost:2181(CONNECTED) 16] stat /FirstNode
cZxid = 0xa2
ctime = Wed Dec 12 13:29:14 CST 2018
mZxid = 0xa6
mtime = Wed Dec 12 13:51:39 CST 2018
pZxid = 0xa8
cversion = 2
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 2
移除Znode
语法:
delete /path
示例:
delete /FirstNode
输出:
[zk: localhost:2181(CONNECTED) 17] rmr /FirstNode
[zk: localhost:2181(CONNECTED) 18] get /FirstNode
Node does not exist: /FirstNode
删除(delete /path)命令类似于 remove 命令,但是只适用于没有子节点的znode。
zk做为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK通过ACL机制来解决访问权限问题。
ZooKeeper的权限控制是基于每个znode节点的,需要对每个节点设置权限
每个znode支持设置多种权限控制方案和多个权限
子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点
ACL 权限控制,使用:schemapermission 来标识,主要涵盖 3 个方面:
权限模式(Schema):鉴权的策略
授权对象(ID)
权限(Permission)
schema
world:只有一个用户:anyone,代表所有人(默认)
ip:使用IP地址认证
auth:使用已添加认证的用户认证
digest:使用“用户名:密码”方式认证
id
授权对象ID是指,权限赋予的用户或者一个实体,例如:IP 地址或者机器。授权模式 schema 与 授权对象 ID 之间关系:
world:只有一个id,即anyone
ip: 通常是一个ip地址或地址段,比如192.168.0.110或192.168.0.1/24
auth:用户名
digest:自定义:通常是"username:BASE64(SHA-1(username:password))"
权限
CREATE, 简写为c,可以创建子节点
DELETE,简写为d,可以删除子节点(仅下一级节点),注意不是本节点
READ,简写为r,可以读取节点数据及显示子节点列表
WRITE,简写为w,可设置节点数据
ADMIN,简写为a,可以设置节点访问控制列表
查看ACL
getAcl /parent
返回
[zk: localhost:2181(CONNECTED) 122] getAcl /parent
'world,'anyone
: cdrwa
默认创建的节点的权限是最开放的,所有都可以增删查改管理。
设置ACL
设置节点对所有人都有删(d)和管理权限(a)
setAcl /parent world:anyone:da
所以去读取数据的时候会提示
[zk: localhost:2181(CONNECTED) 124] get /parent
Authentication is not valid : /parent
先添加用户:
addauth digest zhangsan:12345
再设置权限,这个节点只有zhangsan这个用户拥有所有权限
setAcl /parent auth:zhangsan:12345:rdwca
超级管理员
超级管理员的用户名为super,密码自定义比如:admin
首先调用DigestAuthenticationProvider.generateDigest(“super:admin”)获取签名,比如结果为:super:xQJmxLMiHGwaqBvst5y6rkB6HQs=
在启动Zookeeper服务端(zkServer.sh脚本中)时加入-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=
启动zookeeper并使用客户端进行连接
如果遇到没有操作权限的节点,这时可以addauth digest super:admin来开启管理员,即有所有权限
tickTime:Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。tickTime以毫秒为单位。该参数用来定义心跳的间隔时间,zookeeper的客户端和服务端之间也有和web开发里类似的session的概念,而zookeeper里最小的session过期时间就是tickTime的两倍。
nitLimit:Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许Follower在 initLimit 时间内完成这个工作。通常情况下,我们不用太在意这个参数的设置。如果ZK集群的数据量确实很大了,Follower在启动的时候,从Leader上同步数据的时间也会相应变长,因此在这种情况下,有必要适当调大这个参数了。默认为10。
syncLimit:在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。如果Leader发出心跳包在syncLimit之后,还没有从Follower那里收到响应,那么就认为这个Follower已经不在线了。
dataDir:存储快照文件snapshot的目录。默认情况下,事务日志也会存储在这里。建议同时配置参数dataLogDir, 事务日志的写性能直接影响zk性能。
clientPort:客户端连接服务器的端口
maxClientCnxns:Zookeeper服务端同时支持在线的客户端数量限制
autopurge.snapRetainCount:Zookeeper服务端自动清除多余的日志和快照文件时至少保留的快照数量
autopurge.purgeInterval:Zookeeper服务端自动清除多余的日志和快照文件的周期
Zookeeper自带了两个客户端:
一个是命令行客户端,就是zkCli.sh/zkCli.cmd
一个是Java客户端,就是Zookeeper类,也就是我说的原生客户端
连接服务端
/**
* connectString 连接地址可以写多个,比如"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183",当客户端与服务端的Socket连接断掉后就会重试去连其他的服务器地址
* sessionTimeout 客户端设置的话会超时时间
* watcher 监听器
*/
ZooKeeper zooKeeper = new ZooKeeper("192.168.40.52:2181", 60 * 1000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent);
}
});
先忽略监听器,后面详细讲
创建节点
// 创建一个节点,并设置内容,设置ACL(该节点的权限设置),
// 节点类型(7种:持久节点、临时节点、持久顺序节点、临时顺序节点、容器节点、TTL节点、TTL顺序节点)
String result = zooKeeper.create("/luban123", "123".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(result);
enum CreateMode:
PERSISTENT:持久节点
PERSISTENT_SEQUENTIAL:持久顺序节点
EPHEMERAL:临时节点
EPHEMERAL_SEQUENTIAL:临时顺序节点
CONTAINER:容器节点
PERSISTENT_WITH_TTL:TTL持久节点
PERSISTENT_SEQUENTIAL_WITH_TTL:TT持久顺序节点
查询节点
Stat stat = new Stat();
byte[] data = zooKeeper.getData("/luban123", false, stat);
System.out.println(new String(data));
一个节点,除开有节点名字,节点内容之外,还有其他的信息,这些信息成为Stat,表示节点的状态信息,Stat包括:
在查询的时候,要传入一个Stat对象用来承载待查询节点的状态信息。
节点是否存在
Stat stat = zooKeeper.exists("/bml1234", false);
System.out.println(stat); // 如果节点不存在,则stat为null
修改节点
// 修改节点的内容,这里有乐观锁,version表示本次修改, -1表示不检查版本强制更新
// stat表示修改数据成功之后节点的状态
Stat stat = zooKeeper.setData("/bml", "xxx".getBytes(), -1);
删除节点
zooKeeper.delete("/bml123", -1);
创建子节点
String result = zooKeeper.create("/bml/bml123", "123".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(result);
获取子节点
Stat stat = new Stat();
List<String> children = zooKeeper.getChildren("/bml", false, stat);
System.out.println(children);
原生客户端Watch机制
一次性的Watcher
给/bml节点绑定了一个监听,用来监听节点的数据变化和当前节点的删除事件
Stat stat = new Stat();
zooKeeper.getData("/bml", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent);
}
}, stat);
给/bml节点绑定了一个监听,用来监听节点的子节点的变化,子节点的增加和删除
zooKeeper.getChildren("/bml", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("child");
}
});
给/bml123节点绑定了一个监听,用来监听节点的创建事件
zooKeeper.exists("/bml123", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent);
}
});
持久化的Watcher
给/bml123节点绑定了一个持久化监听,可以监听节点的创建、删除、修改、增加子节点、删除子节点事件
zooKeeper.addWatch("/bml123", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent);
}
}, AddWatchMode.PERSISTENT);
递归持久化的Watcher
给/bml123节点绑定了一个持久化监听,可以监听节点的创建、删除、修改、增加子节点、删除子节点事件。
并且还会监听/bml123的所有子节点的变化,包括子节点的创建、删除、修改。
zooKeeper.addWatch("/bml123", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println(watchedEvent);
}
}, AddWatchMode.PERSISTENT_RECURSIVE);
原生客户端异步调用
zooKeeper.create("/bml123/xxx_", "123".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL, new AsyncCallback.StringCallback() {
@Override
public void processResult(int i, String s, Object o, String s1) {
System.out.println(i); // 命令执行结果,可以参考KeeperException.Code枚举
System.out.println(s); // 传入的path
System.out.println(o); // 外部传进来的ctx
System.out.println(s1);// 创建成功后的path,比如顺序节点
}
}, "ctx");
zooKeeper.create("/bml123/xxx_", "123".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL, new AsyncCallback.Create2Callback() {
@Override
public void processResult(int i, String s, Object o, String s1, Stat stat) {
System.out.println(i); // 命令执行结果,可以参考KeeperException.Code枚举
System.out.println(s); // 传入的path
System.out.println(o); // 外部传进来的ctx
System.out.println(s1); // 创建成功后的path,比如顺序节点
System.out.println(stat); // 创建出来的节点的stat
}
}, "ctx");
完整的例子:
/**
* zookeeper原生的客户端操作
*/
public class ZookeeperClient {
// 连接服务端,连接地址可以写多个,比如"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"
// 当客户端与服务端的连接断掉后就会重试去连其他的服务器地址
// watcher
// 初始化timeout
// 启动SendThread(socket, 初始化, 读写事件, 发送时), EventTrhead
// outgoingqueue packet pendingqueue
/**
* connectString 连接地址可以写多个,比如"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183",当客户端与服务端的Socket连接断掉后就会重试去连其他的服务器地址
* sessionTimeout 客户端设置的话会超时时间
* watcher 监听器
*/
public static void main(String[] args) throws Exception {
ZooKeeper zooKeeper = new ZooKeeper("localhost:2181", 60 * 1000, new Watcher() {
public void process(WatchedEvent event) {
System.out.println(event.getPath());
System.out.println(event);
}
});
// 创建一个节点,并设置内容,设置ACL(该节点的权限设置),
// 节点类型(7种:持久节点、临时节点、持久顺序节点、临时顺序节点、容器节点、TTL节点、TTL顺序节点)
/**
* 创建的节点类型枚举
* PERSISTENT:持久化节点
* PERSISTENT_SEQUENTIAL:持久化顺序节点
* EPHEMERAL:临时节点
* EPHEMERAL_SEQUENTIAL:临时顺序节点
* CONTAINER:容器节点
* PERSISTENT_WITH_TTL:TTL节点
* PERSISTENT_SEQUENTIAL_WITH_TTL:顺序的TTL节点
*/
// String s = zooKeeper.create("/p01", "p01_0000".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// System.out.println("创建的持久化节点:"+s);
//创建持久化顺序节点和获取节点内容
// String s1 = zooKeeper.create("/e_01", "e01_1111".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
// System.out.println("创建的持久化顺序节点:"+s1);
// Stat stat = new Stat();
// byte[] data = zooKeeper.getData(s1, false, stat);
// System.out.println("创建的持久化临时节点内容为="+new String(data));
// System.out.println("状态为:"+stat);
//判断节点是否存在
Stat exists = zooKeeper.exists("/bml", false);
System.out.println("节点存在状态:" + exists);
// 修改节点的内容,这里有乐观锁,version表示本次修改, -1表示不检查版本强制更新
// stat表示修改数据成功之后节点的状态
System.out.println(zooKeeper.setData("/bml", "0000000001".getBytes(), -1));
//删除节点
// zooKeeper.delete("/e_010000000011",-1);
//创建子节点
// String s = zooKeeper.create("/bml/ccc", "cc3333".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//获取子节点
// List children = zooKeeper.getChildren("/bml", false);
// System.out.println(children);
//给节点添加监听(一次性监听),只能监听当前节点的变化和删除事件,并且只能监听一次
// Stat stat = new Stat();
// byte[] data = zooKeeper.getData("/bml", new Watcher() {
// public void process(WatchedEvent event) {
// System.out.println("监听到节点变化="+event);
// }
// }, stat);
// System.out.println("data="+new String(data));
// System.out.println("状态="+stat);
// System.in.read();
//
// 给/bml节点绑定了一个监听,用来监听节点的子节点的变化,子节点的增加和删除
//也是一个一次性事件,只能监听子节点的删除,增加,但是只监听一次,一次过后不再监听
// zooKeeper.getChildren("/bml", new Watcher() {
// public void process(WatchedEvent watchedEvent) {
// System.out.println(watchedEvent);
// }
// });
给/xxx节点绑定了一个监听,用来监听节点的创建事件,仅仅 只是创建事件
// zooKeeper.exists("/xxx", new Watcher() {
// public void process(WatchedEvent watchedEvent) {
// System.out.println(watchedEvent);
// }
// });
//给/bml111节点绑定了一个持久化监听,可以监听节点的创建、删除、修改、增加子节点、删除子节点事件
// zooKeeper.addWatch("/bml111", new Watcher() {
// public void process(WatchedEvent watchedEvent) {
// System.out.println(watchedEvent);
// }
// },AddWatchMode.PERSISTENT);
// System.in.read();
// 给/bml111节点绑定了一个持久化监听,可以监听节点的创建、删除、修改、增加子节点、删除子节点事件
// 并且还会监听/bml111的所有子节点的变化,包括子节点的创建、删除、修改
// zooKeeper.addWatch("/bml111", new Watcher() {
// public void process(WatchedEvent watchedEvent) {
// System.out.println(watchedEvent);
// }
// },AddWatchMode.PERSISTENT_RECURSIVE);
// System.in.read();
zooKeeper.create("/bml/ss_", "123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL, new AsyncCallback.StringCallback() {
public void processResult(int i, String s, Object o, String s1) {
System.out.println(i); // 命令执行结果,可以参考KeeperException.Code枚举
System.out.println(s); // 传入的path
System.out.println(o); // 外部传进来的ctx
System.out.println(s1);// 创建成功后的path,比如顺序节点
}
}, "ctx");
System.in.read();
}
}
连接服务端
// 重连策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// 建立客户端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.40.52:2181")
.sessionTimeoutMs(60 * 1000) // 会话超时时间
.connectionTimeoutMs(5000) // 连接超时时间
.retryPolicy(retryPolicy)
.build();
client.start();
创建节点
String path = client.create().forPath("/bml-curator", "123".getBytes());
System.out.println(path);
查询节点
byte[] bytes = client.getData().forPath("/bml-curator");
System.out.println(new String(bytes));
节点是否存在
Stat stat = client.checkExists().forPath("/bml-curator");
System.out.println(stat);
修改节点
Stat stat = client.setData().forPath("/bml-curator", "456".getBytes());
System.out.println(stat);
删除节点
client.delete().forPath("/bml-curator");
创建子节点
String path = client.create().forPath("/bml-curator/test", "xxx".getBytes());
System.out.println(path);
获取子节点
List<String> list = client.getChildren().forPath("/bml-curator");
System.out.println(list);
创建容器节点
client.createContainers("/bml-curator-containers");
创建TTL节点
String path = client.create().withTtl(3000).withMode(CreateMode.PERSISTENT_WITH_TTL).forPath("/bml-ttl");
System.out.println(path);
前提是zkServer启动时得做一点而外的配置,在zkServer.sh中修改:
Curator中的Watch机制
NodeCache
NodeCache nodeCache = new NodeCache(client, "/bml");
nodeCache.start(true);
System.out.println(nodeCache.getCurrentData());
NodeCache监听自身节点的数据变化
NodeCache nodeCache = new NodeCache(client, "/bml");
nodeCache.start();
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println(123);
}
});
PathChildrenCache
PathChildrenCache能够监听自身节点下的子节点的变化
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/bml", true);
pathChildrenCache.start();
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
switch (event.getType()) {
case CHILD_ADDED: {
System.out.println("Node added: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
break;
}
case CHILD_UPDATED: {
System.out.println("Node changed: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
break;
}
case CHILD_REMOVED: {
System.out.println("Node removed: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
break;
}
}
}
});
TreeCache
TreeCache即能够监听自身节点的变化,也能监听子节点的变化
TreeCache treeCache = new TreeCache(client, "/bml");
treeCache.start();
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
if (event.getData() != null) {
System.out.println("type=" + event.getType() + " path=" + event.getData().getPath());
} else {
System.out.println("type=" + event.getType());
}
}
});
CuratorCache
这是新版curator-framework新增的特性,用来替换以上三个Cache的。
forCreates:监听监听节点的创建,和子节点的创建
forChanges:监听监听节点的内容的改变,和子节点内容的改变
forDeletes:监听监听节点的删除,和子节点的删除
CuratorCacheListener listener = CuratorCacheListener.builder()
.forCreates(node -> System.out.println(String.format("Node created: [%s]", node)))
.forChanges((oldNode, node) -> System.out.println(String.format("Node changed. Old: [%s] New: [%s]", oldNode, node)))
.forDeletes(oldNode -> System.out.println(String.format("Node deleted. Old value: [%s]", oldNode)))
.forInitialized(new Runnable() {
@Override
public void run() {
System.out.println("Cache initialized");
}
})
.build();
CuratorCache curatorCache = CuratorCache.build(client, "/bml");
curatorCache.listenable().addListener(CuratorCacheListener.builder().forAll(listener).build());
curatorCache.start();
System.in.read();
Curator中的异步调用
String s = client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println(curatorEvent);
}
}).forPath("/bml/h_", "123".getBytes());
System.out.println(s);
System.in.read();
完整的例子:
public class CuratorTest {
public static void main(String[] args) throws Exception {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.93.10:2181")
.sessionTimeoutMs(60 * 1000) // 会话超时时间
.connectionTimeoutMs(5000) // 连接超时时间
.retryPolicy(retryPolicy)
.build();
client.start();
//创建节点
// String s = client.create().forPath("/bbb", "456".getBytes());
// System.out.println(s);
// byte[] bytes = client.getData().forPath("/bbb");
// System.out.println(new String(bytes));
//
// Stat stat = client.checkExists().forPath("/bbb");
// System.out.println(stat);
Stat stat = client.setData().forPath("/bbb", "456".getBytes());
System.out.println(stat);
// client.delete().forPath("/bbb");
// String path = client.create().forPath("/bbb/test", "xxx".getBytes());
// System.out.println(path);
// List list = client.getChildren().forPath("/bbb");
// System.out.println(list);
// client.createContainers("/bbb-containers");
// String path = client.create().withTtl(3000).withMode(CreateMode.PERSISTENT_WITH_TTL).forPath("/bbb-ttl");
// System.out.println(path);
// client.getData().usingWatcher(new CuratorWatcher() {
// @Override
// public void process(WatchedEvent watchedEvent) throws Exception {
// System.out.println(watchedEvent);
// }
// }).forPath("/bbb");
// NodeCache监听自身节点的数据变化
// NodeCache nodeCache = new NodeCache(client, "/bbb");
// nodeCache.start();
// nodeCache.getListenable().addListener(new NodeCacheListener() {
// @Override
// public void nodeChanged() throws Exception {
// System.out.println(123);
// }
// });
// PathChildrenCache能够监听自身节点下的子节点的变化
// PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/bbb", true);
// pathChildrenCache.start();
//
// pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
// @Override
// public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
// switch (event.getType()) {
// case CHILD_ADDED: {
// System.out.println("Node added: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
// break;
// }
//
// case CHILD_UPDATED: {
// System.out.println("Node changed: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
// break;
// }
//
// case CHILD_REMOVED: {
// System.out.println("Node removed: " + ZKPaths.getNodeFromPath(event.getData().getPath()));
// break;
// }
// }
// }
// });
// TreeCache即能够监听自身节点的变化,也能监听子节点的变化
// TreeCache treeCache = new TreeCache(client, "/bbb");
// treeCache.start();
// treeCache.getListenable().addListener(new TreeCacheListener() {
// @Override
// public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
// if (event.getData() != null) {
// System.out.println("type=" + event.getType() + " path=" + event.getData().getPath());
// } else {
// System.out.println("type=" + event.getType());
// }
// }
// });
// System.in.read();
// 一个监听器
// 节点创建,子节点的创建
// 节点内容的改变,子节点内容的改变
// 节点的删除,子节点的删除
// CuratorCacheListener listener = CuratorCacheListener.builder()
// .forCreates(node -> System.out.println(String.format("Node created: [%s]", node)))
// .forChanges((oldNode, node) -> System.out.println(String.format("Node changed. Old: [%s] New: [%s]", oldNode, node)))
// .forDeletes(oldNode -> System.out.println(String.format("Node deleted. Old value: [%s]", oldNode)))
// .forInitialized(new Runnable() {
// @Override
// public void run() {
// System.out.println("Cache initialized");
// }
// })
// .build();
//
//
// CuratorCache curatorCache = CuratorCache.build(client, "/bbb");
// curatorCache.listenable().addListener(CuratorCacheListener.builder().forAll(listener).build());
// curatorCache.start();
//
// System.in.read();
// String s = client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).inBackground(new BackgroundCallback() {
// @Override
// public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
// System.out.println(curatorEvent);
// }
// }).forPath("/bbb/h_", "123".getBytes());
//
// System.out.println(s);
//
// System.in.read();
}
}
zk的应用场景之一就有分布式锁,可以通过zk的顺序节点来做一个分布式锁,关于zk的分布式锁的原理网络上有很多,我这里就不赘述了,网络上原理和图都有很多,我这里只是根据zk的顺序节点自己实现了一个简单的分布式锁
DistributedLock
我这里使用DistributedLock来实现分布式锁,没有写太复杂,就是简单的使用zk原始的一些监听机制去实现,代码可能有些地方写的不够好,也没有去管 ,反正就是理解zk分布式锁的一种思想即可
package com.zookeeper.demo1.lock;
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
public class DistributedLock {
private String tmpPath = "/lock_distributed";
private String seqPath = "lock_";
private Function<String, Integer> integerFunction = Integer::valueOf;
private ZooKeeper zooKeeper;
private static ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();
public void init() throws IOException {
zooKeeper = new ZooKeeper("192.168.93.10:2181", 60 * 1000, new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
}
public void delete(String path) throws Exception {
zooKeeper.delete(path, -1);
}
public String getLock(int time) throws Exception {
String path = null;
//1.创建顺序临时节点
String createPath = tmpPath + "/" + seqPath;
String seqNode = zooKeeper.create(createPath, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
int createSeq = integerFunction.apply(seqNode.substring(23));
//2.获取所有的顺序节点
List<String> children = zooKeeper.getChildren(tmpPath, false);
System.out.println(children);
Map<Integer, String> map = children.stream().map(i -> {
Integer seq = integerFunction.apply(i.substring(5));
return new Node(seq, i);
}).collect(Collectors.toMap(Node::getSeq, Node::getPath, (k, v) -> v));
List<Integer> collect = map.keySet().
stream().
sorted((a, b) -> a.compareTo(b)).
collect(Collectors.toList());
//取出最小的那个临时节点
Integer minSeq = collect.get(0);
String minPath = map.get(minSeq);
if (minSeq != createSeq) {
LockStatus.STATUS state = getWaitLock(tmpPath + "/" + minPath, createSeq, time).getState();
System.out.println(state);
if (state == LockStatus.STATUS.FAILURE) {
//将之前创建的节点删除
delete(seqNode);
return null;
}
}
path = seqNode;
threadLocal.remove();
return path;
}
private LockStatus getWaitLock(String minPath, Integer createSeq, int timeout) throws KeeperException, InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
LockStatus status = new LockStatus();
threadLocal.set(Boolean.FALSE);
//3.没有获取到锁,监控最小的节点的删除事件
zooKeeper.exists(minPath, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (Event.EventType.NodeDeleted == event.getType()) {
//删除事件
try {
while (true) {
//这里的判断其实就是为了如果监听到时间,但是已经超过客户端设置的超时时间,因为到这里也不一定能够获取到锁,所以直接放弃
if (threadLocal.get() == Boolean.TRUE) {
threadLocal.remove();
status.setState(LockStatus.STATUS.FAILURE);
break;
}
List<String> children = zooKeeper.getChildren(tmpPath, false);
Map<Integer, String> map = children.stream().map(i -> {
Integer seq = integerFunction.apply(i.substring(5));
return new Node(seq, i);
}).collect(Collectors.toMap(Node::getSeq, Node::getPath, (k, v) -> v));
List<Integer> collect = map.keySet().
stream().
sorted((a, b) -> a.compareTo(b)).
collect(Collectors.toList());
Integer minSeq = collect.get(0);
if (minSeq == createSeq) {
latch.countDown();
status.setState(LockStatus.STATUS.SUUCESS);
break;
}
if (minSeq != createSeq) {
//没有获取到锁,先让出cpu的执行权限
Thread.yield();
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
if (timeout > 0) {
//等待超时的时间内没有获取到锁就放弃获取锁
threadLocal.set(latch.await(timeout, TimeUnit.SECONDS));
} else {
latch.await();
}
return status;
}
}
锁状态类LockStatus
public class LockStatus {
private STATUS state = STATUS.FAILURE;
public STATUS getState() {
return state;
}
public void setState(STATUS state) {
this.state = state;
}
enum STATUS{
SUUCESS,
FAILURE;
}
}
Node
public class Node {
public Node(Integer seq, String path) {
this.seq = seq;
this.path = path;
}
private Integer seq;
private String path;
public Integer getSeq() {
return seq;
}
public void setSeq(Integer seq) {
this.seq = seq;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
服务测试1:
public class LockTest {
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
DistributedLock lock = new DistributedLock();
String path = null;
try {
lock.init();
path = lock.getLock(0);
if (path != null) {
System.out.println(Thread.currentThread().getName() + ":获取锁成功");
System.out.println(Thread.currentThread().getName() + ":正在处理...");
Thread.sleep(2000);//模拟业务处理请求
System.out.println(Thread.currentThread().getName() + ":处理完成");
} else {
System.out.println(Thread.currentThread().getName() + ":获取锁失败");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (path != null) {
try {
lock.delete(path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, "Thread-" + i).start();
}
}
}
服务测试2:
public class LockTest1 {
public static void main(String[] args) throws Exception {
for (int i = 11; i <= 20; i++) {
new Thread(() -> {
DistributedLock lock = new DistributedLock();
String path = null;
try {
lock.init();
path = lock.getLock(0);
if (path != null) {
System.out.println(Thread.currentThread().getName() + ":获取锁成功");
System.out.println(Thread.currentThread().getName() + ":正在处理...");
Thread.sleep(2000);//模拟业务处理请求
System.out.println(Thread.currentThread().getName() + ":处理完成");
} else {
System.out.println(Thread.currentThread().getName() + ":获取锁失败");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (path != null) {
try {
lock.delete(path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, "Thread-" + i).start();
}
}
}
用两个服务,也就是启动了两个进程分别去获取锁,我这里测试有个问题就是如果并发大了,如果没有设置超时时间,很有可能就卡在await那里,这个我研究过,其实就是监听事件没有监听到,没有进入监听逻辑的原因,这个问题我也没有太去纠结了,反正写这个例子就是为了把zk的分布式锁的一些思想简单用代码来实现一下,有兴趣的可以进行交流,输出如下:
服务1测试结果:
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
[lock_0000000002, lock_0000000003, lock_0000000000, lock_0000000001, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000007, lock_0000000004, lock_0000000005]
Thread-6:获取锁成功
Thread-6:正在处理...
Thread-6:处理完成
SUUCESS
Thread-8:获取锁成功
Thread-8:正在处理...
Thread-8:处理完成
SUUCESS
Thread-4:获取锁成功
Thread-4:正在处理...
Thread-4:处理完成
SUUCESS
Thread-7:获取锁成功
Thread-7:正在处理...
Thread-7:处理完成
SUUCESS
Thread-1:获取锁成功
Thread-1:正在处理...
Thread-1:处理完成
SUUCESS
Thread-10:获取锁成功
Thread-10:正在处理...
Thread-10:处理完成
SUUCESS
Thread-3:获取锁成功
Thread-3:正在处理...
Thread-3:处理完成
SUUCESS
Thread-2:获取锁成功
Thread-2:正在处理...
Thread-2:处理完成
SUUCESS
Thread-9:获取锁成功
Thread-9:正在处理...
Thread-9:处理完成
SUUCESS
Thread-5:获取锁成功
Thread-5:正在处理...
Thread-5:处理完成
服务2测试结果:
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
[lock_0000000002, lock_0000000013, lock_0000000003, lock_0000000014, lock_0000000011, lock_0000000001, lock_0000000012, lock_0000000010, lock_0000000008, lock_0000000019, lock_0000000009, lock_0000000006, lock_0000000017, lock_0000000007, lock_0000000018, lock_0000000004, lock_0000000015, lock_0000000005, lock_0000000016]
SUUCESS
Thread-16:获取锁成功
Thread-16:正在处理...
Thread-16:处理完成
SUUCESS
Thread-12:获取锁成功
Thread-12:正在处理...
Thread-12:处理完成
SUUCESS
Thread-13:获取锁成功
Thread-13:正在处理...
Thread-13:处理完成
SUUCESS
Thread-19:获取锁成功
Thread-19:正在处理...
Thread-19:处理完成
SUUCESS
Thread-15:获取锁成功
Thread-15:正在处理...
Thread-15:处理完成
SUUCESS
Thread-20:获取锁成功
Thread-20:正在处理...
Thread-20:处理完成
SUUCESS
Thread-18:获取锁成功
Thread-18:正在处理...
Thread-18:处理完成
SUUCESS
Thread-17:获取锁成功
Thread-17:正在处理...
Thread-17:处理完成
SUUCESS
Thread-11:获取锁成功
Thread-11:正在处理...
Thread-11:处理完成
SUUCESS
Thread-14:获取锁成功
Thread-14:正在处理...
Thread-14:处理完成
代码我放在gitee上https://gitee.com/scjava/zookeeper-demo.git