ZooKeeper
ZooKeeper简介
是一个基于观察者模式设计的分布式服务管理框架,它负责管理和存储大家都关心的数据,然后接收观察者的注册,一旦数据发生变化,就会通知在ZooKeeper注册的观察。 作出相应的反应。
ZooKeeper特点
- 一个Learder,多个follower组成的集群,主写从读。
- 集群中只要有半数以上的机器存活就可以工作,所以一般适合搭建奇数台服务器。(若此处搭建六台,挂断2或者3台,整个集群都不能工作。所以多搭建一台并没有保证高可用)
- 每个server的数据都保持一致,Client无论连接到那台Server,访问的数据都一样。
- 更新请求顺序执行,好比Server1,那个client先来那个client先被处理。
- 数据更新原子性,更新存在事务,要么成功,要么失败。
- 实时性,在一定时间范围内(同步数据需要较短时间),每个client能读到最新的数据。
ZooKeeper数据结构
Zookeeper的数据结构模型和unix的文件系统类似,整体是一棵树,每个节点称作ZNode,每个ZNode可以存储数据,默认为1MB,并且每个ZNode可以通过路径作为唯一标识。
应用场景
1) 统一命名服务 ,在分布式环境下,需要对域名与机器统一命名。
2) 统一配置管理,配置文件的同步,每个client监听该node,当配置发生变更时,各个client同步配置。
3)统一集群管理,客户端1号向ZNode节点注册客户端信息(如ip,是否存活等等),其他客户端在该ZNode注册,可监听到客户端1号的状态。当然,客户端1号也可以注册其他ZNode监控其他客户端信息。
4)服务器动态上下线
5)软负载均衡,让访问数最少的服务器去处理请求。
ZooKeeper选举机制
相关概念:
SID:服务器id,用来标识一台ZooKeeper集群中的机器,是唯一标识和myid一样。
ZXID:事务id,用来标识一次服务器状态的变更。在某一时候,集群中的每台机器的ZXID不一定完全一致,这和客户端与服务器的更新请求有关。客户端对ZooKeeper服务器每次写操作都有事务,所产生的id。
Epoch:每个learder任期的代号
假设有五台服务器(每台服务器持有一张票数)。分别为server1 ,myid=1。server2 myid=2。。。
服务器初始化启动
server1启动先给自己投一票,然后处于locking状态。server2启动同样先是给自己投一票。
这时,server1和server2各持一票,然后那个myid大,就把票数给myid大的服务器。
同理,server3启动,也是给自己一票,然后与server2比较,myid大,于是server2把两票给了server3。server3因为有了3票(超过总服务器一半票数)所以当选为learder,状态置为leading.server4启动自己会变成follower,状态为following。
服务器运行期间无法和Leader保持连接
当某台服务器和Leader保持不了连接,会认为Leader或者其他服务器挂掉,重新发起Leader选举。当前集权可能会处于两种状态
1)Leader存在
当该服务器试图去选举Leader时,会被告知Leader信息,对于该机器来说,只需要重新连接Leader即可。
2)Leader不存在
- 首先比较EPOCH大的胜出
- 若EPOCH相同,则ZXID大的胜出
- 若ZXID相同,则服务器ID大的相同
ZooKeeper节点
节点信息
##查看根节点信息
ls -s /
(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 数据变化号
(8)aclVersion:znode 访问控制列表的变化号
(9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是
临时节点则是 0。 (10)dataLength:znode 的数据长度
(11)numChildren:znode 子节点数量
节点类型
持久节点:客户端与服务端断开连接后,创建的节点不删除
- 持久节点都不带序号 /Znode1
- 持久节点带序号 /Znode2_001 /Znode3_002
短暂节点:客户端与服务端断开连接后,创建的节点自动删除 - 临时节点都不带序号 /Znode1
- 临时节点带序号 /Znode2_001 /Znode3_002
## 创建永久节点(不带序号)不可重复创建
create /testNode '描述信息'
create /testNode/test111 'testNode的子节点'
## 创建永久节点(带序号 -s)可重复创建 响应 : Created /testNode/test2220000000001
create -s /testNode/test222
## 创建临时节点 (带序号 -s)
create -e /testNode/tmp
## 查看节点
ls /
ls /testNode
## 获取节点
get /testNode/test111
get -s /testNode/test111
## 修改节点值
set /testNode/test111 '修改testNode的子节点'
## 删除节点
delete /testNode/test111/test11111
delteall /testNode
## 查看节点状态信息
stat /testNode/test111
监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目
录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数
据的任何改变都能快速的响应到监听了该节点的应用程序。
1、监听原理详解
1)首先要有一个main()线程
2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
3)通过connect线程将注册的监听事件发送给Zookeeper。
4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6)listener线程内部调用了process()方法。
2、常见的监听
1)监听节点数据的变化
get path [watch]
## 监控 test111的节点数据
get -w /TestNode/test111
再次修改,就不会再监听。一次注册,一次监听。再次注册,才能再次监听
2)监听子节点增减的变化
ls path [watch]
create /testNode/test111/test11111
ls -w /testNode/test111
写数据流程
1)客户端写数据请求发送到Leader
- 首先客户端发送请求值Leader接收
- Leader将数据自己先写一份,然后通知Follwer,让Follwer写数据
- Follwer写完数据,返回ack
- 只要有超过一半机器写完数据,Leader就会返回ack给客户端
- 继续通知其他Follwer写数据,其他机器写完回复ack。
2)请求发送到Follwer
- 请求发送到Follower,Follwer会将请求转发给Leader
- Leader会通知Follwer写入数据,Follower写完数据恢复ack
- 当有超过一半机器写完数据,Leader会回复ack给接收请求的Follwer
- 接收请求的Follwer回复ack给Client
- 最后继续让其他未完成同步操作的机器写数据,写完回复ack即完成。
代码实现
<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>
<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>
</dependencies>
package com.example.demo.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;
/**
* @author huikf
*/
public class ZookeeperClient {
private static final String CONNECT = "127.0.0.1:2181";
private static final int TIME_OUT = 2_000;
private ZooKeeper zooKeeper;
@Before
public void init() throws IOException {
zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, (Watcher) watchedEvent -> {
try {
List<String> children = zooKeeper.getChildren("/", true);
children.forEach(System.out::println);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
});
}
@Test
public void create() throws KeeperException, InterruptedException {
String s = zooKeeper.create("/family", "hkf".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(s);
}
/**
* watch参数为true,代表使用默认监听器实现,即init中的watch
* 但也可以自己再实现一个
* 当主线程未结束时,init中的watch一直监听变化
*/
@Test
public void getChildren() throws KeeperException, InterruptedException {
List<String> children = zooKeeper.getChildren("/", true);
children.forEach(System.out::println);
Thread.sleep(Long.MAX_VALUE);
}
@Test
public void exist() throws KeeperException, InterruptedException {
Stat exists = zooKeeper.exists("/family", false);
System.out.println(exists == null);
}
}
案例
服务器动态上下线
服务端
/**
* @author huikf
* @since 2021/9/11 9:44
*/
public class DistributeServer {
ZooKeeper zooKeeper;
private static final String CONNECT = "127.0.0.1:2181";
private static final int TIME_OUT = 2_000;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeServer distributeServer = new DistributeServer();
//初始化客户端
distributeServer.initClient();
//注册服务器,给arges[0] 赋值
distributeServer.register(args[0]);
//业务处理
distributeServer.bussiness();
}
private void bussiness() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
/**
* 创建/servers/host1
* /host2
* 节点类型为虚拟,有序号的,当某台服务器挂掉,节点就会消失。
*
* @param host 主机
* @author huikf
*/
private void register(String host) throws KeeperException, InterruptedException {
zooKeeper.create("/servers/"+host, host.getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("host:" + host + "is online");
}
private void initClient() throws IOException {
zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {
});
}
}
客户端
/**
* @author huikf
* @since 2021/9/11 9:53
*/
public class DistributeClient {
ZooKeeper zooKeeper;
private static final String CONNECT = "127.0.0.1:2181";
private static final int TIME_OUT = 2_000;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
DistributeClient distributeClient = new DistributeClient();
distributeClient.initClient();
//监听 /servers下面的子节点的增加或者删除
distributeClient.getServersList();
distributeClient.bussiness();
}
private void bussiness() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
private void getServersList() throws KeeperException, InterruptedException {
//true:代表使用初始化时默认的监听器,下行代码只监听一次,若想再次监听,须在初始化时的监听器中再次调用
List<String> children = zooKeeper.getChildren("/servers", true);
List<String> dataS = new ArrayList<>();
for (String child : children) {
byte[] data = zooKeeper.getData("/servers/" + child, false, null);
dataS.add(new String(data));
}
System.out.println(dataS);
}
private void initClient() throws IOException {
zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {
try {
getServersList();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
});
}
}
测试
## 增加/servers下节点或者删除节点 客户端均可监听到
create -e -s /servers/server1 'servers1'
create -e -s /servers/server2 'servers2'
create -e -s /servers/server3 'server3'
分布式锁
package com.example.demo.zk;
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;
/**
* @author huikf
* @since 2021/9/13 9:59
*/
public class DistributedLock {
/**
* zookeeper server 列表
*/
private String CONNECT = "127.0.0.1:2181";
/**
* 超时时间
*/
private int TIME_OUT = 2000;
private ZooKeeper zooKeeper;
private String rootNode = "locks";
private String subNode = "seq-";
/**
* 当前 client 等待的子节点
*/
private String waitPath;
/**
* ZooKeeper 连接
*/
private CountDownLatch connectLatch = new CountDownLatch(1);
/**
* ZooKeeper 节点等待
*/
private CountDownLatch waitLatch = new CountDownLatch(1);
/**
* 当前 client 创建的子节点
*/
private String currentNode;
/**
* 和 zk 服务建立连接,并创建根节点
*/
public DistributedLock() throws IOException,
InterruptedException, KeeperException {
zooKeeper = new ZooKeeper(CONNECT, TIME_OUT, event -> {
// 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
connectLatch.countDown();
}
// 发生了 waitPath 的删除事件
if (event.getType() == Watcher.Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
waitLatch.countDown();
}
});
// 等待连接建立
connectLatch.await();
//获取根节点状态
Stat stat = zooKeeper.exists("/" + rootNode, false);
//如果根节点不存在,则创建根节点,根节点类型为永久节点
if (stat == null) {
System.out.println("根节点不存在");
zooKeeper.create("/" + rootNode, new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
/**
* 加锁方法
*/
public void zkLock() {
try {
//在根节点下创建临时顺序节点,返回值为创建的节点路径
currentNode = zooKeeper.create("/" + rootNode + "/" + subNode,
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// wait 一小会, 让结果更清晰一些
Thread.sleep(10);
// 注意, 没有必要监听"/locks"的子节点的变化情况
List<String> childrenNodes = zooKeeper.getChildren("/" + rootNode, false);
// 列表中只有一个子节点, 那肯定就是 currentNode , 说明client 获得锁
if (childrenNodes.size() == 1) {
return;
} else {
//对根节点下的所有临时顺序节点进行从小到大排序
Collections.sort(childrenNodes);
//当前节点名称
String thisNode = currentNode.substring(("/" + rootNode + "/").length());
//获取当前节点的位置
int index = childrenNodes.indexOf(thisNode);
if (index == -1) {
System.out.println("数据异常");
} else if (index == 0) {
// index == 0, 说明 thisNode 在列表中最小, 当前client 获得锁
} else {
// 获得排名比 currentNode 前 1 位的节点
this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
// 在 waitPath 上注册监听器, 当 waitPath 被删除时,zookeeper 会回调监听器的 process 方法
zooKeeper.getData(waitPath, true, new Stat());
//进入等待锁状态
waitLatch.await();
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 解锁方法
*/
public void zkUnlock() {
try {
zooKeeper.delete(this.currentNode, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
测试:
package com.example.demo.zk;
import org.apache.zookeeper.KeeperException;
import java.io.IOException;
public class DistributedLockTest {
public static void main(String[] args) throws
InterruptedException, IOException, KeeperException {
// 创建分布式锁 1
final DistributedLock lock1 = new DistributedLock();
// 创建分布式锁 2
final DistributedLock lock2 = new DistributedLock();
new Thread(() -> {
// 获取锁对象
try {
lock1.zkLock();
System.out.println("线程 1 获取锁");
Thread.sleep(5 * 1000);
lock1.zkUnlock();
System.out.println("线程 1 释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
// 获取锁对象
try {
lock2.zkLock();
System.out.println("线程 2 获取锁");
Thread.sleep(5 * 1000);
lock2.zkUnlock();
System.out.println("线程 2 释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
Curator 框架实现分布式锁案例
1)原生的 Java API 开发存在的问题
(1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
(2)Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归
2)Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。
详情请查看官方文档:https://curator.apache.org/index.html
代码实现
/**
* @author huikf
* @since 2021/9/13 10:59
*/
public class CuratorLockTest {
public static void main(String[] args) {
InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");
new Thread(() -> {
try {
lock1.acquire();
System.out.println("线程一获取到锁");
//锁重入
lock1.acquire();
System.out.println("线程一再次获取到锁");
Thread.sleep(5_000);
lock1.release();
System.out.println("线程一释放锁");
lock1.release();
System.out.println("线程一再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
lock2.acquire();
System.out.println("线程二获取到锁");
lock2.acquire();
System.out.println("线程二再次获取到锁");
Thread.sleep(5_000);
lock2.release();
System.out.println("线程二释放锁");
lock2.release();
System.out.println("线程二再次释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private static CuratorFramework getCuratorFramework() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(2_000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
.connectionTimeoutMs(2_000)
.sessionTimeoutMs(2_000)
.retryPolicy(retryPolicy)
.build();
client.start();
return client;
}
}