需求
在分布式系统中存在多个服务器,这些服务器可以动态上下线,而客户端可以连接任意服务器,但是如果连接的服务器突然下线那么客户端需要重新连接其他服务器,这就需要在服务器上下线的时候客户端能感知,获取哪些可以连接的服务器。
解决思路
每次服务器启动的时候去zookeeper上进行注册(注册规则自由指定,比如简单使用/servers/server001 hostname),而客户端上线就获取服务器列表,并对节点进行监听,一旦有服务器下线那么就能监听到事件从而重新获取服务器列表。
程序简单实现
服务器端:
/**
* 服务端程序
* @author 12706
*
*/
public class DistributedServer {
private static final String connectionString = "192.168.25.127:2181,"
+ "192.168.25.129:2181,"
+ "192.168.25.130:2181";
public static final Integer sessionTimeout = 2000;
public static ZooKeeper zkClient = null;
/**
* 获取zookeeper连接
* @throws Exception
*/
public void getConnection() throws Exception{
zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher(){
//收到事件通知后的回调函数(应该是我们自己的事件处理逻辑)
public void process(WatchedEvent event) {
System.out.println(event.getType()+","+event.getPath());
try {
//为了能一直监听,调用一次注册一次
zkClient.getChildren("/", true);
}catch(Exception e){
}
}});
}
/**
* 注册服务器信息
* @param hostname 注册的服务器名
* @throws Exception
*/
public void registerServer(String hostname) throws Exception{
//创建的是带序号的临时节点 生成的节点像/servers/server000001,/servers/server000002等
//节点数据即为注册的主机名
String path = zkClient.create("/servers"+"/server", hostname.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname+" --上线了-- "+path);
}
/**
* 服务器注册完后,执行业务逻辑
* @param hostname
* @throws IOException
*/
public void executeBusiness(String hostname) throws IOException{
System.out.println(hostname+"开始工作了!");
System.in.read();
}
public static void main(String[] args) throws Exception {
//获取zookeeper连接
DistributedServer server = new DistributedServer();
server.getConnection();
//服务器上线,完成注册
Scanner scanner = new Scanner(System.in);
System.out.println("输入hostname");
String hostname = scanner.nextLine();
server.registerServer(hostname);
//执行业务逻辑
server.executeBusiness(hostname);
}
}
客户端:
/*
* 客户端程序
*/
public class DistributeClient {
private static final String connectionString = "192.168.25.127:2181,"
+ "192.168.25.129:2181,"
+ "192.168.25.130:2181";
public static final Integer sessionTimeout = 2000;
public static ZooKeeper zkClient = null;
public static final String parentNode = "/servers";
//注意:加volatile的意义何在?使得多线程看到的服务器列表一致而不会拷贝到自己的工作空间
public volatile List<String> serverList = new ArrayList<String>();
/**
* 获取zookeeper连接
* @throws Exception
*/
public void getConnection() throws Exception{
zkClient = new ZooKeeper(connectionString, sessionTimeout, new Watcher(){
//收到事件通知后的回调函数(应该是我们自己的事件处理逻辑)
public void process(WatchedEvent event) {
System.out.println(event.getType()+","+event.getPath());
try {
//重新获取(更新)服务器列表,并进行监听
getServerList();
}catch(Exception e){
}
}});
}
/**
* 获取服务器列表信息,并对父节点进行监听
* @throws Exception
*/
public void getServerList() throws Exception{
//获取服务器列表,并对父节点进行监听
//getChildren()相对于命令行 ls /znode,对子节点进行监听
List<String> children = zkClient.getChildren(parentNode, true);
//创建临时集合,将子节点存入
List<String> childrenList = new ArrayList<String>();
for (String child : children) {
byte[] data = zkClient.getData(parentNode+"/"+child, false, null);
childrenList.add(new String(data));
}
//将临时集合中的节点赋给服务器列表serverList,以便业务线程使用
serverList = childrenList;
System.out.println(serverList);
}
/**
* 业务功能
* @throws Exception
*/
public void executeBusiness() throws Exception{
System.out.println("获取的服务器列表:"+serverList);
System.out.println("客户端开始工作了...");
System.in.read();
}
public static void main(String[] args) throws Exception {
//获取zookeeper连接
DistributeClient client = new DistributeClient();
client.getConnection();
//获取服务器列表
client.getServerList();
//业务功能
client.executeBusiness();
}
}
测试
运行三次服务器端程序,输入的hostname分别为mini1,mini2,mini3当成注册了三个服务器
运行客户端程序(可以启动多次,简单起见这里就一次)
关闭其中2个(mini1,mini2)连接zookeeper的客户端(关闭后注册的服务器也就消失了),查看客户端输出
一旦服务器下线了,客户端能监听到并且重新获取服务器列表。
zookeepe的监听机制及守护线程
以上述DistributeServer为例,当启动main线程的时候,获取zkClient的时候,会启动两个线程一个是监听器listener一个是客户端连接connet(与zookeeper进行通信,比如节点的增加删除获取等)。当connect调用getChildren(“/”,true)的时候,服务端(zookeeper集群)会获得客户端的ip与监听的节点,当该节点下的子节点编号的时候,会调用客户端的listener去执行process方法。
上面介绍了zkClient开启了两个线程,那么即使main线程执行结束了,只要上述两个线程有一个没有执行结束(比如监听线程)那么程序就不会介绍,那么对于上述程序为什么还要要使用System.in.read()来使程序不结束呢?原因就是设计的时候那两个线程是设计为守护线程的,一旦主程序结束程序就结束,否则如果业务方法都执行完了程序还没结束是不符合逻辑的,所以上述使用了
System.in.read()。
下面写了两个简单的小测试
public class DaemonTest {
public static void main(String[] args) {
System.out.println("main线程开始...");
new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println("线程进行中...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
}).start();;
System.out.println("main线程结束");
}
}
只要存在线程不结束,那么程序就不会退出。
但是如果将线程设置为守护线程一旦main线程结束那么程序就结束了
public class DaemonTest2 {
public static void main(String[] args) {
System.out.println("main线程开始...");
Thread thread = new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println("线程进行中...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
});
thread.setDaemon(true);
thread.start();
System.out.println("main线程结束");
}
}
输出后程序立马就会退出
main线程开始...
main线程结束
线程进行中...