Watcher
概念
zookeeper提供了数据的发布/订阅功能,多个订阅者可以同时监听某一特定主题对象,当该主题对象的自身状态发生变化(例如结点数据改变、结点的子结点列表改变)时会实时主动通知所有订阅者。
zookeeper采用了watcher机制实现数据的发布/订阅功能,该机制在被订阅对象发生变化时会异步通知客户端,因此客户端不必在watcher注册后轮询阻塞,从而减轻了客户端的压力。
watcher机制实际上与观察者模式类似,也可以看作是一种观察者模式在分布式场景下的实现方式。
架构
watcher由三部分组成:zookeeper服务端、zookeeper客户端、客户端的ZKWatchManager对象。
客户端首先将watcher注册到服务端,同时将watcher对象保存到客户端的watch管理器中。当zookeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的watch管理器会触发相关watcher来回调相应的处理逻辑从而完成整体的数据发布/订阅流程。
特性
- 一次性
watcher是一次性的,一旦触发就会被移除,再次使用需要重新注册 - 客户端顺序回调
watcher回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。一个watcher的回调逻辑不应该太复杂,以免影响其他watcher - 轻量级
watchevent是最小的通信单元,结构上只包含通知状态、事件类型和结点路径,并不会告诉数据结点变化前后的具体内容 - 时效性
watcher只有在当前session彻底失效时才无效,若在session有效期内快速重连成功则watcher依然存在。
Watcher接口设计
Watcher是一个接口,任何实现了Watcher接口的类就是一个新的watcher,内部包含了两个枚举类KeeperState和EventType,以及回调方法process。
事件类型一共有五种,分别表示无、结点创建、结点删除、结点数据更改、结点子结点更改
当事件类型为None时,可以捕获连接状态,SysnConnected表示正常连接、Disconnected表示断开连接,Expired表示会话失效、AuthField表示身份认证失败。
zookeeper对象的exists方法可以监控结点的创建、数据更新和删除
getData可以监控结点的数据更新和删除
getChildren可以监控结点的子结点创建/删除和结点删除
watcher连接状态
首先记得关了防火墙!
在zookeeper安装目录的bin目录里打开终端,启动服务器
IDEA中创建自定义Watcher,需要实现process方法
public class ZKWatcher implements Watcher {
//创建计数器对象
static CountDownLatch countDownLatch=new CountDownLatch(1);
//创建连接对象
static ZooKeeper zooKeeper;
public static void main(String[] args) {
try{
zooKeeper=new ZooKeeper("192.168.2.142:2181",5000,new ZKWatcher());
//阻塞线程等待连接创建
countDownLatch.await();
System.out.println("话id为:"+zooKeeper.getSessionId());
Thread.sleep(5000);
zooKeeper.close();
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
try{
if(event.getType()== Event.EventType.None){
if(event.getState()==Event.KeeperState.SyncConnected) {
System.out.println("连接创建成功");
//解除线程阻塞
countDownLatch.countDown();
} else if(event.getState()==Event.KeeperState.Disconnected)
System.out.println("断开连接");
else if(event.getState()== Event.KeeperState.Expired)
System.out.println("会话超时");
else if(event.getState()== Event.KeeperState.AuthFailed)
System.out.println("认证失败");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
正常运行结果:
创建一个结点/tmp 授权给用户kobe,密码123456
然后使用错误的密码去获取/tmp
结果:
使用正确的密码:
使用exists方法监听
可以监听到结点创建、结点数据更新、结点删除
可以使用和zookeeper对象相同的watcher
public class ZKWatcherExists {
private static final String IP="192.168.2.142:2181";
private static ZooKeeper zooKeeper;
//创建一个计数器对象
CountDownLatch countDownLatch=new CountDownLatch(1);
@Before
public void connect() throws Exception{
//第一个参数是服务器ip和端口号,第二个参数是客户端与服务器的会话超时时间单位ms,第三个参数是监视器对象
zooKeeper=new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
//通知主线程解除阻塞
countDownLatch.countDown();
}
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
}
});
//主线程阻塞,等待连接对象的创建成功
countDownLatch.await();
}
@After
public void close() throws Exception{
zooKeeper.close();
}
@Test
public void exist1() throws Exception{
//第二个参数true表示复用zookeeper连接对象的监听器,就是main方法里的匿名内部类
zooKeeper.exists("/watcher1",true);
Thread.sleep(5000);
System.out.println("结束");
}
}
启动运行,然后在Linux端新建一个/watcher1,IDEA捕获到了创建的数据路径和类型,还可以捕捉到数据的改变和删除
也可以使用自定义的watcher
@Test
public void exist2() throws Exception{
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("使用了自定义watcher");
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
}
});
Thread.sleep(50000);
System.out.println("结束");
}
在Linux客户端删除/watcher1
由于watcher是一次性的,如果想要持续监听,必须重新注册
@Test
public void exist3() throws Exception{
Watcher watcher = event -> {
try {
System.out.println("监听到了数据:");
System.out.println("path = " + event.getPath());
System.out.println("eventType = " + event.getType());
zooKeeper.exists("/watcher1", (Watcher) this);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
};
zooKeeper.exists("/watcher1",watcher);
Thread.sleep(50000);
System.out.println("结束");
}
在Linux客户端创建结点并删除,都能被监听到。
也可以注册多个watcher
@Test
public void exist4() throws Exception{
zooKeeper.exists("/watcher1", event -> {
System.out.print("我是watcherA,");
System.out.println("监听到了数据变化类型:"+event.getType());
});
zooKeeper.exists("/watcher1", event -> {
System.out.print("我是watcherB,");
System.out.println("监听到了数据变化类型:"+event.getType());
});
Thread.sleep(50000);
System.out.println("结束");
}
在Linux客户端创建结点
使用getData方法监听
可以监听到结点数据更新、结点删除
public class ZKWatcherGetData {
private static final String IP="192.168.2.142:2181";
private static ZooKeeper zooKeeper;
//创建一个计数器对象
CountDownLatch countDownLatch=new CountDownLatch(1);
@Before
public void connect() throws Exception{
//第一个参数是服务器ip和端口号,第二个参数是客户端与服务器的会话超时时间单位ms,第三个参数是监视器对象
zooKeeper=new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
//通知主线程解除阻塞
countDownLatch.countDown();
}
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
}
});
//主线程阻塞,等待连接对象的创建成功
countDownLatch.await();
}
@After
public void close() throws Exception{
zooKeeper.close();
}
@Test
public void getData1() throws Exception{
//true表示复用zookeeper连接对象的watcher
zooKeeper.getData("/watcher1",true,null);
Thread.sleep(50000);
}
@Test
public void getData2() throws Exception{
//使用自定义watcher
zooKeeper.getData("/watcher1", event -> {
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
}, null);
Thread.sleep(50000);
}
@Test
public void getData3() throws Exception{
//实现多次注册
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("监听到的数据:");
System.out.println("path = " + event.getPath());
System.out.println("eventType = " + event.getType());
//事件类型是数据变化时再注册watcher继续监听
if(event.getType()== Event.EventType.NodeDataChanged)
zooKeeper.getData("/watcher1",this,null);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
};
zooKeeper.getData("/watcher1",watcher,null);
Thread.sleep(50000);
}
@Test
public void getData4() throws Exception{
//注册多个watcher
zooKeeper.getData("/watcher1", event -> {
System.out.print("我是watcherA,");
System.out.println("监听到了数据变化类型:"+event.getType());
}, null);
zooKeeper.getData("/watcher1", event -> {
System.out.print("我是watcherB,");
System.out.println("监听到了数据变化类型:"+event.getType());
}, null);
Thread.sleep(50000);
}
}
使用getChildern方法监听
可以监听到子结点创建/删除、结点删除
public class ZKWatcherGetChildren {
private static final String IP="192.168.2.142:2181";
private static ZooKeeper zooKeeper;
//创建一个计数器对象
CountDownLatch countDownLatch=new CountDownLatch(1);
@Before
public void connect() throws Exception{
//第一个参数是服务器ip和端口号,第二个参数是客户端与服务器的会话超时时间单位ms,第三个参数是监视器对象
zooKeeper=new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getState()==Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
//通知主线程解除阻塞
countDownLatch.countDown();
}
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
}
});
//主线程阻塞,等待连接对象的创建成功
countDownLatch.await();
}
@After
public void close() throws Exception{
zooKeeper.close();
}
@Test
public void getChildren1() throws Exception{
//true表示复用zookeeper连接对象的watcher
zooKeeper.getChildren("/watcher1",true);
Thread.sleep(50000);
}
@Test
public void getChildren2() throws Exception{
//使用自定义watcher
zooKeeper.getChildren("/watcher1", event -> {
System.out.println("监听到的数据:");
System.out.println("path = "+event.getPath());
System.out.println("eventType = "+event.getType());
});
Thread.sleep(50000);
}
@Test
public void getChildren3() throws Exception{
//实现多次注册
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("监听到的数据:");
System.out.println("path = " + event.getPath());
System.out.println("eventType = " + event.getType());
//事件类型是数据变化时再注册watcher继续监听
if(event.getType()== Event.EventType.NodeChildrenChanged)
zooKeeper.getChildren("/watcher1",this);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
};
zooKeeper.getChildren("/watcher1",watcher);
Thread.sleep(50000);
}
@Test
public void getChildren4() throws Exception{
//注册多个watcher
zooKeeper.getChildren("/watcher1", event -> {
System.out.print("我是watcherA,");
System.out.println("监听到了数据变化类型:"+event.getType());
});
zooKeeper.getChildren("/watcher1", event -> {
System.out.print("我是watcherB,");
System.out.println("监听到了数据变化类型:"+event.getType());
});
Thread.sleep(50000);
}
}
Zookeeper作为配置中心
以连接MySQL为例,先初始化连接信息,存储url、user和password
思路:
连接zookeeper服务器
读取zookeeper中的配置信息,注册watcher监听器,存入本地变量
当配置信息发生变化时,通过wather的回调方法捕获数据变化事件
重新获取配置信息
完整代码:
public class ZKConfigWatcher implements Watcher {
private CountDownLatch countDownLatch=new CountDownLatch(1);
//zookeeper信息
private static final String IP="192.168.2.142:2181";
private static ZooKeeper zooKeeper;
//数据库配置
private String url;
private String user;
private String password;
public ZKConfigWatcher(){
//创建时初始化
initValue();
}
@Override
public void process(WatchedEvent event) {
try{
if(event.getType()== Event.EventType.None){
if(event.getState()== Event.KeeperState.SyncConnected){
System.out.println("连接创建成功");
countDownLatch.countDown();
}else if(event.getState()==Event.KeeperState.Disconnected)
System.out.println("断开连接");
else if(event.getState()== Event.KeeperState.Expired) {
System.out.println("会话超时");
zooKeeper=new ZooKeeper(IP,5000,new ZKConfigWatcher());
} else if(event.getState()== Event.KeeperState.AuthFailed) {
System.out.println("认证失败");
}
}else if(event.getType()== Event.EventType.NodeDataChanged){//结点的数据变化时
initValue();
}
}catch (Exception e){
e.printStackTrace();
}
}
private void initValue() {
try{
//连接zookeeper
if(zooKeeper==null)
zooKeeper=new ZooKeeper(IP,5000,this);
//阻塞线程等待连接创建成功
countDownLatch.await();
this.url=new String(zooKeeper.getData("/config/url", true, null));
this.user=new String(zooKeeper.getData("/config/user", true, null));
this.password=new String(zooKeeper.getData("/config/password", true, null));
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ZKConfigWatcher zkConfigWatcher = new ZKConfigWatcher();
for(int i=1;i<=10;i++){
Thread.sleep(2000);
System.out.println("当前url:"+zkConfigWatcher.getUrl());
System.out.println("当前user:"+zkConfigWatcher.getUser());
System.out.println("当前password:"+zkConfigWatcher.getPassword());
System.out.println("--------------");
}
}
public void setUrl(String url) {
this.url = url;
}
public void setUser(String user) {
this.user = user;
}
public void setPassword(String password) {
this.password = password;
}
public String getUrl() {
return url;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
}
运行时在Linux客户端修改user
Zookeeper生成分布式唯一ID
思路:
创建临时有序结点
完整代码
public class ZKUniqueIDWatcher implements Watcher {
private CountDownLatch countDownLatch=new CountDownLatch(1);
//zookeeper信息
private static final String IP="192.168.2.142:2181";
private static ZooKeeper zooKeeper;
//唯一ID
private String uniqueId="/id";
public ZKUniqueIDWatcher(){
try{
//连接zookeeper
if(zooKeeper==null)
zooKeeper=new ZooKeeper(IP,5000,this);
//阻塞线程等待连接创建成功
countDownLatch.await();
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
try{
if(event.getType()== Event.EventType.None) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("连接创建成功");
countDownLatch.countDown();
} else if (event.getState() == Event.KeeperState.Disconnected)
System.out.println("断开连接");
else if (event.getState() == Event.KeeperState.Expired) {
System.out.println("会话超时");
zooKeeper = new ZooKeeper(IP, 5000, new ZKUniqueIDWatcher());
} else if (event.getState() == Event.KeeperState.AuthFailed) {
System.out.println("认证失败");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private String getUniqueId(){
try{
//创建临时有序结点
return zooKeeper.create(uniqueId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws InterruptedException {
ZKUniqueIDWatcher zkUniqueIDWatcher= new ZKUniqueIDWatcher();
for(int i=1;i<=10;i++){
System.out.println(zkUniqueIDWatcher.getUniqueId());
}
}
}
运行结果:
分布式锁
思路:
- 每个客户端往/locks下创建临时有序节点/locks/lock_,创建成功后/locks下会有每个客户端对应的结点,例如/locks/lock_000000001
- 客户端获取/locks下子结点并进行排序,判断排在最前面的是否是自己,如果自己的锁结点在第一位代表获取锁成功
- 如果不在第一位,监听自己前一位的锁结点,例如自己是lock_000000002,那么监听lock_000000001
- 当前一位锁结点lock_000000001对应客户端执行完成,释放了锁,将会触发监听客户端lock_000000002的逻辑
- 监听客户端重新执行第二步的逻辑,判断自己是否获得锁
分布式锁代码:
//分布式锁案例
public class ZKLock {
//计数器对象
private CountDownLatch countDownLatch=new CountDownLatch(1);
//zookeeper信息
private static ZooKeeper zooKeeper;
private static final String IP="192.168.2.142:2181";
private static final String LOCK_ROOT_PATH="/locks";
private static final String LOCK_NODE_PATH="lock_";
private String lockPath;
//监视上一个结点是否被删除
private Watcher watcher=new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getType()== Event.EventType.NodeDeleted){
synchronized (this){
watcher.notifyAll();
}
}
}
};
//在构造器中连接zookeeper
public ZKLock(){
try {
zooKeeper=new ZooKeeper(IP, 5000, event -> {
if(event.getType()== Watcher.Event.EventType.None) {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("连接创建成功");
countDownLatch.countDown();
}
}
});
countDownLatch.await();
}catch (Exception e){
e.printStackTrace();
}
}
//获取锁
public void acquireLock() throws Exception{
//创建锁结点
createLock();
//尝试获取锁
attemptLock();
}
//创建锁结点
private void createLock() throws Exception{
//判断locks是否存在,不存在则创建为持久化结点
if (zooKeeper.exists(LOCK_ROOT_PATH, false)==null)
zooKeeper.create(LOCK_ROOT_PATH,new byte[0],ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
//创建临时有序结点
lockPath=zooKeeper.create(LOCK_ROOT_PATH+"/"+LOCK_NODE_PATH,new byte[0],ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("结点创建成功: "+lockPath);
}
//尝试获取锁
private void attemptLock() throws Exception{
//获取/locks下的所有子结点
List<String> lockList = zooKeeper.getChildren(LOCK_ROOT_PATH, false);
//对子结点列表排序
Collections.sort(lockList);
//当前结点的位置
int index=lockList.indexOf(lockPath.substring(LOCK_ROOT_PATH.length()+1));
//是第一位
if(index==0){
System.out.println("获取锁成功");
}else {
//获取上一个结点的路径并监视
String prePath=lockList.get(index-1);
Stat stat = zooKeeper.exists(LOCK_ROOT_PATH + "/" + prePath, watcher);
if(stat==null){
attemptLock();
}else {
synchronized (this){
wait();
}
attemptLock();
}
}
}
//释放锁
public void releaseLock() throws Exception{
//删除临时有序结点
zooKeeper.delete(lockPath,-1);
zooKeeper.close();
System.out.println(lockPath+" 锁已经释放");
}
}
售票测试类:
public class TicketSeller {
private void sell(){
System.out.println("开始售票");
int sleepMills=5000;
try{
//模拟复杂逻辑
Thread.sleep(sleepMills);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("售票结束");
}
private void sellWithLock() throws Exception{
ZKLock zkLock=new ZKLock();
zkLock.acquireLock();
sell();
zkLock.releaseLock();
}
public static void main(String[] args) throws Exception {
TicketSeller seller = new TicketSeller();
for(int i=0;i<10;i++){
seller.sellWithLock();
}
}
}
记得允许多进程,和多线程不同,现在是两个Java客户端,不是单客户端多线程
运行结果:
客户端1的序号是5、7…
客户端2的序号是6、8…可见多客户端之间的分布式锁成功咧
集群
集群的搭建
先复制3份zookeeper
cp -r zookeeper-3.4.14 zookeeper2181
cp -r zookeeper-3.4.14 zookeeper2182
cp -r zookeeper-3.4.14 zookeeper2183
进入2181服务器,编辑配置文件
修改datadir为2181的目录,并添加集群信息
之后进入data目录,将1写入myid
配置完毕,进入2181下的bin目录,等待启动
对2182进行相同操作,修改zoo.cfg里的datadir为2182的目录,端口为2182,保存。然后给data的myid输入2,并检查。
然后进入bin目录,等待启动,对2183进行相同操作
全部启动服务
再通过status查询状态 可以发现2182的模式为leader,其余两个为follow,集群搭建成功了 !
通过不同的端口号分别登陆3个集群
在其中一个客户端进行操作,其他客户端也都能读取到
一致性协议ZAB
ZAB全称zookeeper atomic broadcast (zookeeper原子广播),zookeeper是通过zab协议保证分布式事务的最终一致性
基于zab协议,集群角色主要分为以下三类:
- leader
领导者负责进行投票的发起和决议,更新系统状态 - learner
follower:用于接收客户端请求并响应,在选举中参与投票
observer:可以接收客户端连接,将写请求转发给leader结点,不参与投票,只同步状态。observer的目的是为了扩展系统,提高读取速度。 - client
请求发起者
zab协议类似两阶段提交协议方式解决数据一致性:
①leader从客户端收到写请求(非leader收到也要转发给leader)
②leader生成一个新的事务并为其生成唯一的ZXID
③leader将这个事务提议发送给所有follow结点
④follow结点将收到的事务请求加入历史队列,并发送ack给leader
⑤leader收到半数以上ack时,会发送commit请求
⑥follow收到commit请求时,从历史队列中将事务请求commit
类似于打仗,follow就是个小兵,收到了消息必须给老大,然后老大决定要进攻,传下去,然后有一半人以上人都已经确定了就准备攻击,然后全军攻击。
leader选举
服务器状态:
- looking 处于该状态时集群中没有leader,因此会进入leader选举状态
- leading 领导者状态
- following 跟随者状态
- observing 观察者状态
服务器启动时的leader选举
①每个server发出一个投票,由于是初始情况,server1和server2都会将自己作为leader进行投票,每次投票都会包含所推举的服务器的myid和zxid(事务id),使用(myid,zxid)表示,此时server1的投票为(1,0),server2的投票为(2,0),然后各自将投票发给集群中其他机器。
②集群中每台机器接受来自集群中各个服务器的投票。
③处理投票,针对每个投票,服务器都需要将别人的投票和自己的投票比较:
- 优先检查事务编号zxid,谁大谁优先
- 如果zxid相同,比较myid,谁大谁优先。所以server1会更新自己的投票为(2,0)
④统计投票,每次投票后服务器都会统计投票信息判断是否有半数机器接收到相同的投票信息,对于server1和server2而言都统计出集群中已经有两台机器接受了(2,0)的投票信息,因此便认为已经选出了leader
⑤改变服务器状态,一旦确定了leader,如果是follower就变为follew,leader就变为leadering。
服务器运行时的leader选举
一旦leader机器故障后,整个集群将暂停服务重新选举
假设有2181、2182、2183,2182是leader
①变更状态,2181和2183变为looking
②每个server都会投票给自己,例如2181投票给(1,5),1表示自己的myid,5表示事务id,2183投票(3,4)。
③接收来自各个服务器的投票,与启动时的投票类似
④处理投票,由于2181的zxid大于2183,所以2181成为leader
⑤统计投票,与启动时类似
⑥改变服务器状态,与启动时类似
观察者角色及其配置
特点:不参与leader选举,不参与写数据时的ack反馈
任何想变成observer角色的配置文件加入以下配置peerType=observer
并在所有server的配置文件中,配置成observer模式的server那行追加配置:observer
这里出了点小问题搞了我一个小时,貌似三个要全部start才能status…
IDEA连接集群
只需要把集群ip用逗号分隔即可
//连接集群
public class ZKClusterTest {
public static void main(String[] args) {
CountDownLatch countDownLatch=new CountDownLatch(1);
try{
ZooKeeper zooKeeper=new ZooKeeper("192.168.2.142:2181,192.168.2.142:2182,192.168.2.142:2183", 5000, event -> {
if(event.getState()== Watcher.Event.KeeperState.SyncConnected){
System.out.println("连接集群成功");
countDownLatch.countDown();
}
});
countDownLatch.await();
zooKeeper.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果:
明天见