Zookeeper
文章目录
1 Zookeeper概述
1.1 工作机制
- 概念:zookeeper是一种面向分布式应用程序的分布式开源协调服务。
- zookeeper是文件系统和通知机制的融合
1.2 主要特点
- 顺序一致性:来自客户端的请求将按照发送顺序依次执行
- 原子性:数据的更新要么成功,要么失败
- 单个系统映像:客户端看到相同的服务视图
- 可靠性:应用更新后,它将从该时间持续存在,直到数据被更新
- 及时性:保证系统的客户端视图在一定时间范围内是最新的
1.3 数据结构
- 分层次的命名空间,与Unix的文件系统类似,整体可以看作是一个文件树,但与标准文件系统不同,zookeeper命名空间中的每个节点都可以具有与其关联的数据以及子节点。节点用znode表示,每个znode默认能够存储1MB的数据,并且每个znode可以通过其路径唯一标识。节点的路径都是绝对路径,没有相对路径。
注:临时节点不能创建子节点
1.4 实现
-
zookeeper服务的高级组件,除请求处理器外,构成zookeeper的每个服务器都会复制每个组件的副本
1.5 主要应用
- 统一命名服务
- 统一配置管理
- 统一集群管理
- 服务器节点动态上下线
- 软负载均衡
- 分布式锁
2 zookeeper安装
2.1 单机安装
-
下载
-
安装脚本
#从官网下载对应版本的压缩包,解压文件到指定安装路径
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz -C /opt/softwares/
#更改安装目录名称
cd /opt/softwares/
mv apache-zookeeper-3.7.1-bin zookeeper-3.7.1
#修改配置文件名称
cd /opt/softwares/zookeeper-3.7.1/conf
mv zoo_sample.cfg zoo.cfg
#创建zookeeper数据存放目录
mkdir /opt/softwares/zookeeper-3.7.1/zkData
#修改内容
vim zoo.cfg
dataDir=/opt/softwares/zookeeper-3.7.1/zkData
- 服务启动与客户端连接
cd /opt/softwares/zookeeper-3.7.1/
#启动zookeeper
bin/zkServer.sh start
#查看状态
bin/zkServer.sh status
#停止服务
bin/zkServer.sh stop
#启动客户端
bin/zkClid.sh
#退出客户端
quit
- zoo.cfg配置文件内容
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
# LF初始通信时限
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
# LF同步通信时限
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/opt/softwares/zookeeper-3.7.1/zkData
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
2.2 集群配置
- 在zkData目录下新建文件myid
cd /opt/softwares/zookeeper-3.7.1/zkData
#新建文件并输入服务器编号
vim myid
- 在配置文件中增加如下的内容
##########################Cluster基本格式############################
server.1=host1:port1:port2
server.2=host2:port1:port2
server.3=host3:port1:port2
##########################伪集群配置Cluster##########################
server.1=localhost:2888:3888
server.2=localhost:2889:3889
server.3=localhost:2890:3890
###########################纯集群配置Cluster##########################
server.1=myvm001:2888:3888
server.2=myvm001:2888:3888
server.3=myvm001:2888:3888
- 增加内容解读
server.A=B:C:D
A表示服务器的标识,是一个唯一的数字通过在dataDir目录下新建文件myid指定
B表示服务器的地址(可以是ip地址,设置了主机名映射后也可以是主机名)
C表示集群中Follower与Leader进行信息交换的端口号
D表示集群选举时各个服务器之间进行通信的端口号
- 集群启停脚本
#!/bin/bash
case $1 in
"start"){
for i in myvm001 myvm002 myvm003
do
echo ===============zookeeper $i 启动======================
ssh $i "/opt/softwares/zookeeper-3.7.1/bin/zkServer.sh start"
done
};;
"stop"){
for i in myvm001 myvm002 myvm003
do
echo ===============zookeeper $i 停止=======================
ssh $i "/opt/softwares/zookeeper-3.7.1/bin/zkServer.sh stop"
done
};;
"status"){
for i in myvm001 myvm002 myvm003
do
echo ===============zookeeper $i 状态=======================
ssh $i "/opt/softwares/zookeeper-3.7.1/bin/zkServer.sh status"
done
};;
esac
3 客户端常用命令与节点介绍
3.1 常用命令
常用命令 | 命令作用 |
---|---|
help | 显示所有的操作命令 |
ls 【-w -s】 path | 查看当前path节点的子节点 -w 监听子节点的变化 -s 显示当前节点的统计结构信息 |
create 【-e -s】 path | 创建节点 -e 创建临时节点 -s创建带有序列的节点 |
get 【-w -s】path | 获取节点的值 -w 监听节点内容变化 -s 显示节点的统计结构信息 |
set path data | 设置节点的值 |
stat path | 显示节点的统计结构的信息 |
delete path | 删除节点 |
deleteall | 递归删除节点 |
3.2 节点分类与节点内容
-
节点分类
- 持久化节点(PERSISTENT)
- 持久化顺序编号节点(PERSISTENT_SEQUENTIAL)
- 临时目录节点(EPHEMERAL)
- 临时顺序编号目录节点(EPHEMERAL_SEQUENTIAL)
- 容器节点(新增)
- TTL节点(新增)
-
节点维护的stat structure
Zxid:每次对zookeeper状态的修改都会产生一个ZooKeeper Transaction Id
- cZxid:创建节点时的Zxid
- mZxid:最后一次修改节点时的Zxid
- pZxid:最后一次修改节点的子节点的Zxid
- ctime:节点创建时的时间
- mtime:节点最后依次被修改的时间
- cversion:改变节点的子节点的次数
- dataVersion:改变节点的数据的次数
- aclVersion:改变节点的ACL(Access Control List)的次数
- ephemeralOwner:节点所有者的会话ID(如果当前节点时临时节点),不是临时节点该值为0
- dataLength:节点的数据字段的长度
- numChildren:节点的儿子的个数
3.3 监听节点
-
get -w path
-
监听节点的修改 NodeDataChanged
-
监听节点的删除 NodeDeleted
注:无法监听节点的创建
-
-
ls -w path
-
监听子节点的增加 NodeChildrenChanged
-
监听子节点的修改
-
监听本节点的删除
注:无法监听本节点的修改,同时无法监听子节点的修改
-
3.4 权限控制(Access Control List)
- ACL的组成
- 权限模式
- 权限对象
- 权限信息
- 权限模式
- 范围验证
- 口令验证
- 权限信息
- 创建权限
- 更新权限
- 读取权限
- 删除权限
- 管理者权限
4 JAVA客户端连接zookeeper
-
引入依赖
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.5.7</version> </dependency>
-
配置客户端连接信息
zookeeper: address: myvm001:2181,myvm002:2181,myvm003:2181 timeout: 5000
package com.lzx.zookeeper.config; import lombok.extern.slf4j.Slf4j; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.CountDownLatch; @Configuration @Slf4j public class ZooConfig { //服务器地址 @Value("${zookeeper.address}") private String connectString; //连接超时时间 @Value("${zookeeper.timeout}") private int timeout; //客户端名称 @Bean(name = "zkClient") public ZooKeeper zkClient(){ ZooKeeper zooKeeper = null; try { CountDownLatch countDownLatch = new CountDownLatch(1); zooKeeper = new ZooKeeper(connectString, timeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { if(Event.KeeperState.SyncConnected==watchedEvent.getState()){ countDownLatch.countDown(); } } }); countDownLatch.await(); log.info("zookeeper初始化连接状态:{}",zooKeeper.getState()); } catch (Exception e) { log.error("zookeeper初始化连接异常:{}",e); } return zooKeeper; } }
-
测试相关API
-
监听器实现
package com.lzx.zookeeper.watcher; import lombok.extern.slf4j.Slf4j; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; @Slf4j public class WatcherImp implements Watcher { @Override public void process(WatchedEvent event) { log.info("【Watcher监听事件】={}",event.getState()); log.info("【监听路径为】={}",event.getPath()); // 三种监听类型: 创建,删除,更新 log.info("【监听的类型为】={}",event.getType()); } }
-
相关API实现
package com.lzx.zookeeper.utils; import com.lzx.zookeeper.watcher.WatcherImp; import lombok.extern.slf4j.Slf4j; import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.List; @Slf4j @Component public class ZooUtil { @Autowired private ZooKeeper zkClient; /** * 判断指定节点是否存在 * @param path * @param needWatch 指定是否复用zookeeper中默认的Watcher * @return */ public Stat exists(String path, boolean needWatch){ try { return zkClient.exists(path,needWatch); } catch (Exception e) { log.error("【断指定节点是否存在异常】{},{}",path,e); return null; } } /** * 检测结点是否存在 并设置监听事件 * 三种监听类型: 创建,删除,更新 * * @param path * @param watcher 传入指定的监听类 * @return */ public Stat exists(String path,Watcher watcher ){ try { return zkClient.exists(path,watcher); } catch (Exception e) { log.error("【断指定节点是否存在异常】{},{}",path,e); return null; } } /** * 创建持久化节点 * @param path * @param data */ public boolean createNode(String path, String data){ try { zkClient.create(path,data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); return true; } catch (Exception e) { log.error("【创建持久化节点异常】{},{},{}",path,data,e); return false; } } /** * 修改持久化节点 * @param path * @param data */ public boolean updateNode(String path, String data){ try { //zk的数据版本是从0开始计数的。 //如果客户端传入的是-1,则表示zk服务器需要基于最新的数据进行更新。 //如果对zk的数据节点的更新操作没有原子性要求则可以使用-1. //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查 zkClient.setData(path,data.getBytes(),-1); return true; } catch (Exception e) { log.error("【修改持久化节点异常】{},{},{}",path,data,e); return false; } } /** * 删除持久化节点 * @param path */ public boolean deleteNode(String path){ try { //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查 zkClient.delete(path,-1); return true; } catch (Exception e) { log.error("【删除持久化节点异常】{},{}",path,e); return false; } } /** * 获取当前节点的子节点(不包含孙子节点) * @param path 父节点path */ public List<String> getChildren(String path) throws KeeperException, InterruptedException{ List<String> list = zkClient.getChildren(path, false); return list; } /** * 获取指定节点的值 * @param path * @return */ public String getData(String path, Watcher watcher){ try { Stat stat=new Stat(); byte[] bytes=zkClient.getData(path,watcher,stat); return new String(bytes); }catch (Exception e){ e.printStackTrace(); return null; } } /** * 测试方法 初始化 */ @PostConstruct public void init(){ log.info("【执行初始化测试方法。。。。。。。。。。。。】"); String path="/zk-watch-test"; Stat stat = exists(path, null); log.info("【判断节点是否存在,返回stat】={}",stat); String data = "测试监听"; createNode(path,data); log.info("【创建节点,初始值】={}",data); String value=getData(path,new WatcherImp()); log.info("【执行初始化测试方法getData返回值。。。。。。。。。。。。】={}",value); //更新节点,触发删除事件 updateNode(path,"测试更新"); value=getData(path,new WatcherImp()); log.info("【执行更改操作后节点的值】={}",value); // 删除节点,触发监听事件 deleteNode(path); } }
-
5 Zookeeper实现分布式锁
5.1独占式锁
- 通过单节点实现
5.2 顺序锁
- 通过临时顺序节点进行实现
5.2.1 zookeeper+java API实现
package com.lzx.zookeeper.dislock;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 分布式锁客户端
*/
@Slf4j
public class DisClient {
//当前连接创建的节点
private String curNodePath;
//当前节点的上一个节点
private String preNodePath;
//zookeeper客户端
private ZooKeeper zkClient = null;
//创建连接时的线程计数器
private CountDownLatch clientLatch=null;
//在当前节点等待的线程计数器
private CountDownLatch waitLatch =null;
//根节点
private String lockRoot = "/disLock";
//子节点的前缀
private final static String SEQ_LOCK_PREFIX="/lock-";
public DisClient(){
//初始化客户端连接
try {
clientLatch = new CountDownLatch(1);
zkClient = new ZooKeeper("myvm001:2181,myvm002:2181,myvm003:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
clientLatch.countDown();
}
}
});
clientLatch.await();
//利用同步代码块创建根节点
synchronized (DisClient.class){
if(zkClient.exists(lockRoot,false)==null){
zkClient.create(lockRoot,"the root of disLock".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
//上锁
public void lock() throws KeeperException, InterruptedException {
if(tryLock()){
log.info("线程:---{}---获得了锁",Thread.currentThread().getName());
}else {
//获取锁失败,等待上一个节点释放锁
waitForLock();
log.info("线程:---{}---在等待锁",Thread.currentThread().getName());
//重新获取锁
lock();
}
}
public boolean tryLock() throws KeeperException, InterruptedException {
//在根节点创建当前节点
if(curNodePath==null||"".equals(curNodePath)){
curNodePath = zkClient.create(lockRoot + SEQ_LOCK_PREFIX, new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
}
List<String> children = zkClient.getChildren(lockRoot, false);
if(children.size()==1){
return true;
}else{
Collections.sort(children);
int curIndex = children.indexOf( curNodePath.substring((lockRoot + "/").length()));
if(curIndex==-1){
log.error("数据异常");
}else if(curIndex==0){
return true;
}else{
preNodePath=lockRoot+"/"+children.get(curIndex-1);
}
}
return false;
}
public void waitForLock() throws KeeperException, InterruptedException {
waitLatch=new CountDownLatch(1);
zkClient.getData(preNodePath, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType()== Event.EventType.NodeDeleted&&
preNodePath.equals(watchedEvent.getPath())){
waitLatch.countDown();
}
}
},new Stat());
waitLatch.await();
}
public void unLock() throws KeeperException, InterruptedException {
zkClient.delete(curNodePath,-1);
log.info("线程:---{}---释放了锁",Thread.currentThread().getName());
}
}
package com.lzx.zookeeper.dislock;
import org.apache.zookeeper.KeeperException;
public class DisLockTest {
public static void main(String[] args) {
for(int i =0;i<10;i++){
new Thread(new GetLockRunnable()).start();
}
}
static class GetLockRunnable implements Runnable{
@Override
public void run() {
DisClient disClient = new DisClient();
try {
disClient.lock();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
disClient.unLock();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.2.2 Curator客户端实现
package com.lzx.zookeeper.dislock;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class CuratorDisLock {
//根节点
private static final String LOCK_ROOT_NODE = "/curatorDisLock";
//
private CuratorFramework curatorFramework;
//
private RetryPolicy retryPolicy;
private String connectString = "myvm001:2181,myvm002:2181,myvm003:2181";
private int connectTimeout = 2000;
public CuratorDisLock(){
//初始化客户端连接
retryPolicy = new ExponentialBackoffRetry(3000,3);
curatorFramework = CuratorFrameworkFactory.builder().connectString(connectString)
.connectionTimeoutMs(connectTimeout)
.retryPolicy(retryPolicy).build();
curatorFramework.start();
}
public static void main(String args[]){
for(int i =0;i<10;i++){
new Thread(()->{
new CuratorDisLock().lock();
}).start();
}
}
public void lock(){
InterProcessMutex lock = new InterProcessMutex(curatorFramework, LOCK_ROOT_NODE);
try {
if (lock.acquire(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName()+"获取锁成功");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if(lock.isAcquiredInThisProcess()){
lock.release();
System.out.println(Thread.currentThread().getName()+"释放锁");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}