文章目录
1 Zookeeper基础概念
1.1 概念
- Zookeeper 是一个开源的分布式的,为分布式框架提供协调服务的 Apache 项目。
- Zookeeper工作机制:Zookeeper从设计模式角度来理解:是一个基 于观察者模式设计的分布式服务管理框架,它负 责 存储和管理大家都关心的数据,然 后接受观察者的 注 册,一旦这些数据的状态发生变化,Zookeeper 就 将负责通知已经在Zookeeper上注册的那些观察 者做出相应的反应。
通俗来说,就是zookeeper用于管理服务器节点提供的服务,并且客户端能够在zookeeper注册监听这些服务器的状态,一旦发生服务器上下线事件,客户端可以收取到相应的信息,zookeeper相当于一个有通知机制的文件系统。
1.2 Zookeeper特点
-
Zookeeper的特点:
1)Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。 (不存在歧义的操作可以跟follower进行通信,否则需跟leader进行通信)
2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所 以Zookeeper适合安装奇数台服务器。
3)全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4)更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
5)数据更新原子性,一次数据更新要么成功,要么失败。
6)实时性,在一定时间范围内,Client能读到最新数据。 -
ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个 节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过 其路径唯一标识。
1.3 Zookeeper提供的服务
- ZooKeeper提供的服务包括:提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下 线、软负载均衡等。
- 统一命名服务:在分布式环境下,经常需要对应用/服 务进行统一命名,便于识别。
- 统一配置管理:
1)分布式环境下,配置文件同步非常常见。 (1)一般要求一个集群中,所有节点的配置信息是 一致的,比如 Kafka 集群。(2)对配置文件修改后,希望能够快速同步到各个 节点上。
2)配置管理可交由ZooKeeper实现。 (1)可将配置信息写入ZooKeeper上的一个Znode。(2)各个客户端服务器监听这个Znode。 (3)一 旦Znode中的数据被修改,ZooKeeper将通知 各个客户端服务器。 - 统一集群管理:
1)分布式环境中,实时掌握每个节点的状态是必要的。 (1)可根据节点实时状态做出一些调整。
2)ZooKeeper可以实现实时监控节点状态变化 (1)可将节点信息写入ZooKeeper上的一个ZNode。 (2)监听这个ZNode可获取它的实时状态变化。 - 服务器节点动态上下线:客户端能够实时洞察到服务器上下线的变化
- 软负载均衡:在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
2 Zookeeper安装
2.1 zookeeper本地安装
-
访问官网https://zookeeper.apache.org/下载相应的版本且后缀名为bin.tar.gz的安装包
-
上传到linux指定路径中进行解压并修改解压得到的文件夹名称为zookeeper-3.5.7
mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7
-
将/opt/module/zookeeper-3.5.7/conf 这个路径下的 zoo_sample.cfg 修改为 zoo.cfg
-
在/opt/module/zookeeper-3.5.7下创建zookeeper目录存储目录zkData
-
打开 zoo.cfg 文件,指定zookeeper数据存储路径,修改 dataDir 路径为/opt/module/zookeeper-3.5.7/zkData
-
相关启动关闭命令
# 开启zookeeper服务 bin/zkServer.sh start # 查看zookeeper服务状态 bin/zkServer.sh status # 启动客户端 bin/zkCli.sh # 退出客户端 quit # 停止zookeeper服务 bin/zkServer.sh stop
2.2 zookeeper配置
- zookeeper配置文件zoo.cfg中的参数
1)tickTime = 2000:通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒。配置服务器与客户端或服务器与服务器之间的通信心跳时间。
2)initLimit = 10:LF初始通信时限。表示Leader和Follower初始连接时能容忍的时间,10代表10个tickTime,超过该时间则认为通信失败。
3)syncLimit = 5:LF同步通信时限。Leader和Follower之间通信时间如果超过syncLimit * tickTime,Leader认为Follwer死 掉,从服务器列表中删除Follwer。
4)dataDir:保存Zookeeper中的数据。注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录。
5)clientPort = 2181:客户端连接端口,通常不做修改。
2.3 zookeeper集群安装
-
做好集群规划,即需要在集群哪些节点上部署zookeeper。目前只有三个节点hadoop102、103、104,三个节点都部署上zookeeper,因为zookeeper至少需要三台服务器
-
在hadoop102上解压zookeeper安装包,同1.4进行配置
-
在/opt/module/zookeeper-3.5.7/zkData 目录下创建一个 myid 的文件,在文件中添加与服务器相对应的编号,hadoop102用编号2。分发后在hadoop103中的myid文件中编号改为3,hadoop104中myid文件中编号改为4,以此类推。
-
在conf/下的配置文件zoo.cfg中添加以下配置:并分发到各个zookeeper节点上
#######################cluster########################## server.2=hadoop102:2888:3888 server.3=hadoop103:2888:3888 server.4=hadoop104:2888:3888
server.A=B:C:D
A 是一个数字,表示这个是第几号服务器; 集群模式下配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面有一个数据 就是 A 的值,Zookeeper 启动时读取此文件,拿到里面的数据与 zoo.cfg 里面的配置信息比 较从而判断到底是哪个 server。
B 是这个服务器的地址;
C 是这个服务器 Follower 与集群中的 Leader 服务器交换信息的端口;
D 是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。 -
在每个服务器中都启动zookeeper,会通过选举机制来产生follower跟leader
3 Zookeeper操作
3.1 Zookeeper选举机制(面试重点)
-
zookeeper集群第一次启动的选举机制:
-
zookeeper集群非第一次启动的选举机制:
3.2 Zookeeper集群启动停止脚本
-
#!/bin/bash case $1 in "start"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 启动 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start" done };; "stop"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 停止 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop" done };; "status"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 状态 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status" done };; esac
3.3 客户端命令行操作
-
指定服务器开启客户端
bin/zkCli.sh -server 主机名称:2181
-
# 查看当前znode中所包含的内容 ls / # 查看当前节点详细数据 ls -s / [zookeeper]cZxid = 0x0 ctime = Thu Jan 01 08:00:00 CST 1970 mZxid = 0x0 mtime = Thu Jan 01 08:00:00 CST 1970 pZxid = 0x0 cversion = -1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 0 numChildren = 1 (1)czxid:创建节点的事务 zxid 每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。 (2)ctime:znode 被创建的毫秒数(从 1970 年开始) (3)mzxid:znode 最后更新的事务 zxid (4)mtime:znode 最后修改的毫秒数(从 1970 年开始) (5)pZxid:znode 最后更新的子节点 zxid (6)cversion:znode 子节点变化号,znode 子节点修改次数 (7)dataversion:znode 数据变化号,znode数据修改次数 (8)aclVersion:znode 访问控制列表的变化号 (9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。 (10)dataLength:znode 的数据长度 (11)numChildren:znode 子节点数量
3.4 Zookeeper节点类型
-
zookeeper包含以下4种类型节点:
-
创建永久不带序号节点(持久化目录节点)操作
# 创建sanguo节点,描述信息为diaochan create /sanguo "diaochan" # 在sanguo节点下创建shuguo节点,描述信息为liubei create /sanguo/shuguo "liubei"
-
获取节点的具体描述信息
# 获取/sanguo节点的具体描述信息 get -s /sanguo diaochan # 描述信息 cZxid = 0x20000000a # 创建节点时的事务id ctime = Fri Jul 22 16:18:25 CST 2022 # 创建时间 mZxid = 0x20000000a # 最后更新的事务id mtime = Fri Jul 22 16:18:25 CST 2022 # 最后修改时间 pZxid = 0x20000000b # 子节点的事务id cversion = 1 # 子节点变化号 dataVersion = 0 # 数据变化号 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 8 # 数据长度 numChildren = 1 # 子节点个数
-
创建永久带序号节点(持久化顺序编号目录节点)
# 在/sanguo/weiguo节点下创建zhangliao永久带序号节点,创建后会自动生成序号0000000000,如果执行操作第二次的话,会在节点名后面生成序号0000000001 create -s /sanguo/weiguo/zhangliao "zhangliao" Created /sanguo/weiguo/zhangliao0000000000
-
创建临时不带序号节点,退出客户端后会自动删除节点
# 在sanguo节点下创建临时节点wuguo create -e /sanguo/wuguo "zhouyu"
-
创建临时带序号节点,退出客户端后会自动删除节点
# 在sanguo节点下创建临时带序号节点wuguo,会自动生成序号 create -e -s /sanguo/wuguo "zhouyu" Created /sanguo/wuguo0000000003
-
修改节点数据值
# 更改weiguo节点的数据值 set /sanguo/weiguo "simayi"
3.5 监听器原理
-
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目 录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数 据的任何改变都能快速的响应到监听了该节点的应用程序。
-
监听节点数据变化:
在hadoop104主机上注册监听/sanguo 节点数据变化get -w /sanguo
在hadoop103修改sanguo节点的数据,hadoop104会收到监听,但是如果多次修改,只会收到一次监听,因为注册 一次,只能监听一次。想再次监听,需要再次注册。
WATCHER:: WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo
-
节点的子节点变化监听(路径变化):
在 hadoop104 主机上注册监听/sanguo 节点的子节点变化ls -w /sanguo
在 hadoop103 主机/sanguo 节点上创建子节点,hadoop104会收到子节点变化的监听,但是如果多次创建,也是只会收到一次监听,想再次监听,需要再次注册。
WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo
-
删除节点
# 删除节点 delete /sanguo/jin # 递归删除节点 deleteall /sanguo/shuguo # 查看节点状态 stat /sanguo
3.6 客户端API操作
-
环境搭建
1)新建maven工程zookeeper
2)pom文件添加依赖<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>RELEASE</version> </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> </dependencies>
3)拷贝log4j.properties文件到src/main/resources
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
4)创建包com.F.zk
5)创建类zkClient
-
创建zookeeper客户端,zookeeper对象中的process方法在zookeeper对象调用各种方法请求服务器时会调用
package com.F.zk; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import org.junit.Test; import java.io.IOException; public class zkClient { // 注意:逗号左右不能有空格 private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181"; private int sessionTimeout = 2000; private ZooKeeper zkClient; @Test public void init() throws IOException { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } }
-
创建子节点,调用zookeeper客户端对象的create方法,需要传入4个参数,分别是节点路径,节点数据,节点权限,节点类型
package com.F.zk; import org.apache.zookeeper.*; import org.junit.Before; import org.junit.Test; import java.io.IOException; public class zkClient { // 注意:逗号左右不能有空格 private String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181"; private int sessionTimeout = 400000; private ZooKeeper zkClient; @Before public void init() throws IOException { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } @Test public void create() throws InterruptedException, KeeperException { String nodeCreated = zkClient.create("/F", "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } }
-
获取子节点并监听节点变化,调用zookeeper客户端对象的getChildren方法,需要传入监听节点路径和监听器(也可以传入参数true,使用客户端对象自带的监听器对象),这里如果直接运行我们定义的getChildren函数,里面调用zkClient.getChildren,由于设置了getChildren第二个参数为true,所以会走zkClient对象的process方法,但是只会对服务器注册监听一次,这样的话当增加或删除节点时就不会打印输出节点变化,因此还需要在初始化方法中,创建zkClient对象时重写process方法重新注册监听,这样第一次运行getChildren方法后,走process方法的时候就会重新注册监听,就能够一直监听下去了。
package com.F.zk; import org.apache.zookeeper.*; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.util.List; public class zkClient { // 注意:逗号左右不能有空格 private String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181"; private int sessionTimeout = 400000; private ZooKeeper zkClient; @Before public void init() throws IOException { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { System.out.println("----------------------"); List<String> children = null; try { children = zkClient.getChildren("/", true); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } for (String child : children) { System.out.println(child); } } }); } @Test public void create() throws InterruptedException, KeeperException { String nodeCreated = zkClient.create("/F", "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } @Test public void getChildren() throws InterruptedException, KeeperException { List<String> children = zkClient.getChildren("/", true); for (String child : children) { System.out.println(child); } // 延时 Thread.sleep(Long.MAX_VALUE); } }
-
判断znode节点是否存在
package com.F.zk;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class zkClient {
// 注意:逗号左右不能有空格
private String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";
private int sessionTimeout = 4000000;
private ZooKeeper zkClient;
@Before
public void init() throws IOException {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// System.out.println("----------------------");
// List<String> children = null;
// try {
// children = zkClient.getChildren("/", true);
// } catch (KeeperException e) {
// e.printStackTrace();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// for (String child : children) {
// System.out.println(child);
// }
}
});
}
@Test
public void create() throws InterruptedException, KeeperException {
String nodeCreated = zkClient.create("/F", "abc".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
@Test
public void getChildren() throws InterruptedException, KeeperException {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
// 延时
Thread.sleep(Long.MAX_VALUE);
}
@Test
public void exist() throws InterruptedException, KeeperException {
Stat stat = zkClient.exists("/F", false);
System.out.println(stat == null ? "no exist" : "exist");
}
}
3.7 客户端向服务端写数据原理
-
客户端的写入请求直接发送给Leader节点的流程
- clent向leader节点发送写入请求
- leader节点进行写数据并同时将写请求通知给follower节点
- follower节点向leader节点发送写成功确认信息
- 当有半数以上节点写成功后,leader节点向客户端发送写成功确认信息
- 剩余follower节点继续执行写操作,并向leader节点发送信息
-
客户端的写入请求直接发送给follower节点的流程
- client向follower节点发送写入请求
- follower节点转发写入请求给leader节点
- leader节点进行写数据并同时把写请求转发给follower
- follower节点写完后会给leader节点一个应答
- 当有半数以上节点写成功后,leader节点向follower节点发送写成功应答
- follower节点向client发送写成功应答
- 剩余follower节点继续执行写操作,并向leader节点发送信息
4 服务器动态上下线监听案例
-
需求:某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知 到主节点服务器的上下线。
-
服务器上线:即服务器可以对外提供服务,在zookeeper集群上创建对应节点,节点信息为服务器主机名称以及当前连接的客户端数等
-
流程:
1)服务端启动时向zookeeper注册信息(注册临时节点)(服务器上线)
2)客户端获取当前在线服务器列表并注册监听
3)服务器节点与zookeeper集群失去联系(服务器节点下线,临时节点删除)
4)客户端收到服务器节点下线通知,执行相应操作并再次注册监听
-
服务器跟客户端对zookeeper集群来说都是客户端,上面的客户端(服务器)是到zookeeper集群上创建节点,下面的客户端(用户)是对zookeeper集群的一个监听。
-
具体实现:
(1)先在集群上创建/servers 节点
(2)在 Idea 中创建包名:com.atguigu.zkcase1
(3)服务器端向 Zookeeper 注册代码package com.F.case1; import org.apache.zookeeper.*; import java.io.IOException; public class DistributeServer { private String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181"; private int sessionTimeout = 4000000; private ZooKeeper zk; public static void main(String[] args) throws IOException, InterruptedException, KeeperException { DistributeServer server = new DistributeServer(); // 1 获取zk连接 server.getConnect(); // 2 注册服务器到zk集群(创建节点) server.regist(args[0]); // 3 启动业务逻辑(等待触发) server.buisness(); } private void buisness() throws InterruptedException { Thread.sleep(Long.MAX_VALUE); } private void regist(String hostname) throws InterruptedException, KeeperException { // 创建带序号临时节点 String create = zk.create("/servers/" + hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); System.out.println(hostname + " is online"); } private void getConnect() throws IOException { zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } }
(4)客户端代码
package com.F.case1; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class DistributeClient { private String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181";; private int sessionTimeout = 400000; private ZooKeeper zk; public static void main(String[] args) throws IOException, InterruptedException, KeeperException { DistributeClient client = new DistributeClient(); // 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 InterruptedException, KeeperException { 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 watchedEvent) { try { // 重新注册监听 getServerList(); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } }); } }
(5)测试,启动客户端监听,再测试服务端
5 Zookeeper分布式锁案例
-
什么叫做分布式锁呢? 比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其 他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的 访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。
-
zookeeper分布式锁案例流程:
-
代码实现
创建DistributedLock类,定义了连接、加锁、解锁操作package com.F.case2; 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; public class DistributedLock { private final String connectString = "192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181"; private final int sessionTimeout = 400000; private final ZooKeeper zk; private CountDownLatch connectLatch = new CountDownLatch(1); private CountDownLatch waitLatch = new CountDownLatch(1); private String waitPath; private String currentMode; public DistributedLock() throws IOException, InterruptedException, KeeperException { // 获取连接 zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { // connectLatch 如果连接上zk可以释放 if (watchedEvent.getState() == Event.KeeperState.SyncConnected) { connectLatch.countDown(); } // waitLatch需要释放 if (watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)) { waitLatch.countDown(); } } }); // 等待zk正常连接后,往下走程序 connectLatch.await(); // 判断集群根节点locks是否存在 Stat stat = zk.exists("/locks", false); if (stat == null) { // 创建/locks节点 zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } // 对zk加锁 public void zkLock() { // 创建对应的临时带序号节点 try { currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 判断创建的节点是否序号最小的节点,如果是,则获取到锁,如果不是,则监听序号前一个节点 List<String> children = zk.getChildren("/locks", false); // 如果children只有一个值,则直接获取锁,如果有多个阶段,需要判断谁最小 if (children.size() == 1) return; else { // 对children排序 Collections.sort(children); // 获取节点名称, 去掉/locks/,只取后面的节点名称 String thisNode = currentMode.substring("/locks/".length()); // 通过当前节点名称获取该节点再children中的位置 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 (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } // 解锁 public void unZkLock() { // 删除节点 try { zk.delete(currentMode, -1); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } }
测试分布式锁,lock1和lock2模拟客户端对zookeeper集群的两个请求,分别进入到zkLock函数中会创建各自对应的临时带序号节点,序号为小的节点可以直接通过zkLock方法,调用线程等待方法模拟获取资源操作,然后再调用unZkLock方法删除临时节点;序号大的节点会走到zkLock方法中最后一部分,监听另一个节点的情况,通过getData方法来访问到zookeeper对象的process方法,从而释放watchLatch,然后就能通过zkLock方法,调用线程等待方法模拟获取资源操作,然后调用unZkLock方法删除临时节点(释放watchLatch相当于第一个请求已经释放资源解除锁了)。
package com.F.case2; import org.apache.zookeeper.KeeperException; import java.io.IOException; public class DistributedLockTest { public static void main(String[] args) throws IOException, InterruptedException, KeeperException { final DistributedLock lock1 = new DistributedLock(); final DistributedLock lock2 = new DistributedLock(); new Thread(new Runnable() { @Override public void run() { lock1.zkLock(); System.out.println("线程1启动,获取到锁"); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } lock1.unZkLock(); System.out.println("线程1释放锁"); } }).start(); new Thread(new Runnable() { @Override public void run() { lock2.zkLock(); System.out.println("线程2启动,获取到锁"); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } lock2.unZkLock(); System.out.println("线程2释放锁"); } }).start(); } }
-
Curator框架实现分布式锁案例:
1)原生的 Java API 开发存在的问题
(1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
(2)Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归
2)Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。 详情请查看官方文档:https://curator.apache.org/index.html -
Curator案例实操
(1)添加依赖<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>
(2)代码实现,通过框架来连接到zookeeper集群,并且实现分布式锁
package com.F.case3; 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; public class CuraotrLockTest { public static void main(String[] args) { // 创建分布式锁1 InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks"); // 创建分布式锁2 InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks"); new Thread(new Runnable() { @Override public void run() { try { lock1.acquire(); System.out.println("线程1 获取到锁"); lock1.acquire(); System.out.println("线程1 再次获取到锁"); Thread.sleep(5 * 1000); lock1.release(); System.out.println("线程1 释放锁"); lock1.release(); System.out.println("线程1 再次释放锁"); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { lock2.acquire(); System.out.println("线程2 获取到锁"); lock2.acquire(); System.out.println("线程2 再次获取到锁"); Thread.sleep(5 * 1000); lock2.release(); System.out.println("线程2 释放锁"); lock2.release(); System.out.println("线程2 再次释放锁"); } catch (Exception e) { e.printStackTrace(); } } }).start(); } private static CuratorFramework getCuratorFramework() { // 设置连接失败重试策略,3秒后重试3次 ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3); CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.10.102:2181,192.168.10.103:2181,192.168.10.104:2181") .connectionTimeoutMs(400000) .sessionTimeoutMs(400000) .retryPolicy(policy).build(); // 启动客户端 client.start(); System.out.println("zookeeper启动成功"); return client; } }
6 企业面试真题
- 选举机制:半数机制,超过半数的投票通过,即通过。
(1)第一次启动选举规则: 投票过半数时,服务器 id 大的胜出
(2)第二次启动选举规则: ①EPOCH 大的直接胜出 ②EPOCH 相同,事务 id 大的胜出 ③事务 id 相同,服务器 id 大的胜出 - 生产集群安装多少 zk 合适?
安装奇数台。
生产经验: 10 台服务器:3 台 zk; 20 台服务器:5 台 zk; 100 台服务器:11 台 zk; 200 台服务器:11 台 zk 服务器
台数多:好处,提高可靠性;坏处:提高通信延时 - 常用命令:ls、get、create、delete