一、Zookeeper学习要点
Zookeeper是一个基于观察者模式设计的分布式管理框架,为分布式框架提供协调服务的 Apache 项目。
1、Zookeeper工作机制
1)分为三部分:
①ZooKeeper集群(负责管理)
②服务器(具有业务功能)
③客户端
本质上②和③对于ZooKeeper 集群而言都是一样的,是客户。只是需要存储和反馈的信息不同。
2) ZooKeeper = 文件系统 + 通知系统
文件系统:负责存储和管理大家都关心的数据。存储的数据量很小,只存储节点的路径,名称等关键信息。
通知系统:接受观察者的注册,一旦数据的状态发生变化,ZooKeeper将负责通知已经在ZooKeeper上注册的那些观察者做出相应的反应。
2、Zookeeper特点
1)一个领导者(Leader),多个跟随者(Follower)组成的集群。
2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所 以Zookeeper适合安装奇数台服务器。(半数以上决定了Zookeeper适合安装奇数台服务器,偶数台会造成服务器资源的浪费)
3)全局数据一致性,写入数据需要Leader的权限。每个服务器都保持一份相同的数据副本。
4)更新数据顺序执行,同一个客户端(Client)的更新请求按照请求的顺序进行。
5)数据更新原子性,要么成功,要么失败,不存在部分更新。
6)实时性,一定时间内Client读取到的数据最新,服务器上下线的状态最新。
7)ZooKeeper的数据结构是树形结构,每个节点看作一个ZkNode,可存1MB数据,且路径唯一,路径作为唯一标识。
3、应用场景
统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。
4、选取机制(假设共5台服务器)
1)第一次启动集群进行Leader选举
严格遵循半数原则:
① 服务器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更改状态为FOLLOWING,服务器3更改状态为LEADING;
④ 服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为 1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
⑤ 服务器5启动,同4一样当小弟。
2)非第一次选取
<1> 当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
• 服务器初始化启动。• 服务器运行期间无法和Leader 保持连接。<2>选举Leader规则:①EPOCH大的直接胜出。②EPOCH相同,事务id大的胜出。③事务id相同,服务器id大的胜出。【注】:·EPOCH是指每个Leader的代号。· 事务id(ZXID)在某个时刻,每台服务器的ZXID不一定一致,跟更新请求后的更新逻辑有关系,操作或则数据在某一时刻不是绝对一致的。·服务器id(SID),用来唯一标识Zookeeper集群中的服务器,在集群中每台服务器SID唯一。
5、客户端向服务器端写数据的流程
1)情况一:客户端(Client)连接的是ZooKeeper集群中的Leader服务器①Client向Leader发送请求,Leader先自己更新数据。②随后Leader向别的服务器 Follower发送更新数据的命令, Follower更新后向Leader回馈消息已经更新完数据。③Leader检查是否已经更新完 半数 服务器的数据,如果 超过半数 ,向Client回馈消息,已经更新完Zookeeper集群中的数据。④如果没有超过半数Leader选择别的Follower继续执行②,直到超过半数执行③,最后继续执行更新剩下的Follower服务器。2)情况二:客户端(Client)连接的是ZooKeeper集群中的Follower服务器①Client向 Follower发送请求, Follower向Leader发送更新请求。②Leader收到请求后,先更新自己的数据。更新后,向Client请求的 Follower服务器发送更新请求,Follower接收到请求更新数据,然后向Leader反馈已更新数据。③如果此时Zookeeper中超过半数服务器更新完数据,Leader向Client连接的Fllower反馈信息,集群数据已更新完毕,此Fllower向Client回馈已更新完数据。③如果没有超过半数,Leader选择别的Follower继续执行更新请求,如果超过半数时执行③,随后继续更新剩下的Fllower的数据。
二、API编写
6、服务器动态上下线监听案例
create /servers "servers"
import org.apache.zookeeper.*;
import java.io.IOException;
public class DistributeServer {
private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout=2000;
private ZooKeeper zk;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
//服务端在Zookeeper上进行注册
//1、获取zk链接
//用DistributeServer类对整体进行封装
DistributeServer server = new DistributeServer();
server.getconnect();
//2、利用zk链接注册服务器信息(创建节点)
server.registServer(args[0]);//args[0] 手动传 实参
//3、启动业务功能(目前没有功能,我们们执行sleep()睡觉)
server.business();
}
//业务功能
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
//在zk注册服务器(就是在父节点下面创建新的子节点)
private void registServer(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.");
}
//链接zk集群
private void getconnect() throws IOException {
//把zk做全局变狼
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
}//DistributeServer
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 DistributeCliient {
private String connectString="hadoop102:2181,hadoop103:2181,hadoop104:2181";
private int sessionTimeout=2000;
private ZooKeeper zk;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributeCliient Client = new DistributeCliient();
//1、获取zk链接
Client.getconnect();
//2、获取servers的子节点信息。从中得到服务器信息的列表
Client.getServersList();
//3、业务进程启动
Client.business();
}
//业务进程
private void business() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
//获取服务器列表的信息
private void getServersList() throws InterruptedException, KeeperException {
//1获取服务器节点信息,对父节点(指定的节点"/servers",需要先在ZK集群上创建该父节点)进行监听
List<String> children = zk.getChildren("/servers", true);//监听
//2存储服务器的列表信息
ArrayList<Object> servers = new ArrayList<>();
//3遍历所有节点,获取节点中的主机名称信息
for (String child : children) {
//不在监听,已经监听父节点,不返回状态信息
byte[] data = zk.getData("/servers/" + child, false, null);
//添加到列表中,循环添加
servers.add(new String(data)); //注意类型转换
}
//4打印列表
System.out.println(servers);
}
//链接ZK集群
private void getconnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
//多次监听
@Override
public void process(WatchedEvent watchedEvent) {
try {
getServersList();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
});
}
}
7、ZooKeeper 分布式锁案例 (Java API实现)
·什么叫做分布式锁呢?比如说 " 进程 1" 在使用该资源的时候,会先去获得锁, " 进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源, " 进程 1" 用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。·实现逻辑:多个客户端Client连接ZooKeeper集群,发送申请锁的请求① 服务器 接收到请求后,在 /locks 节点下创建一个临时顺序节点。②判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听。③获取到锁,处理完业务后,delete 节点释放锁,然后下面的节点将收到通知,重复第二步判断。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class DistrabuteLocks {
private final String connectString="hadoop102:2181,hadoop103:2181,hadoop104:2181";
private final int sessionTimeout=2000;
private final ZooKeeper zk;
//增加代码的健壮性
private CountDownLatch connectLatch = new CountDownLatch(1);
private CountDownLatch waitLatch =new CountDownLatch(1);
private String lastNodePath; //当前节点的前一个节点的路径
private String currentNode;
//1、获取链接,连接上ZK
public DistrabuteLocks() throws IOException, InterruptedException, KeeperException {
//1)获取链接
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(lastNodePath))
waitLatch.countDown();//释放
}
});
//2)等待连接上zk,再进行后续操作,相当于阻塞,等待前序进程完成
connectLatch.await();
//3)判断根节点"/locks"是否存在,不需监听,获取到status(状态)
Stat stat = zk.exists("/locks", false);
if (stat == null){
//需要创建根节点
zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
//2、对ZK加锁
public void zklock(){
try {
//创建临时带序号的节点
currentNode = zk.create("/locks/seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//判断创建的节点是不是最小序号的节点,如果是获取到锁,如果不是需要监听当前序号的前一个节点
List<String> children = zk.getChildren("/locks", false);
//如果"/locks"下只有一个节点,直接获取锁,如果多个节点,找出最小的序号获取锁
if (children.size() == 1)
return;//直接返回就是,获取锁
else {
//先排序
Collections.sort(children);
//获取当前节点currentNode的定位是第几个,先获取当前节点的名称
String thisNodeName = currentNode.substring("/locks/".length());
int index = children.indexOf(thisNodeName);
//判断thisNodeName是哪个位置
if (index == -1 ){
System.out.println("数据异常~~~~");
}else if (index == 0) {
return;//只有一个数据,就是当前节点获取锁
}else {
//不是第一个节点,需要监听前一个节点的状态
//前一个节点的路径
lastNodePath = "/locks/"+ children.get(index -1);
//获取前一个节点的数据变化,达到监听的效果
zk.getData(lastNodePath,true,null);
//增加代码的健壮性,等待监听
waitLatch.await();
//接听结束
return;//获取锁
}
}
} catch (KeeperException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//3、解锁
public void unzklock(){
//删除节点
try {
zk.delete(currentNode,-1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
}
2)分布式锁测试
import org.apache.zookeeper.KeeperException;
import java.io.IOException;
//对DistrabuteLocks进行测试,创建两个节点,查看运行状态
public class LocksTest {
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
//先创建需要测试的DistrabuteLocks 类
final DistrabuteLocks locks1 = new DistrabuteLocks();
final DistrabuteLocks locks2 = new DistrabuteLocks();
final DistrabuteLocks locks3 = new DistrabuteLocks();
//线程1
new Thread(new Runnable() {
@Override
public void run() {
try {
//加锁(创建节点)
locks1.zklock();
System.out.println("线程1 启动~~,获取到锁。");
//增加延时5s
Thread.sleep(5*1000);
//延时过后释放锁
locks1.unzklock();
System.out.println("线程1 结束~~,释放锁。");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
try {
//加锁(创建节点)
locks2.zklock();
System.out.println("线程2 启动~~,获取到锁。");
//增加延时5s
Thread.sleep(5*1000);
//延时过后释放锁
locks2.unzklock();
System.out.println("线程2 结束~~,释放锁。");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
//线程3
new Thread(new Runnable() {
@Override
public void run() {
try {
//加锁(创建节点)
locks3.zklock();
System.out.println("线程3 启动~~,获取到锁。");
//增加延时5s
Thread.sleep(5*1000);
//延时过后释放锁
locks3.unzklock();
System.out.println("线程3 结束~~,释放锁。");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}//main
}
8、Curator 框架实现分布式锁案例
1 )原生的 Java API 开发存在的问题( 1 )会话连接是异步的,需要自己去处理。比如使用 CountDownLatch( 2 ) Watch 需要重复注册,不然就不能生效( 3 )开发的复杂性还是比较高的( 4 )不支持多节点删除和创建。需要自己去递归
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.BoundedExponentialBackoffRetry;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class CuratorLockTest {
private static String NodePath= "/locks";
private static String connectAdress="hadoop102:2181,hadoop103:2181,hadoop104:2181";
public static void main(String[] args) {
//1、创建分布式锁
//使用Curator的API进行编写
//创建分布式锁1,使用getCuratorClient()方法创建客户端
InterProcessMutex lock1 = new InterProcessMutex(getCuratorClient(), NodePath);
//创建分布式锁2
InterProcessMutex lock2 = new InterProcessMutex(getCuratorClient(), NodePath);
//创建分布式锁3
InterProcessMutex lock3 = new InterProcessMutex(getCuratorClient(), NodePath);
//2、测试
//线程1
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);//延时5s
lock1.release();//释放锁
System.out.println("线程1释放锁~~~");
lock1.release();//释放锁
System.out.println("线程1再次释放锁~~~");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();//启动线程
//线程2
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);//延时5s
lock2.release();//释放锁
System.out.println("线程2释放锁~~~");
lock2.release();//释放锁
System.out.println("线程2再次释放锁~~~");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();//启动线程
//线程3
new Thread(new Runnable() {
@Override
public void run() {
try {
lock3.acquire();//获取到锁
System.out.println("线程3获取到锁~~~");
lock3.acquire();//获取到锁
System.out.println("线程3再次获取到锁~~~");
Thread.sleep(5*1000);//延时5s
lock3.release();//释放锁
System.out.println("线程3释放锁~~~");
lock3.release();//释放锁
System.out.println("线程3再次释放锁~~~");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();//启动线程
}
//编写获取Curator客户端的方法
private static CuratorFramework getCuratorClient() {
ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);
//创建客户端
CuratorFramework client = CuratorFrameworkFactory.builder().connectString(connectAdress) //链接地址
.connectionTimeoutMs(2000) //链接超时时间设置
.sessionTimeoutMs(2000) //心跳时间设置
.retryPolicy(policy) //重新试链接次数
.build();//客户端创建
//启动客户端
client.start();
System.out.println("ZooKeeper 客户端启动成功!~~");
return client; //返回客户端
}
}