zookeeper
ZooKeeper是做分布式协调的,那它协调啥?
配置写在哪里?难道运维人员要登录到每一台机器一台一台地改吗?
可以将配置文件放在一个共享的位置中,比如redis,比如数据库,比如zk,任何一个地方。
zk具有回调机制,就不需要轮询了。
分布式锁的实现,这个zk也可以做,面试常问,虽然可能用不到,但是这道题可以带出很多知识点。
使用ZooKeeper实现分布式配置中心
思路:我们将所有的配置数据用data配置到zk中去,在客户端我们既可以get它,也可以watch它。
一旦外界对这个数据进行了修改,这个修改就会引发watch的回调。
1. 四台机器配好 Zookeeper 集群
2. 项目目录结构
3. 代码
TestConfig.java,主程序测试入口
package com.msb.zookeeper.config;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestConfig {
ZooKeeper zk;
@Before
public void conn() {
zk = ZKUtils.getZK();
}
@After
public void close() {
try {
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void getConf() {
WatchCallBack watchCallBack = new WatchCallBack();
watchCallBack.setZk(zk);
MyConf myConf = new MyConf();
watchCallBack.setMyConf(myConf);
watchCallBack.awaitExists();
//1,节点不存在
//2,节点存在
while (true) {
if (myConf.getConf().equals("")) {
System.out.println("conf diu le ......");
watchCallBack.awaitExists();
} else {
System.out.println("In getConf Test, myConf is:" + myConf.getConf());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Myconf.java,用来存放配置。
真正的开发中,配置中的内容可能是一个复杂的json,xml等等。
此处用一个String conf
字符串代替。
package com.msb.zookeeper.config;
//这个class是你未来用来存放配置数据的。真正的开发中,配置的内容可能是一个复杂的json,xml等等
public class MyConf {
private String conf;
public String getConf() {
return conf;
}
public void setConf(String conf) {
System.out.println("MyConf.conf is set to: " + conf);
this.conf = conf;
}
}
ZKUtils.java,用来配置Zookeeper的Server端服务器地址,以及创建并返回Zookeeper实例
package com.msb.zookeeper.config;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
public class ZKUtils {
private static ZooKeeper zk;
// 需要在zk中设置 create /testLock create /testLock/AppConf set /testLock/AppConf "hello,conf"
// ip:port 后面的 /testLock 是指定的根目录,后续所有的操作都在以 /testLock 为根目录的基础上进行
private static String address = "10.0.0.131:2181,10.0.0.132:2181,10.0.0.133:2181,10.0.0.134:2181/testLock";
private static DefaultWatch watch = new DefaultWatch();
private static CountDownLatch init = new CountDownLatch(1);
public static ZooKeeper getZK() {
try {
zk = new ZooKeeper(address, 1000, watch);//new ZooKeeper 对象的时候需要使用到 DefaultWatch
watch.setLatch(init);
init.await();
} catch (Exception e) {
e.printStackTrace();
}
return zk;
}
}
DefaultWatch.java,在 new ZooKeeper() 对象的时候需要用到,作为参数传入
package com.msb.zookeeper.config;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;
public class DefaultWatch implements Watcher {
CountDownLatch latch;
public void setLatch(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent event) {
System.out.println(event.toString());
switch (event.getState()) {
case Unknown:
break;
case Disconnected:
break;
case NoSyncConnected:
break;
case SyncConnected:
System.out.println("In DefaultWatch, SyncConnected,连接成功.");
latch.countDown();
break;
case AuthFailed:
break;
case ConnectedReadOnly:
break;
case SaslAuthenticated:
break;
case Expired:
break;
}
}
}
WatchCallBack.java,实现多个接口,既是Watcher,又是Callback。重写了各个回调函数,是整个Reactor模型的核心
package com.msb.zookeeper.config;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;
public class WatchCallBack implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {
ZooKeeper zk;
MyConf myConf;
CountDownLatch latch = new CountDownLatch(1);
public MyConf getMyConf() {
return myConf;
}
public ZooKeeper getZk() {
return zk;
}
public void setMyConf(MyConf myConf) {
this.myConf = myConf;
}
public void setZk(ZooKeeper zk) {
this.zk = zk;
}
public void awaitExists() {
System.out.println("awaitExists...");
zk.exists("/AppConf", this, this, "ABC");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("awaitExists finish...");
}
@Override
/**
* DataCallback接口实现
*/
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
if (data != null) {
String str = new String(data);
System.out.println("In WatchCallBack, data is: " + str);
myConf.setConf(str);
latch.countDown();
}
}
@Override
/**
* StatCallback 接口实现
*/
public void processResult(int rc, String path, Object ctx, Stat stat) {
if (stat != null) {
zk.getData("/AppConf", this, this, "sdfs");
}
}
@Override
/**
* Watcher 接口实现
*/
public void process(WatchedEvent event) {
switch (event.getType()) {
case None:
break;
case NodeCreated:
//节点被创建事件
zk.getData("/AppConf", this, this, "sdfs");
break;
case NodeDeleted:
//节点被删除事件,这要根据业务的容忍性,写处理方式
System.out.println("In WatchCallBack, 节点被删除了");
myConf.setConf("");
latch = new CountDownLatch(1);//阻塞
break;
case NodeDataChanged:
//数据被更改
zk.getData("/AppConf", this, this, "sdfs");
break;
case NodeChildrenChanged:
break;
}
}
}
4. 运行 & 测试
运行 Test
控制台输出,可以看到由于zk.exists("/AppConf", this, this, "ABC");
后面加了latch.await()
;正在阻塞地等待Zookeeper中对应节点的创建。
用客户端连接Zookeeper
建指定节点create /testLock/AppConf "foo"
]
检测到配置文件节点的产生
修改配置文件的节点数据set /testLock/AppConf "bar"
检测到节点数据的修改
删除配置文件节点delete /testLock/AppConf
输出“conf 丢了”,重新进入阻塞等待状态…等待节点的创建
附:Zookeeper客户端常用命令:
ls /列出根目录所有节点名称
create /testLock/AppConf "foo"创建节点并设置节点内容
set /testLock/AppConf "bar"更新节点内容
delete /testLock/AppConf删除节点
使用Zookeeper实现分布式锁
1. 和前面一样,配好4台Zookeeper集群
2. 目录结构
3、代码
用到了前面的两个:
- ZKUtils.java,获取zk实例,配置server ip
- DefaultWatch.java,默认的Watcher,在 new ZooKeeper 对象的时候会用到
另外,增加了:
** TestLock.java**,测试分布式锁的程序入口
package com.msb.zookeeper.lock;
import com.msb.zookeeper.config.ZKUtils;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestLock {
ZooKeeper zk;
@Before
public void conn() {
zk = ZKUtils.getZK();
}
@After
public void close() {
try {
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void lock() {
for (int i = 0; i < 10; i++) {//模拟10个线程分布在10台机器上
new Thread() {
@Override
public void run() {
WatchCallBack watchCallBack = new WatchCallBack();
watchCallBack.setZk(zk);
String threadName = Thread.currentThread().getName();
watchCallBack.setThreadName(threadName);
//每一个线程:
//抢锁
watchCallBack.tryLock();
//干活
System.out.println(threadName + " working...");
try {
Thread.sleep(100);//模拟干活需要的时间,如果这里不sleep,会在第一个已经释放了,后面的还没开始watch,看不到第一个释放锁的消息,就断层了
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放锁
watchCallBack.unLock();
}
}.start();
}
while (true) {
}
}
}
WatchCallBack.java,分布式锁的Watcher,里面维护了一个CountDownLatch。同时,继承了接口实现各种回调方法,所以既是Watcher,也是Callback,在传参的时候直接传this即可
package com.msb.zookeeper.lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class WatchCallBack implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback, AsyncCallback.StatCallback {
ZooKeeper zk;
String threadName;
CountDownLatch cc = new CountDownLatch(1);
String pathName;
public String getPathName() {
return pathName;
}
public void setPathName(String pathName) {
this.pathName = pathName;
}
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
public ZooKeeper getZk() {
return zk;
}
public void setZk(ZooKeeper zk) {
this.zk = zk;
}
/**
* 通过zk创建顺序节点的方式,获取锁
*/
public void tryLock() {
try {
System.out.println(threadName + " create....");
zk.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "str");
cc.await();//等待自己是第一个的时候,才能返回
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 释放锁
*/
public void unLock() {
try {
zk.delete(pathName, -1);
System.out.println(threadName + ": finish work....");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
//process in interface Watcher
public void process(WatchedEvent event) {
//如果第一台机器拿到的锁释放了,只有第二台机器收到了回调事件
//如果,不是第一台机器,而是其他的某一台机器挂了,也能造成他后边的那台机器收到这个通知,从而让他后边那台机器跟去watch挂掉这个哥们前边的机器
switch (event.getType()) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
zk.getChildren("/", false, this, "str");//去调用getChildren的回调方法
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
@Override
//StringCallback
public void processResult(int rc, String path, Object ctx, String name) {
if (name != null) {//接收zk帮你创建的顺序节点的名称
System.out.println(threadName + " create node : " + name);
pathName = name;
zk.getChildren("/", false, this, "str");
}
}
@Override
//zk.getChildren 的 callback
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
//能看到自己前边的所有节点 List<String> children,对前面所有的节点名称排序
Collections.sort(children);
int index = children.indexOf(pathName.substring(1));//看自己的位置
//判断自己是否为第一个
if (index == 0) {
//如果自己是第一个,执行countdown
System.out.println(threadName + ": I am first....");
try {
zk.setData("/", threadName.getBytes(), -1);//把自己的锁信息写进根目录的node里面去,你可以用get看到,这个步骤为了拖慢一下速度
cc.countDown();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//自己不是第一个,则watch前一个
zk.exists("/" + children.get(index - 1), this, this, "sdf");
}
}
@Override
// StatCallback
public void processResult(int rc, String path, Object ctx, Stat stat) {
//偷懒,这里省略没写
}
}
4、运行 & 测试
输出结果如下,可以看到10个线程在创建之后,都拿到了自己的自增目录名称,然后按照自己的目录名称顺序依次进行:拿到锁,干活,释放锁。
WatchedEvent state:SyncConnected type:None path:null
In DefaultWatch, SyncConnected,连接成功.
Thread-3 create....
Thread-9 create....
Thread-1 create....
Thread-7 create....
Thread-0 create....
Thread-5 create....
Thread-8 create....
Thread-4 create....
Thread-2 create....
Thread-6 create....
Thread-3 create node : /lock0000000071
Thread-9 create node : /lock0000000072
Thread-6 create node : /lock0000000073
Thread-0 create node : /lock0000000074
Thread-5 create node : /lock0000000075
Thread-7 create node : /lock0000000076
Thread-1 create node : /lock0000000077
Thread-4 create node : /lock0000000078
Thread-8 create node : /lock0000000079
Thread-2 create node : /lock0000000080
Thread-3: I am first....
Thread-3 working...
Thread-3: finish work....
Thread-9: I am first....
Thread-9 working...
Thread-9: finish work....
Thread-6: I am first....
Thread-6 working...
Thread-6: finish work....
Thread-0: I am first....
Thread-0 working...
Thread-0: finish work....
Thread-5: I am first....
Thread-5 working...
Thread-5: finish work....
Thread-7: I am first....
Thread-7 working...
Thread-7: finish work....
Thread-1: I am first....
Thread-1 working...
Thread-1: finish work....
Thread-4: I am first....
Thread-4 working...
Thread-4: finish work....
Thread-8: I am first....
Thread-8 working...
Thread-8: finish work....
Thread-2: I am first....
Thread-2 working...
Thread-2: finish work....
在程序执行的同时,从Zookeeper的Client端不断获取目录内容,看到整个过程大致是这样的:
当一个线程中的任务执行完之后,显示调用了zk.delete(pathName, -1);
将节点删除。这样就能触发下一个正在watch它的节点所在的线程,去判断自己是不是在排名的第一个。
- 如果是第一个,开始干活
- 如果不是第一个,watch它的前一个(这种情况出现在前一个节点突然挂了的情况)