1. Zookeeper 入门
1.1 概述
Zookeeper 是一个开源的分布式的(由多台服务器来完成比较复杂的任务),为分布式框架提供协调服务的 Apache 项目
Zookeeper 从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经再 Zookeeper 上注册的那些观察者做出相应的反应。
1.2 特点
- Zookeeper:一个领导者(Leader),多个跟随着(Follower)组成的集群。
- 集群中只要有半数以上节点存活,Zookeeper 集群就能正常服务,所以 Zookeeper 适合安装奇数台服务器。
- 全局数据一致:每个 Server 保存一份相同的数据副本,Client 无论链接到那个 Server,数据都是一样的。
- 更新请求顺序执行,来自同一个 Client 的更新请求按其发送顺序依次执行
- 数据更新原子性,一次数据更新要么成功,要么失败。
- 实时性,在一定时间范围内,Client 能读到最新数据。
1.3 数据结构
Zookeeper 数据模型的结构于 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称作一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。
1.4 应用场景
提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。
1.4.1 统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别
例如:IP 不容易记住,而域名容易记住
1.4.2 统一配置管理
- 分布式环境下,配置文件同步非常常见
一般要求一个集群中,所有节点的配置信息是一致的,比如 Kafka 集群
对配置文件修改后,希望能够快速同步到各个节点上 - 配置管理可交由 Zookeeper 实现
可将配置信息写入 Zookeeper 上的一个 ZNode
各个客户端服务器监听这个 ZNode
一旦 ZNode 中的数据被修改,Zookeeper 将通知各个客户端服务器
1.4.3 统一集群管理
- 分布式环境中,实时掌握每个节点的状态是必要的
可根据节点实时状态做出一些调整 - Zookeeper 可以实现实时监控节点状态变化
可将节点信息写入 Zookeeper 上的一个 ZNode
监听这个 ZNode 可获取它的实时状态变化
1.4.4 服务器是动态上下限
客户端能实时洞察到服务器上下线的变化
1.4.5 软负载均衡
在 Zookeeper 中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
1.5 下载地址
官网地址:https://zookeeper.apache.org/
2. Zookeeper 本地安装
2.1 本地模式安装使用
- Linux 环境中安装 JDK
- 官网下载的压缩包拷贝到 Linux 系统下 /opt/software 目录下,然后解压
tar -zxvf apache.zookeeper-3.5.7-bin.tar.gz //解压
mv apache.zookeeper-3.5.7-bin zookeeper-3.5.7 //修改文件名
- 在自己的目录下创建一个文件夹用于保存数据,在 conf/zoo.cfg (修改后的名字之前是 zoo_sample.cfg)指定路径
- 在 bin 目录下执行命令
./zkServer.sh start
启动服务
- 在 bin 目录下执行命令
./zkCli.sh
启动客户端
- 执行
quit
命令停止掉客户端
- 执行
./zkServer.sh stop
命令停止服务端
2.2 配置参数解读
Zookeeper 中的 zoo.cfg 中的参数意思如下:
- tickTime=2000:通信心跳时间,Zookeeper 服务器客户端心跳时间,单位毫秒
周期性的发一些相关信息
- initLimit=10:LF 初始通信时限
Leader 和 Follower 初始连接时能容忍的最多心跳数(tickTime的数量),第一次建立通信连接,最多不能超过 10 * tickTime 的时间
- syncLimit=5:LF 同步通信时限
Leader 和 Follower 之间通信时间如果超过 syncLimit * tickTime,Leader 认为 Follower死掉,从服务器列表中删除 Follower。 - dataDir:保存 zookeeper中的数据
默认是在 tmp 目录下,因为 tmp 目录定期进行清楚,所以我们需要指定自己创建的路径,一般不用默认目录 - clientPort=2181:客户端端口号,通常不进行修改
3. Zookeeper 集群操作
3.1 集群安装
3.1.1 集群规划
- 有三个虚拟机环境,zookeeper01、zookeeper02、zookeeper03 ,分别在三台主机上部署 Zookeeper,必须保证上面的操作中每一台主机上的 zookeeper 能够跑起来,都有一个自己的 zkData 文件夹
- 在每一个 zkData 文件夹中创建一个
myid
文件,在文件中添加于 server 对应的编号(zookeeper01 上是 1),这个编号就是整个集群中唯一的(注意:上下不要有空行,左右不能有空格)
例如:zookeeper01 的zkData 下之执行命令 vim myid
并且输入 1
- 在每一台主机上配置服务器的信息,在
/zookeeper-3.5.7/conf/zoo.cfg
文件中最下面进行配置
server.1:zookeeper01:2888:3888
server.2:zookeeper02:2888:3888
server.3:zookeeper03:2888:3888
- 配置信息解析
server.A=B:C:D
A 是一个数字,表示这个是第几号服务器
集群模式下配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面有一个数据就是 A 的值,Zookeeper 启动时读取此文件,拿到里面的数据于 zoo.cfg 里面的配置信息比较从而判断到底是那个 server
B 是一个服务器的地址
C 是这个服务器 Follower 于集群中的 Leader 服务器交换信息的端口
D 是万一集群中的 Leader 服务器挂了,需要一个端口重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口
- 启动 zookeeper01 的服务器
启动后我们查看状态是 Error ,因为有 3 台服务器,目前只是启动了一个服务器,没有达到半数,没有选择出 Leader,所以集群暂时就没有办法工作。
当三台服务器都能正常启动后,可以看到状态是一个 Leader,两个follower
- 至此集群搭建成功
注意:
- 一定要关闭防火墙
systemctl stop firewalld
- 配置文件一定不能写错,不用主机名用ip 地址感觉更好 server.1:ip:2888:3888
- myid 文件一定不要放错位置,文件内容也一定不要有空格换行
3.1.2 选举机制
第一次启动的情况
- 服务器 1 启动,发起第一次选举。服务器1 投自己一票,此时服务器 1 票数为 1,不够半数以上(3票),选举无法完成,服务器1 状态保持为 LOOKING
- 服务器 2 启动,再发起一次选举。服务器 1 和 2 分别投自己一票并交换选票信息:此时服务器 1 发现服务器 2 的 myid 比自己目前投票选举的(服务器 1)大,更改选票为推举服务器 2,此时服务器 1 票数为 0,服务器 2 票数为 2 ,没有半数以上结果,选举无法完成,服务器 1,2 保持 LOOKING
- 服务器 3 启动,发起一次选举。此时服务器 1 和 2 都会更改选票为 服务器 3。此次投票结果:服务器 1 为 0票,服务器 2 为 0 票,服务器 3 为 3 票。此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader。服务器 1,2 更改状态为 FOLLOWER,服务器 3 更改状态为 Leader
- 服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器 3 为 3 票,服务器 4 为 1 票。此时服务器 4 服从多数,更改选票信息为 服务器 3,并更改状态为 FOLLOWER
- 服务器 5 启动,同 4 一样为 FOLLOWER
其实就是说,每一个服务器启动的时候都会给自己投一票,然后当有别的服务器启动的时候,还没有 Leader 的情况下,会将自己的票投给 myid 比自己的大的服务器,一旦 Leader 确定之后,在启动的服务器只能为 Follower。
- SID:服务器ID,用来唯一标识一台 Zookeeper 集群中的机器,每台机器不能重复,和 myid 一样
- ZXID:事务 ID,ZXID 是一个事务 ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的 ZXID 值不一定完全一致,这和 Zookeeper 服务器对于客户端 “更新请求” 的处理逻辑有关
- Fpoch:每个 Leader 任期的代号,没有 Leader 时同一轮投票过程中的逻辑时钟值是相同的,每投完一次票这个数据就会增加。
非第一次启动的情况
如果集群中的某一台服务器挂掉的时候,又该怎么选举 Leader 呢?
- 当 Zookeeper 集群中的一台服务器出现以下两种情况之一的时候,就会开始进入 Leader 选举
- 服务器初始化启动
- 服务器运行期间无法和 Leader 保持连接
以上面图的 5 台服务器为例,服务器 5 挂掉了,它不认为是自己挂掉,而是认为其他的服务器都挂掉了,所以进行选举
- 当一台机器进入 Leader 选举流程时,当前集群也可能会处于以下两种状态:
- 集群中本来就已经存在一个 leader
对于这种已经存在 Leader 的情况,机器视图去选举 Leader 时,会被告知当前服务器的 Leader 信息,对于该机器来说,仅仅需要和 Leader 机器建立连接,并进行状态同步即可。 - 集群中确定不存在 Leader
假设 Zookeeper 由 5 台服务器组成,SID 分别为 1、2、3、4、5,ZXID 分别为 8、8、8、7、7,并且此时 SID 为 3 的服务器是 Leader 。某一时刻,3 和 5 服务器出现故障,因此开始进行 Leader 选举。
SID 为 1、2、4 的机器投票情况:
EPOCH | ZXID | SID |
1 | 8 | 1 |
1 | 8 | 2 |
1 | 7 | 4 |
这种情况的选举规则:1. EPOCH 大的直接胜出,2. EPOCH 相同,事务 ID 大的胜出,3. 事务ID 相同,服务器 ID 大的胜出。所以上面胜出的是 2 好服务器
3.1.3 ZK 集群启动停止脚本
由于我们使用的是集群,所以停掉集群关闭需要每一台服务器操作一个停止命令,非常的繁琐
- 脚本内容
#/bin/bash
case $1 in
"start"){
for i in zookeeper01 zookeeper02 zookeeper03
do
echo ------ zookeeper $i 启动 ------
ssh $i "/opt/software/zookeeper-3.5.7/bin/zkServer.sh start"
done
}
;;
"stop"){
for i in zookeeper01 zookeeper02 zookeeper03
do
echo ------ zookeeper $i 停止 ------
ssh $i "/opt/software/zookeeper-3.5.7/bin/zkServer.sh stop"
done
}
;;
"status"){
for i in zookeeper01 zookeeper02 zookeeper03
do
echo ------ zookeeper $i 状态 ------
ssh $i "/opt/software/zookeeper-3.5.7/bin/zkServer.sh status"
done
}
;;
esac
先在本地用 txt 写完之后复制一下
在 Linux 中的 /bin 目录下创建文件 zk.sh
,然后将脚本内容粘贴进去
粘贴进去之后需要对这个脚本设置使用权限 chmon 777 zk.sh
然后在 zookeeper01 服务器上就可以同时操作其他的集群服务器了
执行命令:
-
zk.sh start
启动集群
-
zk.sh status
查看状态
-
zk.sh stop
停止
3.2 客户端命令操作
执行命令 zkCli.sh -c server zookeeper01:2181
在 zookeeper01 服务器上启动客户端
不加 -c 也可以启动,不加 -c 是localhost,加 -c 是指定的服务器名称
3.2.1 命令行语法
基本语法 | 功能描述 |
help | 显示所有操作命令 |
ls path | 使用 ls 命令来查看当前 znode 的子节点[可监听] -w 监听子节点变化 -s 附加次级信息 |
create | 普通创建 -s 含有序列 -e 临时(重启或者超时消失) |
get path | 获得节点的值[可监听] -w 监听节点内容变化 -s 附加次级信息 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
deleteall | 递归删除节点 |
3.2.2 ZNode 节点数据信息
- 查看当前 znode 中包含的内容
ls /
- 查看当前节点详细数据
ls -s /
- czxid:创建节点的事务 zxid
每次修改 Zookeeper 状态都会产生一个 Zookeeper 事务 ID。事务 ID 是 Zookeeper 中所有修改总的次序。每次修改都会唯一的 zxid。 - ctime:znode 被创建的毫秒数(从 1970 年开始)
- mzxid:znode 最后更新的事务 zxid
- mtime:znode 最后修改的毫秒数(从 1970年开始)
- pZxid:znode 最后更新的子节点 zxid
- cversion:znode 子节点变化号,znode 子节点修改次数
- dataversion:znode 数据变化号
- aclVersioni:znode 访问控制列表的变化号
- ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0
- dataLength:znode 的数据长度
- numChildren:znode 子节点数量
3.2.3 节点类型(持久/短暂/有序号/无序号)
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
- 持久化目录节点
客户端与 Zookeeper 断开连接后,该节点依旧存在 - 持久化顺序编号目录节点
客户端与 Zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号 - 临时目录节点
客户端与 Zookeeper 断开连接,该节点被删除 - 临时顺序编号目录节点
客户端与 Zookeeper 断开连接后,该节点被删除,只是 Zookeeper给该节点名称进行顺序编号
说明:创建 znode 时设置顺序标识,znode 名称后会附加一个值,顺序是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
- 创建普通节点(永久节点 不带序号)
create zhongguo "state" // 节点名称是zhongguo,state 是存储的值
在 zhongguo 节点下在创建一个节点
create /zhongguo/beijing "beijing"
- 创建节点(永久节点 带序号)
create -s /zhongguo/shanghai //-s 表示带序号
永久节点在退出客户端
quit
后开启是不会丢失的
- 创建节点(临时节点 不带序号)
create -e /meiguo "state"
- 创建节点(临时节点 带序号)
create -e -s /meiguo/niuyue "state"
临时节点在退出客户端后开启是会丢失的
- 修改节点的值
set /zhongguo "tianjin"
3.2.4 监听器原理
- 首先要有一个 main() 线程
- 在 main 线程中创建 Zookeeper 客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过 connet 线程将注册的监听事件发送给 Zookeeper
- 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中
- Zookeeper 监听到有数据或路径变化,就会将这个消息发送给 listener 线程。
- listener 监听到变化线程内部调用 process() 方法。
- 常见的监听
监听节点数据的变化:get path [watch]
监听子节点增减的变化:ls path [watch]
监听节点数据变化
- 在 zookeeper01 主机上注册监听 /zhongguo 节点的数据变化
get -w /zhongguo // -w 表示监听这个值
- 在 zookeeper02 主机上修改 /zhongguo 节点的数据
set /zhongguo "nanjing"
修改后 zookeeper01 就会提示监听到节点数据发生变化
**注意:**在 zookeeper02 上再次修改 /zhongguo 的值,zookeeper01 上不会再去监听。因为注册一次,只能监听一次。要想再次监听,需要再次注册。
监听节点的子节点变化
- 在 zookeeper01 上执行命令
ls -w /zhongguo // 监听路径使用 ls,监听zhongguo 这个路径是否发生变化
- 在 zookeeper02 上在 /zhongguo 下创建一个路径
create /zhongguo/xian "shanxi"
- 然后在 zookeeper01 上可以看到监听的结果
**注意:**节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。
3.2.5 节点删除与查看
- 删除节点:
delete /zhongguo/xian
- 递归删除节点(节点下面有多个子节点):
deleteall /zhongguo
- 查看节点状态:
stat /zhongguo
3.3 客户端 API 操作
3.3.1 创建节点
前提保证集群可以正常启动,防火墙关闭。
- 创建一个 Maven 项目
- 导入依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</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
- 创建一个类
ZkCli
public class ZkClient {
// 集群的地址端口号
String connectString = "zookeeper01:2181,zookeeper02:2181,zookeeper03:2181";
// 超时时间
int sessionTimeout = 20000;
// 连接对象
ZooKeeper zooKeeper;
@Before // init() 用于建立连接,为什么用 Before ?因为在下面的单元测试中需要提前建立好连接,创建 Zookeeper 连接对象
public void init() throws IOException {
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
// 一个监听事件
@Override
public void process(WatchedEvent event) {
}
});
}
@Test // 创建节点
public void create() throws KeeperException, InterruptedException {
//final String path 创建节点的路径
// byte data[] 节点的内容
// List<ACL> acl 权限控制 OPEN_ACL_UNSAFE 全部可以操作
// CreateMode createMode 创建什么样的节点(持久的带序号/不带 临时的带序号/不带)
String nodeCreated = zooKeeper.create("/world",
"hello,zookeeper".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
}
- 在 Linux 系统中执行命令
ls /
查看是否创建节点world
成功
3.3.2 获取子节点并监听节点变化
@Before
public void init() throws IOException {
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
// 一个监听事件
@Override
public void process(WatchedEvent event) {
List<String> children = null;
try {
children = zooKeeper.getChildren("/", true);
// 监听到变化输出变化次数
count++;
System.out.println("第" + count + "次变化-----------");
for (String child : children) {
System.out.println(child);
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
@Test
public void getChildren() throws KeeperException, InterruptedException {
// 程序不要结束,持续监听
Thread.sleep(Long.MAX_VALUE);
}
}
启动后在 zookeeper 集群中执行命令 create /world1
命令,然后查看 idea 控制台的输出信息的变化
3.3.3 判断 Znode 是否存在
@Test
public void exist() throws KeeperException, InterruptedException {
Stat exists = zooKeeper.exists("/world", false);
System.out.println(exists == null ? "not exist" : "exist");
}
3.3.4 客户端向服务端写数据流程
写入请求直接发送给 Leader 节点
- Client 直接访问 Leader ,然后 Leader 写一份数据,同时告诉 Follower1 写一份数据
- 然后 Follower1 写完数据后告诉 Leader 写完了
- Leader 判断是否超过半数写完,以上面为例,2 台写完就是超过半数了(半数机制)
- 如果超过半数了,Leader 直接告诉 Client 数据写完成
- 告诉 Client 之后,通知生效的 Follwer 进行写操作,写完之后告诉 Leader
写流程写入请求发送给 Follower 节点
- Client 请求了 Follower1
- Follower1 没有写的权限,将请求通知给 Leader ,Leader 写一份数据,写完之后通知 Follower1 写一份数据
- 如果 Leader 收集到的写的次数超过半数,告诉 Follower1 写完了
- Follower1 收到写完的消息后回复 Client,为什么 Leader 不回复?因为 Client 请求的 Follower1
- Follower1 回复完之后,Leader 通知别的 Follower 进行写操作
4. 服务器动态上下线监听案例
4.1 需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
4.2 需求分析
首先服务器1、服务器2、服务器3 上线后都去 Zookeeper 中进行注册
客户端通过监听机制监听服务器的注册情况
假设 服务器2 宕机了,则客户端的会监听到 下线事件,这是在去获取客户端的注册信息的时候就不会获取宕机的那个服务器
4.3 具体实现
**1. 在集群中创建永久目录 /servers **
create /servers "servers"
2. 创建客户端,建立连接,并且监听 servers 的路径变化
public class ZKClient {
// 连接主机和端口号
private String connectString = "zookeeper01:2181,zookeeper02:2181,zookeeper03:2181";
// 超时时间
private int sessionTimeout = 2000;
// 创建的 zookeeper 对象
ZooKeeper zk;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
ZKClient client = new ZKClient();
// 1. zk 连接
client.getConnect();
// 2. 监听 /servers 下面子节点增加和删除
client.getServerList();
// 3. 业务逻辑(睡觉)
client.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
// 得到路径的信息
private void getServerList() throws KeeperException, InterruptedException {
List<String> children = zk.getChildren("/servers", true);
ArrayList<String> servers = new ArrayList<>();
for (String child : children) {
byte[] data = zk.getData("/servers/" + child, false, null);
servers.add(new String(data));
}
System.out.println(servers);
}
private void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
// 因为默认监听只会触发一次,这里调用是为了持续监听
getServerList();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
3. 创建路径查看监听结果
- 控制台输出结果
4. 创建服务端代码
/**
* 服务器
*/
public class ZKServer {
private String connectString = "zookeeper01:2181,zookeeper02:2181,zookeeper03:2181";
private int sessionTimeout = 2000;
ZooKeeper zk;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
ZKServer server = new ZKServer();
// 1. 获取 zk 连接
server.getConnect();
// 2. 注册服务器到 zk 集群
server.regist(args[0]);
// 3. 启动业务逻辑(睡觉)
server.business();
}
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
// hostname 用来创建节点的主机的名称
// 创建节点
private void regist(String hostname) throws KeeperException, InterruptedException {
String s = zk.create("/servers/" + hostname,
hostname.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname + "已经上线了");
}
private void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
}
}
启动运行,向 集群中注册服务,因为 使用到了 args 参数,所以需要先进行设置
分别设置 3 次,也就是启动 3 次,zookeeper01、zookeeper02、zookeeper03
- 执行完成之后,可以看到 client 监控信息
注意: 使用代码的情况下启动一个,下线一个,因为创建的时候使用的是 CreateMode.EPHEMERAL_SEQUENTIAL 创建模式的临时的
5. Zookeeper 分布式锁案例
什么叫做分布式锁?
比如说 进程1 在使用该资源的时候,会先去获得锁,进程1 获取锁以后会对该资源保持独占,这样其他进程就无法访问该资源,进程1 用完该资源以后就将锁式释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。我们把这个分布式环境下的这个锁叫做分布式锁。
- 接收到请求后,在 /locks 节点下创建一个临时顺序节点。
- 判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听
- 获取到锁,处理完业务后,delete 节点释放锁,然后下面的节点将收到通知,重复第二步步骤。
5.1 原生 Zookeeper实现分布式锁案例
代码实现
- DistributedLock.java
public class DistributedLock {
// 集群地址
private final String connectString = "zookeeper01:2181,zookeeper02:2181,zookeeper03:2181";
// 超时时间
private final int sessionTimeout = 2000;
// Zookeeper 对象
ZooKeeper zk;
// 存储当前创建锁的节点
String currentMode = null;
// 用于延迟
private CountDownLatch countDownLatch = new CountDownLatch(1);
private CountDownLatch waitLatch = new CountDownLatch(1);
// 存储当前锁的前一个锁
private String waitPath;
public DistributedLock() throws IOException, InterruptedException, KeeperException {
// 获取连接
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// countDownLatch 连接上 zk,释放
if (event.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
// waitLatch 需要释放
// 删除节点 路径是否与前一个路径相等
if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
waitLatch.countDown();
}
}
});
// 等待 zk 正常连接后再往下走
countDownLatch.await();
// 判断根节点 /locks 是否存在,如果不存在则创建一个
Stat stat = zk.exists("/locks", false);
if (stat == null) {
zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
// 对 zk 加锁
public void zkLock() throws KeeperException {
try {
// 创建对应的临时带序号节点
currentMode = zk.create("/locks/" + "seq-", "null".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(currentMode);
// 判断创建节点是否是最小的序号节点,如果是获取到锁,否则监听前一个节点
List<String> children = zk.getChildren("/locks", false);
// 如果之后一个值,直接获取锁就可以了,如果有多个节点,需要判断谁最小
if (children.size() == 1) {
return;
} else {
// 将节点进行排序
Collections.sort(children);
// 获取节点名称 seq-00000000
String thisNode = currentMode.substring("/locks/".length());
// 通过 seq-00000000 获取到在集合当中的位置
int index = children.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
// 就只有一个节点
return;
} else {
// 需要进行监听前一个节点的变化
waitPath = "/locks/" + children.get(index - 1);
zk.getData(waitPath, true, null);
// 等待监听
waitLatch.await();
return;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 解锁
public void unZkLock() throws KeeperException, InterruptedException {
// 删除节点
zk.delete(currentMode, -1);
}
}
- DistributedLockTest.java
// 创建两个线程,同时去访问数据
public class DistributedLockTest {
public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
// 创建两个客户端
DistributedLock lock1 = new DistributedLock();
DistributedLock lock2 = new DistributedLock();
// 创建了两个线程,每个线程都是有延迟的,同一时间有 lock1 和 lock2 同时获取锁
// lock1 先获取锁,lock2 需要等 lock1 释放锁后才能拿到资源
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.zkLock();
System.out.println("线程 1 启动,获取到锁");
Thread.sleep(5 * 1000);
lock1.unZkLock();
System.out.println("线程 1 释放锁");
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.zkLock();
System.out.println("线程 2 启动,获取到锁");
Thread.sleep(5 * 1000);
lock2.unZkLock();
System.out.println("线程 2 释放锁");
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
5.2 Curator 框架实现分布式案例
- 原生的 JavaAPI 开发存在的问题
会话连接是异步的,需要自己去处理。比如:CountDownLatch
Watch 需要重复注册,不然就不会生效
开发的复杂性比较高
不支持多节点删除和创建。需要自己去递归
- Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。
官方文档:https://curator.apache.org/index.html
Curator 操作案例
- 引入依赖
<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>
- 创建连接并建立两个线程测试
public class CuratorLockTest {
public static void main(String[] args) {
// 创建分布式锁 1
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
// 创建分布式锁 2
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
// 线程 1
new Thread(new Runnable() {
@Override
public void run() {
// 测试
try {
lock1.acquire();
System.out.println("线程 1 获取锁");
// 测试锁冲突
lock1.acquire();
System.out.println("线程 1 再次获得锁");
Thread.sleep(2000);
lock1.release();
System.out.println("线程 1 释放锁");
lock1.release();
System.out.println("线程 1 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// 线程 2
new Thread(new Runnable() {
@Override
public void run() {
// 测试
try {
lock1.acquire();
System.out.println("线程 2 获取锁");
// 测试锁冲突
lock1.acquire();
System.out.println("线程 2 再次获得锁");
Thread.sleep(2000);
lock1.release();
System.out.println("线程 2 释放锁");
lock1.release();
System.out.println("线程 2 再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static CuratorFramework getCuratorFramework() {
ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
// 创建客户端
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("zookeeper01:2181,zookeeper02:2181,zookeeper03:2181")
.connectionTimeoutMs(2000)
.sessionTimeoutMs(2000) // 超时时间
.retryPolicy(policy).build();// 连接失败之后多长时间进行重试
// 启动客户端
client.start();
System.out.println("zookeeper 启动成功");
return client;
}
}
6. 常见面试题
6.1 选举机制
半数机制,超过半数的投票通过,即通过
- 第一次启动选举规则:
投票过半数,服务器 id 大的胜出 - 第二次启动选举:
EPOCH 大的胜出
EPOCH 相同,事务 id 大的胜出
事务 id 相同,服务器 id 大的胜出
6.2 生产集群安装多少 zk 合适
安装奇数台
根据经验可得:
- 10 台服务器:3 台 zk
- 20 台服务器:5 台 zk
- 100 台服务器:11 台 zk
- 200 台服务器: 11 台 zk
服务器的台数多:好处就是提高可靠性,坏处就是提高通信延时
6.3 常用命令
ls、get -s、create、delete、deleteall、set、stat