数据发布订阅
多个订阅者对象同时监听同一主题对象,当被监听的主题对象状态有变化时通知所有订阅者更新自身状态。发布方和订阅方独立封装、独立改变,当一个对象的改变需要同时改变其他对象,并且它不知道有多少个对象需要改变,可以使用发布订阅模式。
在分布式系统中的顶级应用有配置管理和服务发现。
配置管理
指集群中的机器拥有某些某些配置,并且这些配置信息需要动态的改变,那么我们可以使用发布订阅模式把配置信息做统一的管理,让这些机器订阅配置信息的改变,当配置信息有改变时这些机器得到通知并更新自己的配置。
服务发现
指对集群中服务的上下线做统一管理,每一个工作服务器都可以作为数据的发布方,向集群注册自己的基本信息,而某些监控服务器作为订阅方,订阅工作服务器的基本信息。当工作服务器的基本信息有改变时比如:上下线、服务器的角色或者服务范围改变时,监控服务器可以得到通知并响应这些改变。
架构图
左边是zookeeper集群,右边是服务器集群。其中前面三个是工作服务器,第四个是管理服务器,最后一个是控制服务器。
config节点,用于配置管理。管理服务器(manage server)通过config下发配置信息,工作服务器(work server)通过订阅config节点的改变来更新自己的配置。
servers节点,用于服务发现。工作服务器(work server)在启动的时候,会在servers节点下创建一个临时节点,管理服务器(manage server)充当监听器(monitor),通过监听servers节点下子节点列表的变化来更新自己内存中工作服务器列表的信息。
控制服务器(control server)由command节点作为中介,向管理服务器(manage server)下发指令。控制服务器(control server)向command节点写入命令信息,管理服务器(manage server)通过订阅command节点数据的改变来监听并执行命令。
流程图
核心类
ServerConfig用于记录Work Server的配置信息;
ServerData用于记录Work Server的基本信息;
WorkServer对应架构图的Work Server;
ManageServer对应架构图的Manage Server;
SubscribeZkClient作为示例程序入口服务站启动Work Server和Manage Server
代码实现
/**
* Created by zhangdd on 2019/12/16
*/
public class ServerConfig {
private String dbUrl;
private String dbPwd;
private String dbUser;
public String getDbUrl() {
return dbUrl;
}
public void setDbUrl(String dbUrl) {
this.dbUrl = dbUrl;
}
public String getDbPwd() {
return dbPwd;
}
public void setDbPwd(String dbPwd) {
this.dbPwd = dbPwd;
}
public String getDbUser() {
return dbUser;
}
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
@Override
public String toString() {
return "ServerConfig{" +
"dbUrl='" + dbUrl + '\'' +
", dbPwd='" + dbPwd + '\'' +
", dbUser='" + dbUser + '\'' +
'}';
}
}
/**
* Created by zhangdd on 2019/12/16
*/
public class ServerData {
private String address;
private Integer id;
private String name;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "ServerData{" +
"address='" + address + '\'' +
", id=" + id +
", name='" + name + '\'' +
'}';
}
}
/**
* Created by zhangdd on 2019/12/16
* <p>
* 1.向servers节点下创建一个临时节点
* 2.监听config节点配置信息的变化
*/
public class WorkServer {
private ZkClient zkClient;
private String configPath;
private String serversPath;
private ServerData serverData;
private ServerConfig serverConfig;
private IZkDataListener dataListener;
public WorkServer(String configPath, String serversPath, ServerData serverData,
ZkClient zkClient, ServerConfig initConfig) {
this.zkClient = zkClient;
this.configPath = configPath;
this.serversPath = serversPath;
this.serverData = serverData;
this.serverConfig = initConfig;
this.dataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object data) throws Exception {
String retJson = new String((byte[]) data);
ServerConfig serverConfigLocal = (ServerConfig) JSON.parseObject(retJson, ServerConfig.class);
updateConfig(serverConfigLocal);
System.out.println("new Work server config is:" + serverConfig.toString());
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
};
}
public void start() {
System.out.println("work server start...");
initRunning();
}
public void stop() {
System.out.println("work server stop...");
zkClient.unsubscribeDataChanges(configPath, dataListener);
}
private void initRunning() {
registMe();
zkClient.subscribeDataChanges(configPath, dataListener);
}
private void registMe() {
String mePath = serversPath.concat("/").concat(serverData.getAddress());
try {
zkClient.createEphemeral(mePath, JSON.toJSONString(serverData)
.getBytes());
} catch (ZkNoNodeException e) {
zkClient.createPersistent(serversPath, true);
registMe();
}
}
private void updateConfig(ServerConfig serverConfig) {
this.serverConfig = serverConfig;
}
}
/**
* Created by zhangdd on 2019/12/16
* <p>
* 1.监听command节点数据变化,读取执行命令
* 2.监听servers节点下子节点的变化,更新内存中可以使用的服务列表
*/
public class ManageServer {
private String serversPath;
private String commandPath;
private String configPath;
private ZkClient zkClient;
private ServerConfig config;
private IZkChildListener childListener;
private IZkDataListener dataListener;
private List<String> workServerList;
public ManageServer(String serversPath, String commandPath,
String configPath, ZkClient zkClient, ServerConfig config) {
this.serversPath = serversPath;
this.commandPath = commandPath;
this.zkClient = zkClient;
this.config = config;
this.configPath = configPath;
this.childListener = new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> currentChilds) throws Exception {
workServerList = currentChilds;
System.out.println("work server list changed, new list is ");
execList();
}
};
this.dataListener=new IZkDataListener() {
@Override
public void handleDataChange(String s, Object data) throws Exception {
String cmd = new String((byte[]) data);
System.out.println("cmd:" + cmd);
exeCmd(cmd);
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
};
}
private void initRunning() {
zkClient.subscribeDataChanges(commandPath, dataListener);
zkClient.subscribeChildChanges(serversPath, childListener);
}
/*
* 1: list 2: create 3: modify
*/
private void exeCmd(String cmdType) {
if ("list".equals(cmdType)) {
execList();
} else if ("create".equals(cmdType)) {
execCreate();
} else if ("modify".equals(cmdType)) {
execModify();
} else {
System.out.println("error command!" + cmdType);
}
}
private void execList() {
System.out.println(workServerList.toString());
}
private void execCreate() {
if (!zkClient.exists(configPath)) {
try {
zkClient.createPersistent(configPath, JSON.toJSONString(config)
.getBytes());
} catch (ZkNodeExistsException e) {
zkClient.writeData(configPath, JSON.toJSONString(config)
.getBytes());
} catch (ZkNoNodeException e) {
String parentDir = configPath.substring(0,
configPath.lastIndexOf('/'));
zkClient.createPersistent(parentDir, true);
execCreate();
}
}
}
private void execModify() {
config.setDbUser(config.getDbUser() + "_modify");
try {
zkClient.writeData(configPath, JSON.toJSONString(config).getBytes());
} catch (ZkNoNodeException e) {
execCreate();
}
}
public void start() {
initRunning();
}
public void stop() {
zkClient.unsubscribeChildChanges(serversPath, childListener);
zkClient.unsubscribeDataChanges(commandPath, dataListener);
}
}
public class SubscribeZkClient {
private final static String CONNECTSTRING = "192.168.0.103:2181";
private static final int CLIENT_QTY = 5;
private static final String CONFIG_PATH = "/config";
private static final String COMMAND_PATH = "/command";
private static final String SERVERS_PATH = "/servers";
public static void main(String[] args) {
List<ZkClient> clients = new ArrayList<ZkClient>();
List<WorkServer> workServers = new ArrayList<WorkServer>();
ManageServer manageServer = null;
try {
ServerConfig initConfig = new ServerConfig();
initConfig.setDbPwd("123456");
initConfig.setDbUrl("jdbc:mysql://localhost:3306/mydb");
initConfig.setDbUser("root");
ZkClient clientManage = new ZkClient(CONNECTSTRING, 5000, 5000,
new BytesPushThroughSerializer());
manageServer = new ManageServer(SERVERS_PATH,
COMMAND_PATH, CONFIG_PATH, clientManage, initConfig);
manageServer.start();
for (int i = 0; i < CLIENT_QTY; i++) {
ZkClient client = new ZkClient(CONNECTSTRING, 5000, 5000,
new BytesPushThroughSerializer());
clients.add(client);
ServerData serverData = new ServerData();
serverData.setId(i);
serverData.setName("WorkServer#" + i);
serverData.setAddress("192.168.1." + i);
WorkServer workServer = new WorkServer(CONFIG_PATH, SERVERS_PATH,
serverData, client, initConfig);
workServers.add(workServer);
workServer.start();
}
System.out.println("敲回车键退出!\n");
new BufferedReader(new InputStreamReader(System.in)).readLine();
} catch (Exception e) {
e.printStackTrace();
} finally {
for (WorkServer workServer : workServers) {
try {
workServer.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
for (ZkClient client : clients) {
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}