Zookeeper概述
Zookeeper是Apache Hadoop项目下的一个子项目,是一个树形(数据结构)目录服务。
Zookeeper 翻译过来就是动物园管理员,他是用来管大数据领域的 Hadoop 🐘、Hive🐝、Pig(小猪)等组件的管理员。简称zk。
Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务。
Zookeeper功能
Zookeeper 提供的主要功能包括:
配置管理
分布式锁
集群管理
配置管理
将所有微服务配置文件中相同配置信息,由配置中心进行统一的管理。
优点: 降低各微服务配置文件代码的冗余, 方便对配置信息的统一修改。(例如 : 数据库的连接信息)
分布式锁
多个服务(JVM)操作同一共享资源造成数据的不一致性,通过分布式锁实现服务与服务之间共享资源的互斥性,保证数据的一致性。
集群管理
服务的提供方与消费方将自己的服务地址统一注册到注册中心,由注册中心对客户端的请求进行统一的处理并分发到对应的服务器中。
Zookeeper安装与配置(for Linux)
Zookeeper命令操作
ZK的数据模型
ZooKeeper 是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构.
这里面的每一个节点都被称为: ZNode,每个节点上都会保存自己的数据和节点信息。
节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。
节点可以分为四大类:
PERSISTENT持久化节点
EPHEMERAL临时节点: -e
PERSISTENT SEQUENTIAL持久化顺序节点: -s
EPHEMERAL SEQUENTIAL临时顺序节点: -es
ZK的bin目录
zk的服务端与客户端的运行目录
Zookeeper服务端常用命令
查看
[root@localhost bin]# ./zkServer.sh status
服务端启动状态 -> 已运行
服务端启动状态-> 未运行
停止
[root@localhost bin]# ./zkServer.sh stop
停止服务
启动
[root@localhost bin]# ./zkServer.sh start
开启服务
重启
[root@localhost bin]# ./zkServer.sh restart
重启服务
Zookeeper客户端常用命令
连接服务端
# 切换到启动目录
[root@localhost ~]# cd /opt/zookeeper/apache-zookeeper-3.5.6-bin/bin
# 先启动服务端
[root@localhost bin]# ./zkServer.sh start
# 本机服务器连接服务端
[root@localhost bin]# ./zkCli.sh
# 远程服务器连接服务端
[root@localhost bin]# ./zkCli.sh -server localhost:2181
本机服务器连接
连接结果与远程连接相同
远程服务器连接
退出连接
# 退出连接
[zk: localhost:2181(CONNECTED) 0] quit
查看命令帮助
[zk: localhost:2181(CONNECTED) 24]# help
zk节点操作(持久化的)
根据数据结构进行节点操作
注意 : 所有的命令前都要有 “/”
查看节点
# 查看根节点下的所有子节点信息
[zk: localhost:2181(CONNECTED) 0]# ls /
# 查看zookeeper节点下的所有子节点信息
[zk: localhost:2181(CONNECTED) 1]# ls /zookeeper
注意: 查看多级节点时, ls后必须跟全路径
# 查看config节点下的所有子节点信息
[zk: localhost:2181(CONNECTED) 2]# ls /zookeeper/config
创建节点
在根节点下创建一个app1节点,节点的值为hello
注意: 已有的节点不能重复创建
# 创建非空值节点app1
[zk: localhost:2181(CONNECTED) 3]# create /app1 hello
# 创建空值节点app2
[zk: localhost:2181(CONNECTED) 5]# create /app2
创建多级目录的子节点p1,p2
# 创建节点app1的子节点
[zk: localhost:2181(CONNECTED) 16]# create /app1/p1
[zk: localhost:2181(CONNECTED) 17]# create /app1/p2
获取节点数据
# 获取非空值节点,结果为 hello
[zk: localhost:2181(CONNECTED) 8]# get /app1
# 获取空值节点, 结果为 null
[zk: localhost:2181(CONNECTED) 9]# get /app2
4.设置节点数据
[zk: localhost:2181(CONNECTED) 10]# set /app2 world
5.删除节点
# 删除没有子节点的节点app1
[zk: localhost:2181(CONNECTED) 12]# delete /app1
# 删除多层目录的子节点
[zk: localhost:2181(CONNECTED) 19]# delete /app1/p1
注意: 要有全路径
# 删除多层目录的父节点(有子节点),并删除所有的子节点
[zk: localhost:2181(CONNECTED) 22]# deleteall /app1
zk节点操作(临时,顺序)
临时节点
随着会话/zk的客户端连接断开而删除
# 创建临时节点app1
[zk: localhost:2181(CONNECTED) 3]# create -e /app1
退出再连接查看app1是否存在
顺序节点
会自动在节点后加上序号,可以重复创建同一个名称节点。(默认是持久化的)
# 创建顺序节点app1
[zk: localhost:2181(CONNECTED) 1]# create -s /app1
[zk: localhost:2181(CONNECTED) 2]# create -s /app1
[zk: localhost:2181(CONNECTED) 3]# create -s /app1
[zk: localhost:2181(CONNECTED) 4]# create -s /app1
[zk: localhost:2181(CONNECTED) 5]# create -s /app1
临时顺序节点
# 创建临时顺序节点app3
[zk: localhost:2181(CONNECTED) 7]# create -es /app3
[zk: localhost:2181(CONNECTED) 8]# create -es /app3
[zk: localhost:2181(CONNECTED) 9]# create -es /app3
[zk: localhost:2181(CONNECTED) 10]# create -es /app3
注意: 所有节点共用同一个编号(持久化与非持久化都是按照顺序递增)
断开连接查看临时顺序节点是否存在
查看根节点的详细信息
[zk: localhost:2181(CONNECTED) 1]# ls -s /
ZookeeperAPI
Curator简介
Curator是ApacheZooKeeper 的Java客户端库。
Curator项目的目标是简化ZooKeeper客户端的使用
官网: http://curator.apache.org/
版本尽量高于zk
Curator API 常用操作
查看zk所在服务器的ip与端口
ip addr
建立连接
连接函数解释
/**
* newClient() // 创建连接对象
* Params:
* connectString – 连接字符串。zk server地址和端口 "192.168.100.51:2181,192.168.100.52:2181"
* (可以写集群环境,多个地址之间使用逗号隔开)
* sessionTimeoutMs – 会话超时时间 单位ms
* connectionTimeoutMs – 连接超时时间 单位ms
* retryPolicy – 重试策略
* Returns: client
*/
/**
* ExponentialBackoffRetry() // 指定间隔时间的重试次数
* Params:
* baseSleepTimeMs – 每次重试的间隔时间
* maxRetries – 最大重试次数
*/
连接代码测试
public class CuratorTest {
/**
* 建立连接
*/
@Test
public void testConnect(){
/*
// 方法一: 通过newClient创建连接
// 1. 创建重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 2. 创建连接对象
CuratorFramework client = CuratorFrameworkFactory.newClient(
"192.168.100.51:2181",60*1000,15*1000,retryPolicy);
// 3. 开启连接
client.start();
*/
// 方法二: 通过builder(),多了一个命名空间
// 1. 创建重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 2. 创建连接对象
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.100.51:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
// 名称空间 (创建节点时会默认认为名称空间是根目录,命名空间目录第一次没有时会自动创建)
.namespace("java").build();
// 3. 开启连接
client.start();
}
}
命名空间
建立与关闭连接切面函数
public class CuratorTest {
private CuratorFramework client;
/**
* 建立连接
*/
@Before
public void testConnect(){
// 方法二: 通过builder()
// 1. 创建重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 2. 创建连接对象
client = CuratorFrameworkFactory.builder()
.connectString("192.168.100.51:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
// 名称空间 (创建节点时会默认认为名称空间是根目录,命名空间目录第一次没有时会自动创建)
.namespace("java").build();
// 3. 开启连接
client.start();
}
// 节点操作函数
......
/**
* 关闭连接
*/
@After
public void close(){
if (client != null){
client.close();
}
}
}
添加节点
添加节点的常见类型
/**
* 添加节点: 创建(持久,临时,顺序,数据)
* 1. 基本创建 create.forPath("")
* 2. 创建节点,带有数据 create.forPath("","".getBytes())
* 3. 设置节点的类型(默认为持久性) create.withMode().forPath("")
* 4. 创建多级节点 /app1/p1 create.creatingParentsIfNeeded().forPath("")
*/
基本创建演示
public class CuratorTest {
private CuratorFramework client;
@Before
public void testConnect(){
...
}
@Test
public void createNode1() throws Exception {
// 1. 基本创建
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
String path = client.create().forPath("/app1");
System.out.println("path = " + path);
}
@After
public void close(){
...
}
}
如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
带有数据演示
@Test
public void createNode2() throws Exception {
// 2. 创建节点,带有数据
// 数据需要转换为byte[]数组
String path = client.create().forPath("/app2","hello Zookeeper".getBytes());
System.out.println("path = " + path);
}
3. 设置节点的类型演示
@Test
public void createNode3() throws Exception {
// 3. 设置节点的类型
String path = client.create()
// PERSISTENT 持久的
// PERSISTENT_SEQUENTIAL 持久顺序的
// EPHEMERAL 临时的
// EPHEMERAL_SEQUENTIAL 临时顺序的
.withMode(CreateMode.EPHEMERAL)
.forPath("/app3");
System.out.println("path = " + path);
}
注意: 创建的为临时节点时,在关闭连接方法运行完就会删除,客户端方面是看不到的;
如上图: 我们使用的连接服务器的工具(如Termius)连接的是Zookeeper Client 客户端 ,而我们使用Curator连接的是Zookeeper Java API的客户端, 不是一个连接, 当 API连接断开后其创建的临时节点就会删除,我们就无法通过Client获取到API中的节点;
4. 创建多级节点演示
@Test
public void createNode4() throws Exception {
// 4. 创建多级节点
String path = client.create()
// 如果父节点不存在,则创建父节点
.creatingParentsIfNeeded()
.forPath("/app4/p1");
System.out.println("path = " + path);
}
查询节点
查询节点常见类型
/**
* 1. 查询节点对应数据 get : getData().forPath("")
* 2. 查询子节点 ls : getChildren().forPath("")
* 3. 查询节点的状态信息ls -s : getData().storingStatIn(Stat对象).forPath("")
*/
查询节点对应数据
注意: 查询的根目录是连接时的命名空间(如/java), 使用Curator时无需书写前缀, 使用Client时需要使用
@Test
public void getNode1() throws Exception {
// 1. 查询节点对应数据
byte[] data = client.getData().forPath("/app2"); // 相当于/java/app2
System.out.println("data = " + new String(data)); // 结果data = hello Zookeeper
}
查询子节点
@Test
public void getNode2() throws Exception {
// 2. 查询子节点
List<String> nodeList = client.getChildren().forPath("/");// 相当于/java
System.out.println("nodeList = {");
for (String node : nodeList) {
System.out.println("node = " + node);
}
System.out.println("}");
}
查询节点的状态信息
@Test
public void getNode3() throws Exception {
// 3. 查询节点的状态信息
// 1. 创建Stat对象
Stat status = new Stat();
// 2. 将查询到的状态信息放入Stat对象中
client.getData().storingStatIn(status).forPath("/app2");// java app2
System.out.println("status = " + status);
}
修改节点
修改节点常见类型
/**
* 修改数据
* 1. 修改数据 setData().forPath("")
* 2. 根据版本修改 setData().withVersion().forPath("")
*/
修改数据
@Test
public void setNode1() throws Exception {
// 1. 修改数据
client.setData().forPath("/app1","world".getBytes());
}
2. 根据版本修改
产生原因: 多个客户端连接同一个服务端,同时修改相同节点数据时,造成获取到的数据与修改的不一致
@Test
public void setNode2() throws Exception {
// 2. 根据版本修改
// 1. 获取节点状态信息
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
System.out.println("status = " + status);
// 2. 获取版本信息
int version = status.getVersion(); // 查询出来的
System.out.println("version = " + version);
// 3. 设置修改的版本()
client.setData().withVersion(version).forPath("/app1","hehe".getBytes());
}
修改完版本号默认加一
删除节点
删除常见的类型
/**
* 删除节点 : delete deleteall
* 1. 删除单个节点 : delete().forPath("")
* 2. 删除带有子节点的节点 :delete().deletingChildrenIfNeeded().forPath("")
* 3. 必须成功的删除(防止网络抖动,本质就是重试) : delete().guaranteed().forPath("")
* 4. 回调 : inBackground()
*/
1. 删除单个节点
@Test
public void delNode1() throws Exception {
// 1. 删除单个节点
client.delete().forPath("/app1");
}
2. 删除带有子节点的节点
@Test
public void delNode2() throws Exception {
// 2. 删除带有子节点的节点
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
3. 必须成功的删除
注意: 一般都需要加guaranteed()方法防止服务终端,数据不一致。
删除指令未发送到服务端导致数据在服务端未删除。
@Test
public void delNode3() throws Exception {
// 3. 必须成功的删除
client.delete().guaranteed().forPath("/app2");
}
4. 回调
@Test
public void delNode4() throws Exception {
// 4. 回调
client.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
System.out.println("app3节点被删除了~~~");
System.out.println(event);
}
}).forPath("/app3");
}
resultCode为0表示删除成功
watch事件监听
ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper实现分布式协调服务的重要特性。
Zookeeper 中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
ZooKeeper 原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。
Curator引入了 Cache 来实现对 ZooKeeper 服务端事件的监听。
ZooKeeper提供了三种Watcher:
NodeCache:只是监听某一个特定的节点。
PathChildrenCache:监控一个ZNode的子节点。
TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合。
测试连接服务端代码
public class CuratorWatchTest {
private CuratorFramework client;
/**
* 建立连接
*/
@Before
public void testConnect(){
// 方法二: 通过builder()
// 1. 创建重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 2. 创建连接对象
client = CuratorFrameworkFactory.builder()
.connectString("192.168.100.51:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
// 名称空间 (创建节点时会默认认为名称空间是根目录,命名空间目录第一次没有时会自动创建)
.namespace("java").build();
// 3. 开启连接
client.start();
}
/**
* 关闭连接
*/
@After
public void close(){
if (client != null){
client.close();
}
}
//==========================NodeCache演示================================
//==========================PathChildrenCache演示================================
//==========================TreeCache演示================================
}
NodeCache
/**
* 演示 NodeCache: 给指定的一个节点注册监听器
*/
@Test
public void testNodeCache() throws Exception {
// 1. 创建NodeCache对象(需要传入连接对象与节点路径)
// 加final使变量在匿名内部类中可以使用
final NodeCache nodeCache = new NodeCache(client,"/app3");
// 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){
// 测试保证连接没有终断
}
}
运行上面程序监听当前节点, 在客户端改变当前节点(增删改都会触发)。
注意: 当前节点不存在也可以先设置, 新增时会直接监听到
PathChildrenCache
只监听当节点的子节点变化情况,当前节点的变化不会监听
/**
* 演示 PathChildrenCache: 监听某个节点的所有子节点
*/
@Test
public void testPathChildrenCache() throws Exception {
// 1. PathChildrenCache(需要传入连接对象与节点路径,是否缓存数据)
final PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"/app2",true);
// 2. 注册监听
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
System.out.println("节点变化了~~~");
System.out.println(event);
}
});
// 3. 开启监听
pathChildrenCache.start();
while(true){
// 测试保证连接没有终断
}
}
根据修改类型输出变化结果
/**
* 演示 PathChildrenCache: 监听某个节点的所有子节点
*/
@Test
public void testPathChildrenCache() throws Exception {
// 1. PathChildrenCache(需要传入连接对象与节点路径,是否缓存数据)
final PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"/app2",true);
// 2. 注册监听
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
System.out.println("节点变化了~~~");
// 1. 获取变化类型
PathChildrenCacheEvent.Type type = event.getType();
// 2. 判断变化的类型
if(type.equals(PathChildrenCacheEvent.Type.CHILD_ADDED)){
System.out.println("新增子节点");
// 3. 获取数据
byte[] data = event.getData().getData();
System.out.println("新增的节点数据为: "+new String(data));
}
if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
System.out.println("修改子节点");
// 3. 获取数据
byte[] data = event.getData().getData();
System.out.println("修改后的节点数据为: "+new String(data));
}
if(type.equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)){
System.out.println("删除子节点");
// 3. 获取数据
byte[] data = event.getData().getData();
System.out.println("删除的节点数据为: "+new String(data));
}
}
});
// 3. 开启监听
pathChildrenCache.start();
while(true){
// 测试保证连接没有终断
}
}
TreeCache
/**
* 演示 TreeCache: 监听某个节点以及该节点的所有子节点
*/
@Test
public void testTreeCache() throws Exception {
// 1. 创建TreeCache对象(需要传入连接对象与节点路径)
TreeCache treeCache = new TreeCache(client,"/app3");
// 2. 注册监听
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
System.out.println("节点变化了~~~");
System.out.println("event = " + event);
}
});
// 3. 开启监听
treeCache.start();
while(true){
// 测试保证连接没有终断
}
}
分布式锁实现
演变
在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized(隐式锁)或者Lock(显式锁)的方式来解决多线程间的代码同步问题这时多线程的运行都是在同一个JVM之下,没有任何问题。
但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。
那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题一一这就是分布式锁。
ZK实现分布式锁的原理
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点
客户端获取锁时,在lock节点下创建临时顺序节点。
临时:防止获取到锁的客户端宕机,锁无法释放
然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到锁。使用完锁后,)将该节点删除。
如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
在Curator中有五种锁方案
InterProcessSemaphoreMutex : 分布式排它锁(非可重入锁)
InterProcessMutex : 分布式可重入排它锁
InterProcessReadWriteLock : 分布式读写锁
InterProcessMultiLock : 将多个锁作为单个实体管理的容器
InterProcessSemaphoreV2 : 共享信号量
模拟12306售票
我们可以通过第三方软件(如:携程,飞猪,去哪儿)访问12306服务端进行购票,但是12306如果是集群环境就会出现重复买票的情况,我们可以在12306集群添加zk分布式锁解决
简单模拟
创建车票对象线程
public class Ticket12306 implements Runnable{
// 1. 模拟数据库票数
private int tickets = 10;
// 2. 创建分布式锁对象
private InterProcessMutex lock;
// 3. 创建对象时, 初始化lock对应的节点
public Ticket12306(){
// 1. 创建重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
// 2. 创建连接对象
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.100.51:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy).build();
// 3. 开启连接
client.start();
// 4. 指定zk的锁节点 Param : CuratorFramework zk连接对象, String 节点路径
lock = new InterProcessMutex(client,"/lock");
}
// 4. 线程执行口扣票操作
@Override
public void run() {
while(true){
try {
// 4.1 获取锁 Param: long 重新访问等待时间, TimeUnit 单位
lock.acquire(3, TimeUnit.SECONDS);
// 4.2 判断扣减
if (tickets > 0){
System.out.println(Thread.currentThread()+"购买到:"+tickets--+"号");
}
if (tickets <= 0){
break;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
// 4.3 释放锁,在finally防止异常锁未释放
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试
public class LockTest {
public static void main(String[] args) {
// 创建服务端对象
Ticket12306 thread = new Ticket12306();
// 为服务端线程添加客户端
Thread thread1 = new Thread(thread,"携程");
Thread thread2 = new Thread(thread,"飞猪");
Thread thread3 = new Thread(thread,"去哪");
// 启动所有的客户端
thread1.start();
thread2.start();
thread3.start();
}
}
Zookeeper集群搭建
Leader选举
zk Server集群leader选举
Serverid: 服务器ID
比如有三台服务器,编号分别是1,2,3;编号越大在选择算法中的权重越大。
Zxid: 数据ID
服务器中存放的最大数据ID。值越大说明数据 越新在选举算法中数据越新权重越大。
在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了。
Zookeeper集群角色
在ZooKeeper集群服中务中有三个角色
Leader 领导者:
1.处理事务请求
2.集群内部各服务器的调度者
Follower 跟随者:
1.处理客户端非事务请求,转发事务请求给Leader服务器
2.参与Leader选举投票
Observer 观察者:
1.处理客户端非事务请求,转发事务请求给Leader服务器