Zookeeper
1、概念
大数据生态系统里很多组件的命名都是某种动物,例如Hadoop是🐘,hive是🐝,zookeeper就是动物园管理者,是管理大数据生态系统各组件的管理员
zookeepepr
是一个经典的分布式数据一致性解决方案
主要功能:
-
维护配置信息
由于许多服务都需要使用到该配置文件,因此有必须保证该配置服务的高可用性和各台服务器上配置数据的一致性。因此就需要一种服务
-
分布式锁服务
一个集群是一个分布式系统,由多台服务器组成
有时候需要保证当某个服务在进行某个操作时,其他的服务都不能进行该操作,即对该操作进行加锁,如果当前机器故障,释放锁并fall over到其他机器继续执行
-
集群管理
zookeeper会将服务器加入/移除的情况通知给集群中其他正常工作的服务器,以及即使调整存储和计算等任务的分配和执行等,此外zookeeper还会对故障的服务器做出诊断并尝试修复
-
生成分布式唯一ID
分库分表之后不能使用之前自动生成的标识 id,此时可以使用zookeeper在分布式环境下生成全局唯一性id
每次要生成一个新id时,创建一个持久顺序结点,创建操作返回的结点序号,即为新id,然后把比自己结点小的删除
2、安装与配置
2.1、下载安装
需要 jdk7 及以上版本
下载安装包 apache-zookeeper-3.5.6-bin.tar.gz
启动虚拟机,启动 SecureCRTPortable.exe
启动 SecureFXPortable.exe,拖入安装包进目录 /home/soft/
#将安装包解压到 /usr/local/ 目录下
tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz -C /usr/local/
2.2、配置启动
配置 zoo.cfg
进入 conf 目录拷贝一个 并完成配置
#进入到 conf 目录
cd /usr/local/apache-zookeeper-3.5.6-bin/conf/
#拷贝
cp zoo_sample.cfg zoo.cfg
修改 zoo.cfg
#打开目录
cd /usr/local/
#创建zookeeper存储目录
mkdir zkdata
#修改zoo.cfg
vi /usr/local/apache-zookeeper-3.5.6-bin/conf/zoo.cfg
修改存储目录:/usr/local/zkdata
启动ZooKeeper
#进入bin目录
cd /usr/local/apache-zookeeper-3.5.6-bin/bin/
#启动
./zkServer.sh start
看到图表示成功启动
#关闭
./zkServer.sh stop
查看 ZooKeeper 状态
./zkServer.sh status
ZooKeeper启动成功,standalone代表 zk 没有搭建集群,现在是单节点
如果显示 It is probably not running
,表示未启动
2.3、配置参数解读
Zookeeper中的配置文件zoo.cfg中参数含义解读如下:
-
tickTime = 2000
:通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒 -
initLimit = 10
:LF初始通信时限Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)
-
syncLimit = 5
:LF同步通信时限Leader和Follower之间通信时间如果超过syncLimit * tickTime,Leader认为Follwer死 掉,从服务器列表中删除Follwer
-
dataDir
:保存Zookeeper中的数据注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录
-
clientPort = 2181
:客户端连接端口,通常不做修改
3、命令操作
3.1、数据模型
ZooKeeper 是一个树形目录服务,其数据模型和 Unix 的文件系统目录树类似,拥有一个层次化结构
里面的每个节点都被称为 ZNode,每个节点上都会保存自己的数据和节点信息
节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下
节点分为四大类:
- persistent 持久化节点
- ephemeral 临时节点 -e
- persistent_sequential 持久化顺序节点 -s
- ephemeral_sequential 临时顺序节点 -es
3.2、服务端常用命令
#进入bin目录
cd /usr/local/apache-zookeeper-3.5.6-bin/bin/
#启动
./zkServer.sh start
#关闭
./zkServer.sh stop
#查看状态
./zkServer.sh status
#重启
./zkServer.sh restart
3.3、客户端常用命令
#进入bin目录
cd /usr/local/apache-zookeeper-3.5.6-bin/bin/
#启动客户端
./zkCli.sh -server localhost:2181
#退出客户端
quit
#显示所有操作命令
help
查看当前znode中所包含的内容: ls path
使用 ls 命令来查看当前 znode 的子节点 [可监听]
-w 监听子节点变化
-s 附加次级信息
#查看当前znode中所包含的内容 ls /
[zk: localhost:2181(CONNECTED) 0] ls /
输出:[zookeeper]
普通创建 create path [value]
-s 含有序列
-e 临时(重启或者超时消失)
#创建节点,后跟数据,也可不跟
[zk: localhost:2181(CONNECTED) 1] create /app1 shuju
输出:Created /app1
[zk: localhost:2181(CONNECTED) 2] create /app2
输出:Created /app2
[zk: localhost:2181(CONNECTED) 3] ls /
输出:[app1, app2, zookeeper]
#创建临时节点
create -e /app3
#持久化顺序节点,会自动给你加上编号,所以同名也没事
[zk: localhost:2181(CONNECTED) 13] create -s /app1
输出:Created /app10000000002
[zk: localhost:2181(CONNECTED) 14] create -s /app1
输出:Created /app10000000003
[zk: localhost:2181(CONNECTED) 15] create -s /app1
输出:Created /app10000000004
#创建临时顺序节点
[zk: localhost:2181(CONNECTED) 15] create -es /app3
输出:Created /app30000000005 #共用同一组编号
获得节点的值 [可监听] get path
-w 监听节点内容变化
-s 附加次级信息
[zk: localhost:2181(CONNECTED) 2] get /app1
输出:shuju
[zk: localhost:2181(CONNECTED) 3] get /app2
输出:null
设置节点的具体值 set path value
[zk: localhost:2181(CONNECTED) 4] set /app2 tianjia
[zk: localhost:2181(CONNECTED) 5] get /app2
输出:tianjia
查看节点状态 stat
删除节点 delete path
[zk: localhost:2181(CONNECTED) 6] create /app1/p1
输出:Created /app1/p1
[zk: localhost:2181(CONNECTED) 7] create /app1/p2
输出:Created /app1/p2
[zk: localhost:2181(CONNECTED) 8] ls /app1
输出:[p1, p2]
[zk: localhost:2181(CONNECTED) 9] delete /app1/p1
[zk: localhost:2181(CONNECTED) 10] ls /app1
输出:[p2]
递归删除节点 deleteall path
[zk: localhost:2181(CONNECTED) 11] deleteall /app1
[zk: localhost:2181(CONNECTED) 12] ls /
输出:[app2, zookeeper]
4、JavaAPI操作
4.1、Curator介绍
常用 ZooKeeper Java API:原生的 Java API、ZkClient、Curator
4.2、Curator API 常用操作
建立连接、添加节点、删除节点、修改节点、查询节点、Watch事件监听、分布式锁实现
新建 maven 项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.afei</groupId>
<artifactId>demo003_curator</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</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.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
在其他视频中教学的是以下依赖:
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-client</artifactId> <version>4.3.0</version> </dependency>
日志是以下内容
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n log4j.appender.logfile=org.apache.log4j.FileAppender log4j.appender.logfile.File=target/spring.log log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.properties
需要在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在文件中填入
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=%d %p [%c] - %m%n
#log4j.appender.logfile=org.apache.log4j.FileAppender
#log4j.appender.logfile.File=target/spring.log
#log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
#log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
4.2.1、建立连接
注意:使用 Linux 建立连接需要关闭防火墙,否则会一直执行重试策略
查看防火墙状态:firewall-cmd --state
关闭防火墙:systemctl stop firewalld
、systemctl disable firewalld
建立连接要使用 CuratorFrameworkFactory
工厂类,可以有两种方式:
第一种,使用 CuratorFrameworkFactory.newClient
//建立连接
@Test
public void testConnect(){
//第一种方式
/**
* 参数说明:
* connectString:连接字符串,zk server地址和端口
* "192.168.0.1:2181,192.168.0.2:2181"
* sessionTimeoutMs:会话超时时间,单位毫秒,默认60秒
* connectionTimeoutMs:连接超时时间,单位毫秒,默认15秒
* retryPolicy:重试策略,是接口
* 实现类ExponentialBackoffRetry,参数分别是休眠时间、重试次数
*/
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
CuratorFramework client = CuratorFrameworkFactory.newClient(
"192.168.162.128:2181",
60*1000,15*1000,
retryPolicy);
//开启连接
client.start();
}
第二种,使用 CuratorFrameworkFactory.builder()
//建立连接
@Test
public void testConnect(){
//第二种方式
/**
* namespace:指定名称空间,
* 指定之后在 zk 中操作数据都会默认是在/afei中操作数据,自动加上父节点,默认的名称空间是 /
*/
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.162.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("afei")
.build();
//开启连接
client.start();
}
4.2.2、创建节点
JUnit4常用的几个注解:
- @BeforeClass:针对所有测试,只执行一次,且必须为static void
- @Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)
- @Test:测试方法
- @After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次)
- @AfterClass:针对所有测试,只执行一次,且必须为static void
一个JUnit4的单元测试用例执行顺序为:
@BeforeClass -> @Before -> @Test -> @After ->@Before -> @Test -> @After -> @AfterClass
启动 zk 服务: ./zkCli.sh -server localhost:2181
创建节点
- 基本创建 client.create().forPath(“路径”)
- 创建节点,带有数据 client.create().forPath(“路径”,“hehe”.getBytes())
- 设置节点类型 client.create().withMode(CreateMode.EPHEMERAL).forPath(“”)
- 创建多级节点 client.create().creatingParentsIfNeeded().forPath(“”)
基本创建
public class CuratorTest {
private CuratorFramework client;
//建立连接
@Before
public void testConnect(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
//namespace:指定名称空间
client = CuratorFrameworkFactory.builder()
.connectString("192.168.162.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("afei")
.build();
//开启连接
client.start();
}
/**
* 创建节点
* 1.基本创建
* 2.创建节点,带有数据
* 3.设置节点类型
* 4.创建多级节点 /app1/p1
*/
@Test
public void testCreate1() throws Exception {
//1.基本创建
//因为建立连接的时候指定了 namespace,所以会创建节点 /afei/app1
//如果创建节点没有指定数据,则默认将当前客户端的 ip 作为数据存储
String path = client.create().forPath("/app1");
System.out.println(path);
}
//释放连接
@After
public void close(){
if (client != null)
client.close();
}
}
执行结果:
其他
/**
* 创建节点
* 1.基本创建 client.create().forPath("路径")
* 2.创建节点,带有数据 client.create().forPath("路径","hehe".getBytes())
* 3.设置节点类型 client.create().withMode(CreateMode.EPHEMERAL).forPath("")
* 4.创建多级节点 client.create().creatingParentsIfNeeded().forPath("")
*/
@Test
public void testCreate1() throws Exception {
//1.基本创建
//因为建立连接的时候指定了 namespace,所以会创建节点 /afei/app1
//如果创建节点没有指定数据,则默认将当前客户端的 ip 作为数据存储
String path = client.create().forPath("/app1");
System.out.println(path);
}
@Test
public void testCreate2() throws Exception {
//2.创建节点,带有数据
//注意传入是 byte[] 类型数据
String path = client.create().forPath("/app1","hehe".getBytes());
System.out.println(path);
}
@Test
public void testCreate3() throws Exception {
//3.设置节点类型,默认类型:持久化
/**
* .withMode(CreateMode.EPHEMERAL):设置为临时节点
* 在客户端查看不到生成的数据,因为从 JavaAPI 连接zk和客户端连接zk不是一次会话
* 临时节点数据仅存在于一次会话中
*/
String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3","hehe".getBytes());
System.out.println(path);
}
@Test
public void testCreate4() throws Exception {
//4.创建多级节点 /app1/p1
//注意,当父节点不存在的时候,不能创建多级节点,在客户端也是一样
//使用 .creatingParentsIfNeeded():表示若父节点不存在则创建父节点
String path = client.create().creatingParentsIfNeeded().forPath("/app4/p1");
System.out.println(path);
}
4.2.3、查询节点
查询节点:
- 查询数据 client.getData().forPath(“”)
- 查询子节点 client.getChildren().forPath(“”)
- 查询节点状态信息 client.getData().storingStatIn(stat对象).forPath(“”)
public class CuratorTest {
private CuratorFramework client;
//建立连接
@Before
public void testConnect(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
//namespace:指定名称空间
client = CuratorFrameworkFactory.builder()
.connectString("192.168.162.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("afei")
.build();
//开启连接
client.start();
}
/**
* 查询节点
* 1.查询数据 client.getData().forPath("")
* 2.查询子节点 client.getChildren().forPath("")
* 3.查询节点状态信息 client.getData().storingStatIn(stat对象).forPath("")
*/
@Test
public void testGet1() throws Exception {
//1.查询数据 get
byte[] bytes = client.getData().forPath("/app1");
System.out.println(bytes);
}
@Test
public void testGet2() throws Exception {
//2.查询子节点 ls
//注意,因为建立连接使用了 namespace,所以下面操作相当于 ls /afei
List<String> strings = client.getChildren().forPath("/"); //[app1, app4]
System.out.println(strings);
}
@Test
public void testGet3() throws Exception {
//3.查询节点状态信息 ls -s
//注意需要创建一个空的 Stat 对象, 会将查询出的状态信息结果注该对象
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
System.out.println(status);
//25,25,1645497568136,1645497568136,0,0,0,0,13,0,25
}
//释放连接
@After
public void close(){
if (client != null)
client.close();
}
}
4.2.4、修改节点
修改节点:
- 基本修改数据 client.setData().forPath(“”)
- 根据版本修改 client.setData().withVersion(version).forPath(“”)
/**
* 修改节点
* 1.基本修改数据 client.setData().forPath("")
* 2.根据版本修改 client.setData().withVersion(version).forPath("")
* version 是通过查询出来的,目的是为了让其他客户端或线程不干扰
* 如果其他人更改,导致版本号不一致,就会取消修改,保证原子性
*/
@Test
public void testSet1() throws Exception {
//1.基本修改数据 set
client.setData().forPath("/app1","11".getBytes());
}
@Test
public void testSet2() throws Exception {
//2.根据版本修改
//先查询版本信息
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
int version = status.getVersion();
System.out.println(version); //获得当前版本号
//.withVersion(100):表示在指定版本的基础上进行修改
// 如果当前版本不是指定的版本,则不会修改成功,保证原子性
client.setData().withVersion(version).forPath("/app1","11".getBytes());
}
4.2.5、删除节点
删除节点
- 删除当个节点 client.delete().forPath(“路径”)
- 删除多级节点 client.delete().deletingChildrenIfNeeded().forPath(“父节点路径”)
- 必须成功的删除,为了防止网络抖动,本质是重试 client.delete().guaranteed().forPath(“路径”)
- 回调
/**
* 删除节点
* 1.删除当个节点 client.delete().forPath("路径")
* 2.删除多级节点 client.delete().deletingChildrenIfNeeded().forPath("父节点路径")
* 3.必须成功的删除,为了防止网络抖动,本质是重试 client.delete().guaranteed().forPath("路径")
* 4.回调
*/
@Test
public void testDelete1() throws Exception {
//1.删除当个节点
client.delete().forPath("/app1");
}
@Test
public void testDelete2() throws Exception {
//2.删除多级节点
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
@Test
public void testDelete3() throws Exception {
//3.必须成功的删除
//如果zk访问连接断开了,重启之后发现仍然删除了,就是强制删除一直在重试
client.delete().guaranteed().forPath("/app4");
}
@Test
public void testDelete4() throws Exception {
//4.回调
//如果zk访问连接断开了,重启之后发现仍然删除了,就是强制删除一直在重试
client.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println(curatorEvent); //resultCode 的值为 0 表示删除成功
}
}).forPath("/app1");
}
4.3、Watch 事件监听
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目 录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序
ZooKeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册Watcher,比较繁琐;Curator引入了Cache来实现对ZooKeeper服务端事件的监听
ZooKeeper提供 了三种Watcher:
- NodeCache:只是监听某一个特定的节点
- PathChildrenCache:监控一个ZNode的子节点
- TreeCache:可以监控整个树上的所有节点,类似于 PathChildrenCache 和 NodeCache 的组合
4.3.1、NodeCache
只是监听某一个特定的节点
步骤:
-
创建 NodeCache 对象
new NodeCache(client,“/app1”);
-
注册监听
cache.getListenable().addListener(new NodeCacheListener());
-
开启监听
cache.start(true);
public class CuratorTest {
private CuratorFramework client;
//建立连接
@Before
public void testConnect(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
//namespace:指定名称空间
client = CuratorFrameworkFactory.builder()
.connectString("192.168.162.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("afei")
.build();
//开启连接
client.start();
}
/**
* 演示 NodeCache:只是监听某一个特定的节点
*/
@Test
public void testNodeCache() throws Exception {
//1、创建 NodeCache 对象
/**
* 构造器参数:
* 1.连接客户端对象
* 2.要监听的路径
* 3.boolean dataIsCompressed:是否对数据压缩,默认 false
* 若压缩,数据传输较快,但是获取数据需要解压缩
*/
NodeCache cache = new NodeCache(client,"/app1");
//2、注册监听
cache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
//只要节点增删改此处就会执行
System.out.println("节点变化了");
//获取修改节点后的数据
byte[] data = cache.getCurrentData().getData();
System.out.println(new String(data));
}
});
//3、开启监听
//如果为 true,则开启监听时加载缓冲数据
cache.start(true);
//监听需要保证服务未关闭,因为此处是测试方法
while (true){ }
}
//释放连接
@After
public void close(){
if (client != null)
client.close();
}
}
4.3.2、PathChildrenCache
监控一个ZNode的子节点,只监听子节点,节点本身和孙子节点也不会监听
步骤:
-
创建 PathChildrenCache 对象
new PathChildrenCache(client,“/app4”,true);
-
注册监听
cache.getListenable().addListener(new PathChildrenCacheListener());
-
开启监听
cache.start();
public class CuratorTest {
private CuratorFramework client;
//建立连接
@Before
public void testConnect(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
//namespace:指定名称空间
client = CuratorFrameworkFactory.builder()
.connectString("192.168.162.128:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("afei")
.build();
//开启连接
client.start();
}
/**
* 演示 PathChildrenCache:监控一个ZNode的子节点,只监听子节点,节点本身和孙子节点也不会监听
*/
@Test
public void testPathChildrenCache() throws Exception {
//1、创建 PathChildrenCache 对象
/**
* 构造器参数:
* 1.连接客户端对象client、要监听的路径path
* 2.boolean cacheData:true,缓存节点数据和状态
* 3.boolean dataIsCompressed:是否对数据压缩,默认 false
* 若压缩,数据传输较快,但是获取数据需要解压缩
* 4.CloseableExecutorService executorService:线程池,有默认值,一般使用内部提供的
*/
PathChildrenCache cache = new PathChildrenCache(client,"/app4",true);
//2、注册监听
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
System.out.println(event); //节点变化的信息,内部可直接拿到修改的数据
//1.获取变化类型
PathChildrenCacheEvent.Type type = event.getType();
//2.判断类型
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
//3.获取变化的数据
byte[] data = event.getData().getData();
System.out.println(new String(data));
}
}
});
//3、开启监听
cache.start();
//监听需要保证服务未关闭,因为此处是测试方法
while (true){ }
}
//释放连接
@After
public void close(){
if (client != null)
client.close();
}
}
4.3.3、TreeCache
/**
* 演示 TreeCache:监控某个节点自己和所有子节点们,可以监控整个树上的所有节点
*/
@Test
public void testTreeCache() throws Exception {
//1、创建 TreeCache 对象
//构造器参数:连接客户端对象client、要监听的路径path
TreeCache cache = new TreeCache(client,"/app4");
//2、注册监听
cache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
//里面的对象和PathChildrenCache里面的event对象一样结构
}
});
//3、开启监听
cache.start();
//监听需要保证服务未关闭,因为此处是测试方法
while (true){ }
}
4.4、分布式锁
4.4.1、分布式锁概念
在我们进行单机应用开发,涉汲并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题
但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨 JVM 之间已经无法通过多线程的锁解决同步问题
那么就需要一种更加高级的锁机制, 来处理这种跨机器的进程之间的数据同步问题——这就是分布式锁
从第三方分布式锁组件拿到唯一的锁
也可以在数据上直接加锁,但是会影响性能,因为数据库本身性能就比较差
分布式锁实现的分类:
Redis 分布式锁性能极高,但是有隐患,不安全
4.4.2、ZooKeeper 分布式锁原理
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点
-
客户端获取锁时,在 lock 节点下创建临时顺序节点
临时:以防出现客户端宕机,而导致锁无法释放问题。当客户端宕机,连接会断开,临时节点仅存在于一个会话中,当连接断开,临时节点就会消失
lock 节点是此处举例使用的名称
-
然后获取 lock 下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除
-
如果发现自己创建的节点并非 lock 所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那一个节点,同时对其注册事件监听器,监听删除事件
-
如果发现比自己小的那个节点被删除,则客户端的 Watcher 会收到相应通知,此时再次判断自己创建的节点是否是 lock 子节点中序号最小的
如果是,则获取到了锁;如果不是,则重复以上步骤继续获取到此自己小的那一个节点,并注册监听
这里在监听到事件之后,再次进行一次判断大小,可以防止出现中间客户端宕机状况。
例如此时有 1/2/3,2出现宕机删除节点,3监听到了事件,然后再次判断自己并不是序号最小的,就对比自己小的节点注册监听
4.4.3、模拟售票案例
创建类模拟卖票
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 ticket = 10; //数据库票数
private InterProcessMutex lock;
public Ticket12306() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.0.1:2181,192.168.0.2: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 (ticket > 0){
System.out.println(Thread.currentThread() + "==" + ticket);
ticket--;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试多服务器访问
public class LockTest {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
//创建客户端
Thread t1 = new Thread(ticket12306, "携程");
Thread t2 = new Thread(ticket12306, "飞猪");
t1.start();
t2.start();
}
}
5、集群
5.1、集群搭建
分别在是三个目录位置解压 ZooKeeper 安装包,并分别创建 data 目录
在各自的 data 目录里面创建 myid 文件,在文件中添加对应的编号(注意:上下不要有空行,左右不要有空格)
注意:添加 myid 文件,一定要在 Linux 里面创建
分别修改三个zk的配置文件zoo.cfg文件:
-
修改数据存储路径配置 dataDir
-
修改端口号 clientPort,因为是一台电脑模拟伪集群
-
增加如下配置 server.2=192.168.0.1:2888:3888
-
解读增加的配置:server.A=B:C:D
-
A 是一个数字,表示这个是第几号服务器
集群模式下配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面有一个数据 就是 A 的值,Zookeeper 启动时读取此文件,拿到里面的数据与 zoo.cfg 里面的配置信息比 较从而判断到底是哪个 server。
-
B 是这个服务器的地址
-
C 是这个服务器 Follower 与集群中的 Leader 服务器交换信息的端口,服务器之间通信端口
-
D 是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口,服务器之间投票选举端口
-
5.2、选举机制
当有多个 zk 服务的时候,需要有个领导者,就有选举机制。在Leader选举中,如果某台ZooKeeper获得了超半数的选票,就是Leader
Leader 选举:
-
Serverid:服务器 ID
编号越大在选举算法中的权重越大
-
Zxid:数据 ID
值越大说明数据越新,在选举算法中的越新权重越大
第一次启动:
在有三台服务器的集群中,如果仅仅只启动了一台服务器,集群不会工作,因为投票没有过半数,无法选举出 Leader,集群不可用
如果按顺序启动 1 、2 服务器,2 会被投票成为 Leader;此后,再启动后来的 3 ,Leader仍然是 2 ,不会受影响
非第一次启动:
出现以下情况之一,就进入 Leader 选举:
- 服务器初始化启动
- 服务器运行期间无法和Leader保持连接
5.3、集群角色
在 ZooKeeper 集群服务中有三个角色:
- Leader 领导者
- 处理事务请求
- 集群内部各服务器的调度者(集群内同步数据)
- Follower 跟随者
- 处理客户端非事务请求,转发请求给 Leader 服务器
- 参与 Leader 选举投票
- Observer 观察者(因为查询的请求较多,Follower压力大,用来分担压力)
- 处理客户端非事务请求,转发事务请求给 Leader 服务器