Zookeeper是apache Hadoop项目下的一个子项目,是一个树形目录服务
Zookeeper 翻译过来就是动物园管理员,他是用来管Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员。简称zk
Zookeeper是一个分布式的,开源的分布式应用的协调服务。
Zookeeper主要功能包括:
配置事务
分布式锁
集群管理
它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户
ZooKeeper是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。
zkServer
Linux下常用命令
启动 ZooKeeper 服务:
./zkServer.sh start
查看 ZooKeeper 服务状态:
./zkServer.sh status
停止 ZooKeeper 服务 :
./zkServer.sh stop
重启 ZooKeeper 服务 :
./zkServer.sh restart
打开另外一个cmd运行客户端
zkCli
常用客户端命令如下:
获取帮助
help
客户端断开连接
quit
查看子节点
//ls / 表示查看根节点下的子节点
ls 父节点路径(父节点全路径或相对路径)
创建子节点(默认是持久化节点)
/*
比如 create /jobs 表示在 / 下创建 jobs 子节点
可以给节点存储数据,也可以不存储数据
比如 create /jobs date 表示在 / 下创建 jobs 子节点并存储的数据为 date
*/
create 父节点路径 [数据内容]
获取节点的数据
// 比如 get /jobs 表示获取 jobs 节点中存储的数据,应该会获取到 date
get 节点全路径或相对路径
设置或修改节点的数据值
//比如 set /jobs newdate 表示将 jobs 节点的数据设置为 newdate
set 节点全路径或相对路径 [数据内容]
删除单个节点
/*
比如 delete /jobs 表示删除 / 下面的 jobs 节点
但是如果 jobs 节点下面有子节点的话,该命令就无法删除 jobs 节点了
*/
delete 节点全路径或相对路径
//比如 deleteall /test 表示删除 / 下面的 test 以及 test 下的所有节点
deleteall 节点全路径或相对路径
//当客户端断开与 zookeeper 的拦截后,所创建的临时节点会自动被删除掉
create -e 子节点全路径或相对路径 [数据内容]
/*
创建的节点名称,会自动被添加上数字编号
比如多次运行 create -s /aaa 后,通过 ls / 查看 / 下面的子节点列表
会发现类似 /aaa0000000001 /aaa0000000002 /aaa0000000003 的顺序节点
*/
create -s 子节点全路径或相对路径 [数据内容]
/*
临时顺序节点,兼有临时节点和顺序节点的特性,常用于分布式所的应用场景
*/
create -es 子节点全路径或相对路径 [数据内容]
ls -s 节点全路径或相对路径
czxid
:节点被创建的事务ID
ctime
:节点的创建时间
mtime
: 修改时间
pzxid
:子节点列表最后一次被更新的事务ID
cversion
:子节点的版本号
dataversion
:数据版本号
aclversion
:权限版本号
ephemeralOwner
:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0
dataLength
:节点存储的数据的长度
numChildren
:当前节点的子节点个数
Curator是 ApacheZooKeeper 的Java客户端库。
导入依赖
此处会有curator和zookeeper版本兼容问题,需注意
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>5.3.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>5.3.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.36version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.36version>
dependency>
dependencies>
### 设置日志级别###
#log4j.rootLogger = debug,stdout,D,E
log4j.rootLogger = off,stdout
### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
#log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.D.File = E://logs/log.log
#log4j.appender.D.Append = true
#log4j.appender.D.Threshold = DEBUG
#log4j.appender.D.layout = org.apache.log4j.PatternLayout
#log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
#
### 输出ERROR 级别以上的日志到=E://logs/error.log ###
#log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.E.File =E://logs/error.log
#log4j.appender.E.Append = true
#log4j.appender.E.Threshold = ERROR
#log4j.appender.E.layout = org.apache.log4j.PatternLayout
#log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
建立连接
connectString | 连接字符串:zk server地址和端口 127.0.0.1:2181,127.0.0.1:2182 |
sessionTimeoutMs | 会话超时时间: 单位ms,默认60 * 1000 |
connectionTimeoutMs | 连接超时时间: 单位ms ,默认15 * 1000 |
retryPolicy | 重试策略 |
/*
重试策略
参数1:间隔多长时间重试
参数2:重试多少次
*/
RetryPolicy retryPolicy =new ExponentialBackoffRetry(3000,10);
/**
* 第一种方式
* 参数1 connectString: 连接字符串:zk server地址和端口 127.0.0.1:2181,127.0.0.1:2182
* 参数2 sessionTimeoutMs: 会话超时时间: 单位ms
* 参数3 connectionTimeoutMs: 连接超时时间: 单位ms
* 参数4 retryPolicy: 重试策略
*/
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",
60 * 1000,
15 * 1000,
retryPolicy);
client.start();
/*
重试策略
参数1:间隔多长时间重试
参数2:重试多少次
*/
RetryPolicy retryPolicy =new ExponentialBackoffRetry(3000,10);
//第二种方式
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.namespace("hello") //设置操作根目录
.build();
client.start();
创建节点:create 临时 持久 顺序 数据
- 基本创建
- 设置节点,带有数据
- 设置节点类型
- 创建多级节点
基本创建
创建节点,若节点没有数据,则默认节点数据为ip地址
语法:
client.create().forPath("")
@Test
public void testCreat() throws Exception {
//基本创建
//创建节点,若节点没有数据,则默认节点数据为ip地址
client.create().forPath("/app1");
}
[zk: localhost:2181(CONNECTED) 2] ls /
[dubbo, hello, services, zookeeper]
[zk: localhost:2181(CONNECTED) 3] ls /hello
[app1]
[zk: localhost:2181(CONNECTED) 4] get /hello/app1
192.168.205.116
设置节点,带有数据
语法:
client.create().forPath("",data;
@Test
public void testCreat2() throws Exception {
//设置节点,带有数据
client.create().forPath("/app2","hello zookeeper".getBytes());
}
[zk: localhost:2181(CONNECTED) 5] get /hello/app2
hello zookeeper
设置节点类型
PERSISTENT
:持久化节点
EPHEMERA
:临时节点:-e
PERSISTENT_SEQUENTIAL
持久化顺序节点:-s
EPHEMERAL_SEQUENTIAL
临时顺序节点:-es语法:
client.create().withMode(节点类型).forPath()
@Test
public void testCreat3() throws Exception {
//设置节点,带有数据
client.create().withMode(CreateMode.EPHEMERAL ).forPath("/app3","hello zookeeper".getBytes());
//加个延时试一下当前会话没关闭时,该节点在吗
TimeUnit.MILLISECONDS.sleep(1000*60);
}
创建多级节点
creatingParentsIfNeeded
,如果父节点不存在创建父节点语法:
client.create().creatingParentsIfNeeded().forPath()
@Test
public void testCreat4() throws Exception {
//设置节点,带有数据
//creatingParentsIfNeeded,如果父节点不存在创建父节点
client.create().creatingParentsIfNeeded().forPath("/app4/p1","hello zookeeper".getBytes());
}
查询节点数据 get
语法:
client.getData().forPath("")
@Test
public void testGet() throws Exception {
byte[] bytes = client.getData().forPath("/app1");
System.out.println(new String(bytes));
}
192.168.205.116
查询子节点 ls
PS:注意此处的"/"是指定的hello节点,并不是zkcli的根节点
语法:
client.getChildren().forPath("")
@Test
public void testGet2() throws Exception {
List<String> list = client.getChildren().forPath("/");
System.out.println(list);
}
[app2, app1, app4]
查询节点状态信息 ls -s
语法:
client.getData().storingStatIn(stat).forPath("/app4")
stat 为状态对象
里面包括:
czxid
:节点被创建的事务ID
ctime
:节点的创建时间
mtime
: 修改时间
pzxid
:子节点列表最后一次被更新的事务ID
cversion
:子节点的版本号
dataversion
:数据版本号
aclversion
:权限版本号
ephemeralOwner
:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0
dataLength
:节点存储的数据的长度
numChildren
:当前节点的子节点个数
@Test
public void testGet3() throws Exception {
//创建stat
Stat stat =new Stat();
System.out.println(stat);
System.out.println("------查询之后-----");
client.getData().storingStatIn(stat).forPath("/app4");
System.out.println(stat);
}
0,0,0,0,0,0,0,0,0,0,0
------查询之后-----
387,387,1681452139819,1681452139819,0,1,0,0,0,1,388
修改数据
语法:
client.setData().forPath("",Data);
@Test
public void testSet() throws Exception {
client.setData().forPath("/app1","setapp1date".getBytes());
}
[zk: localhost:2181(CONNECTED) 11] get /hello/app1
setapp1date
根据版本修改
语法
client.setData().withVersion(version).forPath("", data);
version:查询出来的,目的是为了让其他客户端/线程不干扰该次修改操作;
@Test
public void testSet2() throws Exception {
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath("/app1");
int version =stat.getVersion();//查询当前节点状态的版本
System.out.println(version);
client.setData().withVersion(version).forPath("/app1", "hhahaha".getBytes());//修改一次vsrsion++
}
删除单个节点
语法:
client.delete().forPath("");
@Test
public void testDelete() throws Exception {
client.delete().forPath("/app1");
}
删除带有子节点的节点
deletingChildrenIfNeeded
:带有子节点的节点语法
client.delete().deletingChildrenIfNeeded().forPath("");
@Test
public void testDelete2() throws Exception {
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
必须成功的删除
防止网络抖动,本质就是重试
guaranteed
:保证,必须语法:
client.delete().guaranteed().forPath("");
@Test
public void testDelete3() throws Exception { client.delete().guaranteed().forPath("/app2");
}
回调
绑定一个回调函数,删除成功后自动执行该方法。。。。
语法:
client.delete().inBackground(回调函数).forPath("");
@Test
public void testDelete4() throws Exception {
client.delete().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println("删除成功....");
System.out.println(curatorEvent);
}
}).forPath("/app1");
}
删除成功....
CuratorEventImpl{type=DELETE, resultCode=0, path='/app1', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}
ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。
ZooKeeper中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
ZooKeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。
Curator引入了Cache来实现对ZooKeeper服务端事件的监听。
- Cache不止可以用来事件监听,还可以来实现Zookeeper节点数据的缓存
ZooKeeper提供了三种Watcher:
- NodeCache 只是监听某一个特定的节点
- PathChildrenCache:监控一个ZNode的子节点.
- TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合
CuratorCache:可以监控整个树上的所有节点
在curator 5.1.0后, NodeCache 、PathChildrenCache 、TreeCache,被弃用。使用新的CuratorCache
进行监听
NodeCache只是监听某一个特定的节点
- 给单一节点注册监听器
@Test
public void testNodeCatch() throws Exception {
//1. 创建NodeCatch对象
NodeCache nodeCache =new NodeCache(client,"/app1");
//2. 注册监听
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("节点改变了...");
//获取修改后当前节点的数据
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
}
});
//3. 开启监听,若果设置为true,开启监听
nodeCache.start(true);
//此处延时只是让,会话不会快速关闭,以便测试监听功能
while (true){
}
}
监控一节点的的子节点
@Test
public void testPathChildrenCache() throws Exception {
//1. 创建NodeCatch对象
PathChildrenCache childrenCache =new PathChildrenCache(client,"/app2",true);
//2. 注册监听
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
//类似于删除节点的回调
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
System.out.println("子节点变化了....");
System.out.println(pathChildrenCacheEvent);
//监听变更,拿到变更后的数据
// 1.获取类型
PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
//2. 判断类型是否是update
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
System.out.println("数据改变");
byte[] data = pathChildrenCacheEvent.getData().getData();
System.out.println(new String(data));
}
}
});
//3. 开启监听,若果设置为true,开启监听
childrenCache.start(true);
//此处延时只是让,会话不会快速关闭,以便测试监听功能
while (true){
}
}
可以监控整个树上的所有节点
@Test
public void testTreeCache() throws Exception {
//1. 创建NodeCatch对象
TreeCache treeCache =new TreeCache(client,"/app2");
//2. 注册监听
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
System.out.println("节点变化");
System.out.println(treeCacheEvent);
}
});
//3. 开启监听,若果设置为true,开启监听
treeCache.start();
//此处延时只是让,会话不会快速关闭,以便测试监听功能
while (true){
}
}
可以监控整个树上的所有节点
@Test
public void testCuratorCache() throws Exception {
//1. 创建NodeCatch对象
CuratorCache treeCache =CuratorCache.build(client,"/app2");
//2. 注册监听
treeCache.listenable().addListener(new CuratorCacheListener() {
@Override
public void event(Type type, ChildData oldData, ChildData data) {
if (type.equals(Type.NODE_CHANGED)){//节点发生改变
String path = oldData.getPath();
System.out.println("节点"+path+"改变");
System.out.println("----------老数据-----------");
byte[] oldDataStr = oldData.getData();
System.out.println(new String(oldDataStr));
System.out.println("----------新数据-----------");
byte[] dataStr = data.getData();
System.out.println(new String(dataStr));
}
}
});
//3. 开启监听,若果设置为true,开启监听
treeCache.start();
//此处延时只是让,会话不会快速关闭,以便测试监听功能
while (true){
}
}
在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。
但当我们的应用是分布式集群工作的情况下,属于多VM下的工作环境,跨IVM之间已经无法通过多线程的锁解决同步问题。
那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题——这就是分布式锁。
PS:也就是说在多机情况下,多个机器都可以修改同一数据,就算我们给每个机器都加锁了,但是会出现多个机器同时修改该数据,会发生数据错乱。。。
解决方法
① 基于数据库的分布式锁
唯一性约束
当我们多个服务都要执行,插入或修改操作,数据库唯一性约束保证,只能有一个服务可以成功。。。
基于数据库排他锁
获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。
缺点:操作数据库开销大,进行优化会过于复杂,性能不高,不建议
② 基于缓存的分布式锁
Redis中没有分布式锁,我们可以使用setnx
设计一个分布式锁
setnx
:如果key存在存值失败,如果不存在则存值
添加一个key,该key为分布式锁,我们知道setnx在设置数据时如果数据存在则返回0
设置该数据为锁,其他客户端要操作数据前先通过该指令的返回值检测如果返回值为0
则表示当前数据已被锁定不能操作,如果返回值为1表示加锁,然后操作。
setnx lock-num 1
对加锁的数据使用后要解锁,通过del lock-num
移除数据的方式实现解锁过程
del lock-num
③ 基于Zookeeper的分布式锁
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。
PS:在lock节点下,创建临时顺序节点,序号小的先获取锁,若不是序号最小的,给比他还小的添加删除事件监听器,当小的被删除,再次判断是不是最小的,依次…
为啥要是临时节点?
发生宕机时,该锁也可以被删除
无多机条件,只是用多线程模拟
在Curator有五种锁方案:
售票类
public class Ticket implements Runnable{
private Integer tickets =20;//票数
private CuratorFramework client;
private InterProcessMutex lock; //声明可重入排他性锁
public Ticket() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //链接端口号
.sessionTimeoutMs(60 * 1000) //会话超时时间
.connectionTimeoutMs(15 * 1000) //连接超时时间
.retryPolicy(retryPolicy) //重试机制
.build();
client.start();
lock =new InterProcessMutex(client,"/lock");
}
@Override
public void run() {
while (true){
try {
//获取锁
lock.acquire(3000, TimeUnit.MILLISECONDS);
if (tickets>0){
//哪个线程买票
System.out.println(Thread.currentThread()+":"+tickets);
TimeUnit.MILLISECONDS.sleep(100);
tickets--;
}
if (tickets==0){
System.out.println("票售空了...");
break;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
//释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Ticket ticket =new Ticket();
//三个线程,一个线程模拟一台机器
Thread t1 =new Thread(ticket,"p1");
Thread t2 =new Thread(ticket,"p2");
Thread t3 =new Thread(ticket,"p3");
t1.start();
t2.start();
t3.start();
}
}
Leader选举:
Serverid:服务器ID
Zxid:数据ID
在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了。
Zookeeper的三种角色
Zookeeper 集群中Server有三种角色,Leader、Follower 和 Observer
Leader:负责投投票的发起与决议,更新系统状态,写数据
Follower:用于接收客户端请求并用来返回结果,在选主过程中参与投票
Observer:可以接受客户端连接,将写请求转发给leader节点,但是不参与投票过程,只同步leader状态,主要存在目的就是为了提高读取效率
引进Observer角色作用:Zookeeper需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能;引入 Observer, Observer不参与投票; Observer接受客户端的连接,并将写请求转发给leader节点; 加入更多Observer节点,提高伸缩性,同时不影响吞吐率。
Zookeeper 建议集群节点个数为奇数,只要超过一半的机器能够正常提供服务,那么整个集群都是可用的状态,最少满足2n+1台(n >= 1)
Zookeeper 的数据一致性是依靠ZAB协议完成的。
ZAB(ZooKeeper Atomic Broadcast 原子广播)协议
是为 ZooKeeper 特殊设计的一种支持崩溃恢复的原子广播协议。在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式(即Leader和Follower模型)的系统架构来保持集群中各个副本之间的数据一致性。
ZAB 协议包括有两种模式,分别是 崩溃恢复和消息广播。
当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务 器开始与新的 Leader 服务器进行数据同步,当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行处理。
1.在Client向Follwer发出一个写的请求
2.Follwer把请求发送给Leader
3.Leader接收到以后开始发起投票并通知Follwer进行投票
4.Follwer把投票结果发送给Leader
5.Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;
6.Follwer把请求结果返回给Client
Follower主要有四个功能:
1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
2. 接收Leader消息并进行处理;
3. 接收Client的请求,如果为写请求,发送给Leader进行投票;
4. 返回Client结果。
真实的集群是需要部署在不同的服务器上的,但是在我们测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建伪集群,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。
我们这里要求搭建一个三个节点的Zookeeper集群(伪集群)。
本人电脑配置有限,不能同时启动三个Zookeeper…
具体参考大牛的这篇文章
搭建Zookeeper集群