该文档对应代码地址
Zookeeper概念
ZooKeeper的安装与配置
- 下载地址 https://archive.apache.org/dist/zookeeper/
- 安装JDK
- 修改配置文件
- 在Zookeeper安装目录下新建zkData文件夹
- 将/opt/ZooKeeper/apache-zookeeper-3.5.6-bin/conf这个路径下的zoo_sample.cfg修改为zoo.cfg
- 打开zoo.cfg文件,修改dataDir路径
- 启动Zookeeper
cd /opt/ZooKeeper/apache-zookeeper-3.5.6-bin/bin
#启动
./zkServer.sh start
- 查看ZooKeeper状态
zookeeper启动成功。standalone代表zk没有搭建集群,现在是单节点
- 启动Zookeeper客户端:./zkCli.sh
- 退出Zookeeper客户端:qiit
- 退出Zookeeper服务端:/zkServer…sh stop
Zookeeper-配置参数解读
-
tickTime=2000: 通信心跳数,Zookeeper服务与客户端心跳时间,单位号毫秒
- Zookeepert使用的基本时间,服务器之间或客户端与服务之间维持心跳的时间间隔,也就是,每个tickTimel时间就会发送一个心跳,时间单位为毫秒。
- 它用于心跳机制,并且设置最小的sessioni超时时间为两倍心跳时间(sessionl的最小超时时间是2*tickTime)。
-
initlimit=10: LF初始通信时限
- 集群中的follower跟随者服务器(F)与leader领导者服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的ZookeeperF服务器连接到
Leaderf的时限 - 投票选举新leader的初始化时间。
- Follower在启动过程中,会从Leaderl同步所有最新数据,然后确定自己能够对外服务的起始状态。
- Leader允许F在initLimit时间内完成这个工作。
- 集群中的follower跟随者服务器(F)与leader领导者服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的ZookeeperF服务器连接到
-
synclimit=5: LF同步通信时限
- 集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit*tickTime,
- Leaderi认为Follwer死悼,从服务器列表中删除Follwer。.
- 在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。
- 如果L发出心跳包在syncLimit之后,还没有从F那收到响应,那么就认为这个F已经不在线了。
-
dataDir: 数据文件目录+数据持久化路径
- 保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。
-
clientPort=2181:客户端连接端口
- 监听客户端连接的端口。
ZooKeeper命令操作
Zookeeper数据模型
- ZooKeeper是一个树形目录服务,其数据模型和Linux的文件系统目录树很类似,拥有一个层次化结构。
- 这里面的每一个节点都被称为:ZNode,每个节点上都会保存自己的数据和节点信息。
- 节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。
- 节点可以分为四大类:
- PERSISTENT 持久化节点
- EPHEMERAL 临时节点:-e
- PERSISTENT_SEQUENTIAL 持久化顺序节点:-s
- EPHEMERAL_SEQUENTIAL 临时顺序节点:-es
Zookeeper服务端常用命令
- 启动ZooKeeper服务:./zkServer.sh start
- 查看ZooKeeper服务状态:./zkServer.sh status
- 停止ZooKeeper服务:/zkServer.sh stop
- 重启ZooKeeper服务:./zkServer.sh restart
Zookeeper客户端常用命令
- 连接zookeeper服务端
./zkCli.sh -server localhost:2181
解决日志频繁打印问题(没有问题则不用修改)
1)打开 conf/log4j.properties 并做如下修改
2)修改 bin/zkEvn.sh 文件
修改完成后,执行结果为:
2. 查看节点命令ls /
#启动zookeeper服务
[root@localhost bin]# ./zkServer.sh start
/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/ZooKeeper/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
#通过zookeeper服务去连接服务端
[root@localhost bin]# ./zkCli.sh
/bin/java
Connecting to localhost:2181
Welcome to ZooKeeper!
JLine support is enabled
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
#查看信息
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
create [-s] [-e] path data |-s 有序节点 | -e 临时节点
#创建临时有序节点并写入数据
[zk: localhost:2181(CONNECTED) 1] create -es /java helloJava
Created /java0000000000
[zk: localhost:2181(CONNECTED) 2] create -es /java helloJava2
Created /java0000000001
[zk: localhost:2181(CONNECTED) 3] create -es /java helloJava2
Created /java0000000002
[zk: localhost:2181(CONNECTED) 4] create -es /java helloJava2
Created /java0000000003
[zk: localhost:2181(CONNECTED) 5] ls /
[java0000000000, java0000000001, java0000000002, java0000000003, zookeeper]
#创建持久化有序数据
[zk: localhost:2181(CONNECTED) 8] create -s /zook hellozook
Created /zook0000000004
[zk: localhost:2181(CONNECTED) 9] create -s /zook hellozook
Created /zook0000000005
[zk: localhost:2181(CONNECTED) 10] create -s /zook hellozook
Created /zook0000000006
[zk: localhost:2181(CONNECTED) 11] ls /
[java0000000000, java0000000001, java0000000002, java0000000003, zook0000000004, zook0000000005, zook0000000006, zookeeper]
set path data [版本号]
# 更新节点
[zk: localhost:2181(CONNECTED) 12] get /java0000000002
helloJava2
[zk: localhost:2181(CONNECTED) 13] set /java0000000002 updatedata
[zk: localhost:2181(CONNECTED) 14] get /java0000000002
updatedata
delete path [版本号] # 节点下面有子节点的话就无法删除 § deleteall path # 连同节点下的子节点一并删除
#delete删除
[zk: localhost:2181(CONNECTED) 15] ls /
[java0000000000, java0000000001, java0000000002, java0000000003, zook0000000004, zook0000000005, zook0000000006, zookeeper]
[zk: localhost:2181(CONNECTED) 16] delete /java0000000003
[zk: localhost:2181(CONNECTED) 20] delete /java0000000001
[zk: localhost:2181(CONNECTED) 21] delete /zook0000000004
[zk: localhost:2181(CONNECTED) 22] delete /zook0000000005
[zk: localhost:2181(CONNECTED) 23] ls /
[java0000000000, java0000000002, zook0000000006, zookeeper]
# 不能删除有子节点的节点
[zk: localhost:2181(CONNECTED) 0] ls /
[java, java1, zook0000000006, zookeeper]
# 注意临时节点不能创建子节点
[zk: localhost:2181(CONNECTED) 1] create /java1/child2
Created /java1/child2
[zk: localhost:2181(CONNECTED) 2] ls /java1
[child1, child2]
[zk: localhost:2181(CONNECTED) 3] delete /java1
Node not empty: /java1
#使用deleteall删除
[zk: localhost:2181(CONNECTED) 6] deleteall /java1
[zk: localhost:2181(CONNECTED) 7] ls /
[zook0000000006, zookeeper]
- 查询节点
ZooKeeper监听器
- ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。
- ZooKeeper中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
- ZooKeeper原生支持通过注册Vatcher来进行事件监听,但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。
Zookeeper - Acl权限控制
概述
-
zookeeper类似文件系统,client可以创建节点、删除节点,那么如何做到节点的权限控制呢?
-
zookeeper的access control list访问控制列表可以做到这一点。
-
acl权限控制使用:scheme🆔permission来标识,主要涵盖三个方面:
- 权限模式(scheme):授权的策略
- 授权对象(id):授权的对象
- 权限(permission):授予的权限
-
其特性如下:
- zookeeper的权限控制是基于每个zonde节点的,需要对每个节点设置权限。
- 每个znode支持设置多种权限控制发难和多个权限。
- 字节点不会继承父节点的权限,客户端无权访问某个节点,但可能可以访问它的子节点。
授权模式
采用何种方式授权:
授予的权限:
关于授予的命令:
Curator JavaAPI操作
建立连接
- 搭建maven工程并导入坐标
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<!-- curator-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.20</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
- 导入日志配置文件
log4j.rootLogger=error,CONSOLE
log4j.addivity.org.apache=true
## \u5E94\u7528\u4E8E\u63A7\u5236\u53F0
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=WARN
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[OrganizationUI] [%d{yyyy-MM-dd HH:mm:ss}] %-5p => %c.%M(%F:%L) - %m%n
- 编写测试方法
/**
* 方式一 new Client()
* 建立连接
*/
@Test
public void testConnect01() {
/**
* @Param connectstring 连接字符串。zk server地址和端口 "192.168.149.135:2181,192.168.149.136:2181
* @param sessionTimeoutMs 会话超时时间,单位毫秒
* @param connectionTimeoutMs 连接超时时间,单位毫秒
* @param retryPolicy 重试策略
* 重试策略是一个接口,有多个实现类
* 本案例以最简单的为示范
*/
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.150.132:2181",
60 * 1000, 15 * 1000, retryPolicy);
//开启连接
client.start();
}
/**
* 方式二 builder()
* 建立连接
*/
@Test
public void testConnect02() {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.150.132:2181")
.connectionTimeoutMs(15 * 1000)
.sessionTimeoutMs(60 * 1000)
.retryPolicy(retryPolicy)
.namespace("why")
.build();
//开启连接
client.start();
}
添加节点
tps:
junit提供了@before和@after注解,分别表示在@Test注解执行之前和之后执行完对应注解的内容
所以为了方便,创建连接在之前执行,新建一个关闭客户端方法设置为之后执行
修改结构将客户端端放入全局变量
public class CuratorTest {
private CuratorFramework client;
/**
* 方式二 builder()
* 建立连接
*/
@Before
public void testConnect02() {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(3000, 10);
client = CuratorFrameworkFactory.builder().connectString("192.168.150.132:2181")
.connectionTimeoutMs(15 * 1000)
.sessionTimeoutMs(60 * 1000)
.retryPolicy(retryPolicy)
.namespace("why")
.build();
//开启连接
client.start();
}
@After
public void closeCurator() {
if (client != null) {
client.close();
}
}
}
代码
/**
* 创建节点:create 持久 临时 顺序 数据
* 1. 基本创建 :create().forPath("")
* 2. 创建节点 带有数据:create().forPath("",data)
* 3. 设置节点的类型:create().withMode().forPath("",data)
* 4. 创建多级节点 /app1/p1 :create().creatingParentsIfNeeded().forPath("",data)
*/
@Test
public void testCreateNode() throws Exception {
//1. 基本创建
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
String path = client.create().forPath("/javaCreate1");
System.out.println(path);
//2. 创建节点 带有数据
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
String path1 = client.create().forPath("/javaCreate2", ("hello Java").getBytes());
System.out.println(path1);
//3. 设置节点的类型
//默认类型:持久化
/**
* PERSISTENT(0, false, false),
* PERSISTENT_SEQUENTIAL(2, false, true),
* EPHEMERAL(1, true, false),
* EPHEMERAL_SEQUENTIAL(3, true, true);
*/
String path2 = client.create().withMode(CreateMode.EPHEMERAL).forPath("/javaCreate3", "temp Data".getBytes());
System.out.println(path2);
//4. 创建多级节点 /app1/p1
//creatingParentsIfNeeded():如果父节点不存在,则创建父节点
String path3 = client.create().creatingParentsIfNeeded().forPath("/javaCreate4/test");
System.out.println(path3);
//防止客户端关闭造成临时节点消失
while (true) {
;
}
}
Linux系统下进行检查
#创建节点后Linux已生成
[zk: localhost:2181(CONNECTED) 24] ls /
[why, zookeeper]
#查看所有
[zk: localhost:2181(CONNECTED) 25] ls /why
[javaCreate1, javaCreate2, javaCreate3, javaCreate4]
#默认持久化,未指定数据存储当前的ip
[zk: localhost:2181(CONNECTED) 28] get /why/javaCreate1
192.168.137.1
#指定了存储数据
[zk: localhost:2181(CONNECTED) 29] get /why/javaCreate2
hello Java
#创建的临时节点
[zk: localhost:2181(CONNECTED) 30] get /why/javaCreate3
temp Data
#创建的多级节点
[zk: localhost:2181(CONNECTED) 31] ls /why/javaCreate4
[test]
[zk: localhost:2181(CONNECTED) 32] get /why/javaCreate4/test
192.168.137.1
查询节点
/**
* 查询节点:
* 1. 查询数据:get: getData().forPath()
* 2. 查询子节点: ls: getChildren().forPath()
* 3. 查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath()
*/
代码
@Test
public void testGet() throws Exception {
//1. 查询数据:get
byte[] bytes1 = client.getData().forPath("/javaCreate1");
System.out.println(new String(bytes1));
// 2. 查询子节点: ls
List<String> paths = client.getChildren().forPath("/");
System.out.println(paths);
//3. 查询节点状态信息:ls -s
Stat stat = new Stat();
System.out.println(stat);
client.getData().storingStatIn(stat).forPath("/javaCreate1");
System.out.println(stat);
}
修改节点
/**
* 修改数据
* 1. 基本修改数据:setData().forPath()
* 2. 根据版本修改: setData().withVersion().forPath()
* * version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。
*
* @throws Exception
*/
@Test
public void testSet() throws Exception {
//更改javaCreate1中的信息
Stat path1 = client.setData().forPath("/javaCreate1", "updateValue".getBytes());
System.out.println(path1);
//查询版本,根据版本修改
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath("/javaCreate1");
int version = stat.getVersion();
System.out.println(version);
client.setData().withVersion(version).forPath("/javaCreate1","withVersionUpdate".getBytes());
}
Linux系统下验证
[zk: localhost:2181(CONNECTED) 36] get /why/javaCreate1
updateValue
[zk: localhost:2181(CONNECTED) 36] get /why/javaCreate1
withVersionUpdate
删除节点
/**
* 删除节点: delete deleteall
* 1. 删除单个节点:delete().forPath("/app1");
* 2. 删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
* 3. 必须成功的删除:为了防止网络抖动。本质就是重试。 client.delete().guaranteed().forPath("/app2");
* 4. 回调:inBackground
* @throws Exception
*/
@Test
public void testDelete() throws Exception {
// 1. 删除单个节点
client.delete().forPath("/javaCreate1");
//2. 删除带有子节点的节点
client.delete().deletingChildrenIfNeeded().forPath("/javaCreate4");
//3. 必须成功的删除
client.delete().guaranteed().forPath("/javaCreate2");
//4. 回调
client.delete().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println("我被删除了~");
System.out.println(curatorEvent);
}
}).forPath("/javaCreate1");
}
在Linux系统下验证
[zk: localhost:2181(CONNECTED) 1] ls /why
[javaCreate1, javaCreate2, javaCreate4]
[zk: localhost:2181(CONNECTED) 2] ls /why
[]
[zk: localhost:2181(CONNECTED) 3] ls /
[why, zookeeper]
Watch事件监听
NodeCache
/**
* 1. NodeCache:只是监听某一个特定的节点
* 2. PathChildrenCache:监控一个ZNode的子节点
* 3. TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合
*/
/**
* 演示 NodeCache:给指定一个节点注册监听器
*/
@Test
public void testNodeCache() throws Exception {
//1. 创建NodeCache对象
NodeCache nodeCache = new NodeCache(client, "/javaNodeCache1");
//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) {
;
}
Linux系统修改节点的值
[zk: localhost:2181(CONNECTED) 7] get /why/javaNodeCache1
null
[zk: localhost:2181(CONNECTED) 8] set /why/javaNodeCache1 "nodeCache"
[zk: localhost:2181(CONNECTED) 9] get /why/javaNodeCache1
nodeCache
[zk: localhost:2181(CONNECTED) 10] set /why/javaNodeCache1 "updateNote"
控制台结果
PathChildrenCache
/**
* 演示 PathChildrenCache:监听某个节点的所有子节点们
*/
@Test
public void testPathChildrenCache() throws Exception {
//1. 创建NodeCache对象
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/javaNodeCache", true);
//2. 绑定监听器
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
System.out.println("子节点变化了~");
System.out.println(pathChildrenCacheEvent);
//获取变化的类型
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,则开启监听是,加载缓冲数据
pathChildrenCache.start(true);
//保证持续监听
while (true) {
;
}
}
在Linux系统下验证
[zk: localhost:2181(CONNECTED) 16] ls /why
[javaNodeCache, javaNodeCache1]
[zk: localhost:2181(CONNECTED) 17] create /why/javaNodeCache/newNode
Created /why/javaNodeCache/newNode
[zk: localhost:2181(CONNECTED) 18] set /why/javaNodeCache/newNode "new Value"
[zk: localhost:2181(CONNECTED) 19] set /why/javaNodeCache/newNode "update value"
控制台返回
TreeCache
/**
* 演示 TreeCache:监听某个节点自己和所有子节点们
*/
@Test
public void testTreeCache() throws Exception {
//1. 创建监听器
TreeCache treeCache = new TreeCache(client,"/javaNodeCache2");
//2. 注册监听
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
System.out.println("节点变化了");
System.out.println(event);
}
});
//3. 开启
treeCache.start();
while (true){
}
}
在Linux系统下更改
[zk: localhost:2181(CONNECTED) 53] set /why/javaNodeCache2 aaa
[zk: localhost:2181(CONNECTED) 54] set /why/javaNodeCache2/p1 ttt
分布式锁
Zookeeper实现分布式锁的原理
- 核心思想
当客户端需要获取锁的时候,创建结点,使用完锁则删除节点
- 客户端获取锁时,在lock节点下创建临时顺序节点。
- 然后获取lock节点里的所有节点,然后拿自己的节点和lock里面的所有节点对比,如果自己的节点的序号最小,则证明拿到了锁,使用完锁后删除自己的lock子节点。
- 如果发现自己创建的节点并非lock里最小的,说明自己并没有第一个获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
- 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应的通知,此时再次判断自己创建的节点是不是lock里面最小的,如果是则证明获取到了锁,反之重复以上步骤继续监听比自己小的节点,直到获取锁。
案例-Curator实现分布式锁API
Curator中有五种锁方案:
- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
- InterProcessMutex:分布式可便入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
Ticket12306.java——业务模拟
package com.why;
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 Ticket12306 implements Runnable{
private int tickets = 10;//数据库的票数
private InterProcessMutex lock ;
public Ticket12306(){
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
//2.第二种方式
//CuratorFrameworkFactory.builder();
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.150.132: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(3, TimeUnit.SECONDS);
if(tickets > 0){
System.out.println(Thread.currentThread()+":"+tickets);
Thread.sleep(100);
tickets--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试运行
package com.why;
public class testLock {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
Thread thread = new Thread(ticket12306, "携程");
Thread thread1 = new Thread(ticket12306, "飞猪");
thread.start();
thread1.start();
}
}
运行结果
Linux中的节点