目录
Zookeeper 简介
Zookeeper 安装
Zookeeper集群搭建
Java集成ZooKeeper
Zookeeper 监听节点
Zookeeper 实现分布锁
Zab协议
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
- ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
- ZooKeeper包含一个简单的原语集,提供Java和C的接口。代码版本中,提供了分布式独享锁、选举、队列的接口,其中分布锁和队列有Java和C两个版本
# 查看本地镜像
docker images
# 搜索ZooKeeper 镜像
docker search zookeeper
# 拉取ZooKeeper镜像最新版本
docker pull zookeeper:latest
# 自己使用的版本
docker pull zookeeper:3.5.7
创建docker的挂载在本地的目录
mkdir -p /mydata/zookeeper/data # 数据挂载目录
mkdir -p /mydata/zookeeper/conf # 配置挂载目录
mkdir -p /mydata/zookeeper/logs # 日志挂载目录
启动zookeeper
docker run -d --name zookeeper --privileged=true -p 2181:2181 -v /mydata/zookeeper/data:/data -v /mydata/zookeeper/conf:/conf -v /mydata/zookeeper/logs:/datalog zookeeper:3.5.7
参数说明
-e TZ="Asia/Shanghai" # 指定上海时区
-d # 表示在一直在后台运行容器
-p 2181:2181 # 对端口进行映射,将本地2181端口映射到容器内部的2181端口
--name # 设置创建的容器名称
-v # 将本地目录(文件)挂载到容器指定目录;
--restart always #始终重新启动zookeeper,看需求设置不设置自启动
添加ZooKeeper配置文件,在刚刚配置的文件目录(/mydata/zookeeper/conf)下,新增zoo.cfg 配置文件(没有则需配置),配置内容如下:
dataDir=/data # 保存zookeeper中的数据
clientPort=2181 # 客户端连接端口,通常不做修改
dataLogDir=/datalog
tickTime=2000 # 通信心跳时间
initLimit=5 # LF(leader - follower)初始通信时限
syncLimit=2 # LF 同步通信时限
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
maxClientCnxns=60
standaloneEnabled=true
admin.enableServer=true
server.1=localhost:2888:3888;2181
常用的操作指令
# 进入zookeeper 容器内部
docker exec -it zookeeper /bin/bash
# 检查容器状态
docker exec -it zookeeper /bin/bash ./bin/zkServer.sh status
# 进入控制台
docker exec -it zookeeper zkCli.sh
ZooKeeper集群服中务中有三个角色:
- Leader (领导者) :1. 处理事务请求 2. 集群内部各服务器的调度者
- Follower (跟随者) :1. 处理客户端非事务请求,转发事务请求给Leader服务器 2. 参与Leader选举投票
- Observer (观察者):1. 处理客户端非事务请求,转发事务请求给Leader服务器。
Leader选举:
- Serverid:服务器ID :比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大
- Zxid:数据ID :服务器中存放的最大数据ID.值越大说明数据越新,在选举算法中数据越新权重越大
- 在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了
搭建要求
真实的集群是需要部署在不同的服务器上的,但是在测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建伪集群,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。我们这里搭建一个三个节点的Zookeeper集群(伪集群)
导入依赖
org.apache.curator
curator-framework
4.0.0
org.apache.curator
curator-recipes
4.0.0
package eample;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
@Slf4j
public class Zookeeper {
private CuratorFramework client;
/**
* zookeeper连接启动
*/
@Before
public void start() {
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);//失败后休息时间,重试次数
client = CuratorFrameworkFactory.builder()
.connectString("120.****.58:2181")//连接地址
.sessionTimeoutMs(60 * 1000)//会话超时时间 单位ms
.connectionTimeoutMs(15 * 1000)//连接超时时间 单位ms
.retryPolicy(retryPolicy)//重试策略
.namespace("chw")//名称空间
.build();
//开启连接
client.start();
}
/**
* 创建节点:create 持久、临时、顺序 给节点设置数据
* 1、基本创建:create().forPath("");
* 2、创建节点,带有数据:create().forPath("",data);
* 3、设置节点的类型:create().withMode(CreateMode.EPHEMERAL).forPath("",data);
* 4、创建多级节点 /app1/p1:create().creatingParentsIfNeeded().forPath("",data);
*/
@Test
public void create() throws Exception {
//1、基本创建
//如果创建节点、没有指定数据,则默认将当前客户端的ip作为数据存储
String forPath = client.create().forPath("节点一");
System.out.println(forPath);
//如果创建带数据的节点
forPath = client.create().forPath("节点一","测试节点一".getBytes());
System.out.println(forPath);
//如果创建带数据的节点,临时目录节点, 一旦创建这个节点当会话结束, 这个节点会被自动删除
forPath = client.create().withMode(CreateMode.EPHEMERAL).forPath("节点一","测试节点一".getBytes());
System.out.println(forPath);
//创建多级节点 /app1/p1,creatingParentsIfNeeded():如果父节点不存在,则创建父节点
forPath = client.create().creatingParentsIfNeeded().forPath("/节点2/p1");
System.out.println(forPath);
}
/**
* 查询节点:
* 1、查询数据:get:getData().forPath("");
* 2、查询子节点:ls:getChildren().forPath("/");
* 3、查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath("");
*/
@Test
public void select() throws Exception {
//1、查询数据
byte[] data = client.getData().forPath("/app1");
System.out.println(new String(data));
//2、查询子节点:
List forPath = client.getChildren().forPath("/");
System.out.println(forPath);
//3、查询节点状态信息
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("");
System.out.println(status);
}
/**
* 修改节点数据
* 1、修改数据:setData().forPath()
* 2、根据版本修改:setData().withVersion().forPath()
* version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。
* @throws Exception
*/
@Test
public void update() throws Exception {
//修改数据
client.setData().forPath("/节点一","itlovo".getBytes());
Stat status = new Stat();
//根据版本修改
client.getData().storingStatIn(status).forPath("/节点一");
int version = status.getVersion();
System.out.println(version);
client.setData().withVersion(version).forPath("/节点一","hehe".getBytes());
}
/**
* 删除节点:delete
* 1、删除单个节点:delete().forPath("/app1");
* 2、删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
* 3、必须成功的删除:为了防止网络抖动。本质就是重试。 client.delete().guaranteed().forPath("/app2");
* 4、回调:inBackground
* @throws Exception
*/
@Test
public void delete() throws Exception{
//1、删除单个节点
client.delete().forPath("/节点一");
//2、删除带有子节点的节点
client.delete().deletingChildrenIfNeeded().forPath("/节点一");
//3、必须成功的删除
client.delete().guaranteed().forPath("/节点一");
//4、删除回调
client.delete().guaranteed().inBackground((curatorFramework, curatorEvent) -> {
System.out.println("我被删除了~");
System.out.println(curatorEvent);
}).forPath("/节点一");
}
}
package eample;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.junit.Before;
import org.junit.Test;
@Slf4j
public class ZookeeperListener {
private CuratorFramework client;
/**
* zookeeper连接启动
*/
@Before
public void start() {
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);//失败后休息时间,重试次数
client = CuratorFrameworkFactory.builder()
.connectString("120.****.58:2181")//连接地址
.sessionTimeoutMs(60 * 1000)//会话超时时间 单位ms
.connectionTimeoutMs(15 * 1000)//连接超时时间 单位ms
.retryPolicy(retryPolicy)//重试策略
.namespace("chw")//名称空间
.build();
//开启连接
client.start();
}
/**
* NodeCache:给指定一个节点注册监听器
*/
@Test
public void listener() throws Exception {
//1、创建NodeCache对象
final NodeCache nodeCache = new NodeCache(client, "/节点一");
//2、注册监听
nodeCache.getListenable().addListener(() -> {
System.out.println("节点变化了...");
//获取修改节点后的数据
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
});
//3、开启监听,如果设置为true,则开启监听时,加载缓存数据
nodeCache.start(true);
while (true) {
}
}
/**
* 监控某一个节点ZNode的所有子节点们
*/
@Test
public void testPathChildrenCache() throws Exception {
//1、创建NodeCache对象
final PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true);
//2、绑定监听器
pathChildrenCache.getListenable().addListener((curatorFramework, pathChildrenCacheEvent) -> {
System.out.println("子节点变化了...");
//事件对象
System.out.println(pathChildrenCacheEvent);
//监听子节点的数据变更,并且拿到变更后的数据
//1、获取类型
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、开启监听
pathChildrenCache.start();
while (true) {
}
}
/**
* TreeCache监控某个节点自己和其所有的子节点们,类似于PathChildrenCache和NodeCache的组合
*/
@Test
public void testTreeCache() throws Exception {
//1、创建TreeCache对象
final TreeCache treeCache = new TreeCache(client, "/节点一");
//2、注册监听
treeCache.getListenable().addListener((curatorFramework, treeCacheEvent) -> {
System.out.println("节点或其子节点发生变化了");
System.out.println(treeCacheEvent);
});
//3、开启监听
treeCache.start();
while (true) {
}
}
}
Zookeeper分布式锁的原理
核心思想:当请求需要获取锁时,则创建节点,使用完锁,则删除该节点。节点能不能持久化,如果持久化节点,一旦获取锁的节点宕机了,会导致锁就不会被释放,节点不会被删除,如果节点不被删除,则其它节点就获取不到锁了,就会处于一直等待的状态;所以必须是临时的,即使宕机,临时节点也会在会话结束之后自动删除。
获取锁时,会在lock节点下创建临时顺序节点,顺序是因为要寻找最小节点,按照顺序去拿到锁,每次拿到的锁必须为节点序号最小的节点。
所以当获取到lock下面的所有子节点之后,会进行判断自己创建的子节点序号是否最小,如果是,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
当比自己小的那个节点被删除,Watcher会收到相应通知,此时会再次判断自己创建的节点是否是lock子节点中序号最小的,如果是,则获取到了锁,如果不是,则重复以上步骤继续获取到比自己小的一个节点并注册监听。
具体实现:
在Curator中有五种锁方案:
InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
InterProcessMutex:分布式可重入排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
InterProcessSemaphoreV2:共享信号量
模拟抢票功能
package eample;
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;
public class Ticket12306 implements Runnable {
private int tickets = 10;//数据库的票数
//锁
private InterProcessMutex lock;
public Ticket12306() {
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("120.***.58: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();
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();
}
}
}
}
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
//创建客户端
Thread t1 = new Thread(ticket12306, "携程");
Thread t2 = new Thread(ticket12306, "飞猪");
t1.start();
t2.start();
}
}
Zab协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议 ,是Zookeeper保证数据一致性的核心算法。
Zab借鉴了Paxos算法,但又不像Paxos那样,是一种通用的分布式一致性算法。它是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。
Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。
Zab 协议包括两种基本的模式:崩溃恢复 和 消息广播协议过程
当整个集群启动过程中,或者当 Leader 服务器出现网络中弄断、崩溃退出或重启等异常时,Zab协议就会 进入崩溃恢复模式,选举产生新的Leader。当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式,进入消息广播模式。这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,前32位是epoch用来标识 leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。后32位用于递增计数。
Zab协议的核心:定义了事务请求的处理方式
- 所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被叫做 Leader服务器。其他剩余的服务器则是 Follower服务器。
- Leader服务器 负责将一个客户端事务请求,转换成一个事务Proposal,并将该Proposal 分发给集群中所有的 Follower 服务器,也就是向所有 Follower 节点发送数据广播请求(或数据复制)
- 分发之后Leader服务器需要等待所有Follower服务器的反馈(Ack请求),在Zab协议中,只要超过半数的Follower服务器进行了正确的反馈后(也就是收到半数以上的Follower的Ack请求),那么 Leader 就会再次向所有的 Follower服务器发送 Commit 消息,要求其将上一个事务proposal 进行提交。