版權聲明:本文為博主原創文章,遵循 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