发布订阅
发布/订阅模式是一对多的关系,多个订阅者对象同时监听某一主题对象,这个主题对象在自身状态发生变化时会通知所有的订阅者对象。使它们能自动的更新自己的状态。发布/订阅可以使得发布方和订阅方独立封装、独立改变。当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变时可以使用发布/订阅模式。发布/订阅模式在分布式系统中的典型应用有服务发现与注册。
服务发现与注册
服务发现与注册是指对集群中的服务上下线做统一管理。每个工作服务器都可以作为数据的发布方向集群注册自己的基本信息,而让某些监控服务器作为订阅方,订阅工作服务器的基本信息,当工作服务器的基本信息发生改变如上下线、服务器角色或服务范围变更,监控服务器可以得到通知并响应这些变化。
ZK实现DEMO
服务提供方
public class InitListener implements ServletContextListener {
@Value("${server.port}")
private int port;
@Override
public void contextInitialized(ServletContextEvent sce) {
WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()).getAutowireCapableBeanFactory().autowireBean(this);
try {
// 获取本机ip
String hostAddress = InetAddress.getLocalHost().getHostAddress();
// 服务注册
ServiceRegister.register(hostAddress,port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
public class ServiceRegister {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME = "/service1";
public static void register(String address,int port) {
try {
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181",5000,(watchedEvent)->{});
Stat exists = zooKeeper.exists(BASE_SERVICES + SERVICE_NAME, false);
if(exists==null) {
// 如果service1不存在,创建节点
zooKeeper.create(BASE_SERVICES + SERVICE_NAME,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
String server_path = address+":"+port;
// 创建临时子节点
zooKeeper.create(BASE_SERVICES + SERVICE_NAME+"/child",server_path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里通过ZK临时节点的方式进行服务注册,当ZK连接断开后,临时节点会自动销毁。
服务消费方
public class InitListener implements ServletContextListener {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME="/service1";
private ZooKeeper zooKeeper;
@Override
public void contextInitialized(ServletContextEvent sce) {
try {
zooKeeper = new ZooKeeper("127.0.0.1:2181",5000,(watchedEvent)->{
// 监听service1变化
if(watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged && watchedEvent.getPath().equals(BASE_SERVICES+SERVICE_NAME)) {
updateServiceList();
}
});
updateServiceList();
} catch (Exception e) {
e.printStackTrace();
}
}
// 更新实例列表
private void updateServiceList() {
try{
List<String> children = zooKeeper.getChildren(BASE_SERVICES + SERVICE_NAME, true);
List<String> newServerList = new ArrayList<String>();
for(String subNode:children) {
// 获取子节点
byte[] data = zooKeeper.getData(BASE_SERVICES + SERVICE_NAME + "/" + subNode, false, null);
String host = new String(data, "utf-8");
System.out.println("host:"+host);
newServerList.add(host);
}
LoadBalance.SERVICE_LIST = newServerList;
}catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
public abstract class LoadBalance {
// 实例列表
public volatile static List<String> SERVICE_LIST;
// 负载均衡
public abstract String choseServiceHost();
}
public class RamdomLoadBalance extends LoadBalance {
@Override
public String choseServiceHost() {
// 实现随机负载均衡
String result = "";
if(!CollectionUtils.isEmpty(SERVICE_LIST)) {
int index = new Random().nextInt(SERVICE_LIST.size());
result = SERVICE_LIST.get(index);
}
return result ;
}
}
@RequestMapping("/api/demo")
@RestController
public class ConsumerController {
@Resource
private RestTemplate restTemplate;
private LoadBalance loadBalance = new RamdomLoadBalance();
@RequestMapping("/consumer")
public Object doConsumer() {
return restTemplate.getForObject("http://"+loadBalance.choseServiceHost()+"/api/demo/provider", Result.class);
}
}
消费者通过监听ZK节点变化,获取服务列表,通过负载均衡算法调用不同的节点。