基于ZooKeeper实现简单的服务注册于发现
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Shaun_luotao/article/details/87098482
分布式系统离不开服务的注册与发现。这里我采用ZooKeeper实现一个简单的服务注册与发现的例子。
首先简单介绍一下Zookeeper的基本特性。
Zookeeper 实现了一个类似于文件系统的树状结构:
Zookeeper的数据结构
层次化的目录结构,命名符合常规文件系统规范。
每个节点在zookeeper中叫做znode,并且其有一个唯一的路径标识。
节点Znode可以包含数据和子节点(但是EPHEMERAL类型的节点不能有子节点)。
客户端应用可以在节点上设置监视器。
Zookeeper的节点类型
永久节点(除非手动删除,节点永远存在)
永久有序节点(按照创建顺序会为每个节点末尾带上一个序号如:root-1)
瞬时节点(创建客户端与 Zookeeper 保持连接时节点存在,断开时则删除并会有相应的通知)
瞬时有序节点(在瞬时节点的基础上加上了顺序)
思路:
既然Zookeeper能够在节点上保存一定的数据信息,那么我们在服务注册的时候,创建服务端的节点,并将服务的Ip地址和端口保存在Zookeeper的节点中,服务调用的时候,根据负载算法,获取到Zookeeper节点的节点数据,拿到IP地址和端口,根据IP地址和端口就能调用相应的服务。
创建三个新的SpringBoot工程,引入Zookeeper开发包。其中两个工程演示服务端,一个工程演示客户端。
服务端工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shaun.zookeeper</groupId>
<artifactId>serviceproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serviceproject</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在启动类中开启使用 @ServletComponentScan注解后,直接通过@WebListener 开启监听,启动项目的时候调用ServiceRegister,将服务端的物理地址信息注册到Zookeeper。
启动类
@SpringBootApplication
@ComponentScan(basePackages = "com.shaun.*")
@ServletComponentScan
public class ServiceprojectApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceprojectApplication.class, args);
}
}
InitListener 监听器
@WebListener
public class InitListener implements ServletContextListener{
@Override
public void contextInitialized(ServletContextEvent servletContextEvent){
Properties properties = new Properties();
try {
properties.load(InitListener.class.getClassLoader().getResourceAsStream("application.yml"));
String hostAddress = InetAddress.getLocalHost().getHostAddress();
String po = properties.getProperty("port");
int port = Integer.parseInt(po);
ServiceRegister.reister(hostAddress,port);
} catch (IOException e) {
e.printStackTrace();
}
}
}
ServiceRegister服务注册的具体实现。
public class ServiceRegister {
private static final String BASE_SERVICE = "/zookeeper";
private static final String SERVICE_NAME = "/server";
public static void reister(String address,int port){
String path = BASE_SERVICE+SERVICE_NAME;
try {
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181",5000, null);
Stat exists = zooKeeper.exists(BASE_SERVICE+SERVICE_NAME,false);
if(exists == null){
zooKeeper.create(path,"".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
String server_path = address+":"+port;
zooKeeper.create(path+"/child",server_path.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("产品服务注册成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
为什么创建znode的时候是创建临时有序节点,因为服务关闭的时候,节点会自动删除。服务端开启节点监听会刷新服务列表。
新建一个测试controller,返回服务端的地址信息。方便测试客户端调用服务端。
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/getProduct")
public Map getProduct(@RequestBody Map entity){
Map map = new HashMap();
map.put("id",entity.get("id"));
map.put("name","你好");
return map;
}
}
客户端工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shaun.zookeeper</groupId>
<artifactId>clientproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>clientproject</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
和服务端类似,不过客户端在启动监听器中需获取Zookeeper中注册的服务列表。将服务列表保存到负载均衡类中。调用服务的时候,从负载均衡类中获取注册的服务端的物理地址信息。
启动监听程序
开启了节点监听,当节点事件类型watchedEvent为NodeChildrenChanged 并且节点路径和服务端注册的节点路径一致时,说明有服务关闭,更新获取到的服务节点。将服务节点信息保存到负载均衡类中。
@WebListener
public class InitListener implements ServletContextListener{
private static final String BASE_SERVICE = "/zookeeper";
private static final String SERVICE_NAME = "/server";
private ZooKeeper zooKeeper;
@Override
public void contextInitialized(ServletContextEvent servletContextEvent){
init();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent){
}
public void init(){
try {
zooKeeper = new ZooKeeper("127.0.0.1:2181",5000, (watchedEvent -> {
if(watchedEvent.getType()== Watcher.Event.EventType.NodeChildrenChanged && watchedEvent.getPath().equals(BASE_SERVICE+SERVICE_NAME)){
updateServerList();
}
}));
updateServerList();
} catch (IOException e) {
e.printStackTrace();
}
}
public void updateServerList(){
List <String> newServiceList = new ArrayList<>();
try {
List <String> children = zooKeeper.getChildren(BASE_SERVICE+SERVICE_NAME,true);
for(String subNode:children){
byte [] data = zooKeeper.getData(BASE_SERVICE + SERVICE_NAME +"/" + subNode,false,null);
String host = new String(data,"utf-8");
System.out.println("host:"+host);
newServiceList.add(host);
}
LoadBalanse.SERVICE_LIST = newServiceList;
} catch (Exception e) {
e.printStackTrace();
}
}
}
public abstract class LoadBalanse {
public volatile static List<String> SERVICE_LIST;
public abstract String choseServiceHost();
}
public class RandomLoadBalance extends LoadBalanse {
@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;
}
}
调用示例:
@RestController
@RequestMapping("/order")
public class TestController {
private RestTemplate restTemplate = new RestTemplate();
private LoadBalanse loadBalanse = new RandomLoadBalance();
@RequestMapping("/getOrder")
public Object getProduct(@RequestBody Map entity){
String host = loadBalanse.choseServiceHost();
Map res = restTemplate.postForObject("http://"+host+"/product/getProduct",entity,Map.class);
res.put("host",host);
return res;
}
@RequestMapping("/test")
public Map test(){
return null;
}
}
启动服务端之前,我们看一下node节点,只有永久节点/zookeeper/server
启动成功后,输出服务注册成功!
再查下一下znode节点。发现child节点创建成功,说明服务注册成功。
启动客户端工程,测试/order/getOrder
根据测试结果,客户端调用8092或者8091完全是随机的。
本文只简单演示基于Zookeeper做服务注册与发现,Zookeeper还有很多特性未深入研究,示例工程后续会传到GitHub。
————————————————
版权声明:本文为CSDN博主「Shaun_luotao」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Shaun_luotao/article/details/87098482