目录
- 概念说明
- Nacos配置中心
- Naocs配置项
- Naocs配置集
- Naocs配置快照
- 需求分析
- 核心功能
- 代码实现
- AService模块
- BService模块
- NacosService模块
- NacosSDK模块
- 注意事项
- 总结提升
概念说明
Nacos注册中心:https://blog.csdn.net/weixin_45490198/article/details/131256597
Nacos配置中心
在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段。
Naocs配置项
一个具体的可配置的参数与其值域,通常以 key-value 的形式存在。
Naocs配置集
一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。
Naocs配置快照
Nacos 的客户端 SDK 会在本地生成配置的快照。当客户端无法连接到 Nacos Server 时,可以使用配置快照显示系统的整体容灾能力。配置快照类似于 Git 中的本地 commit,也类似于缓存,会在适当的时机更新,但是并没有缓存过期(expiration)的概念。
如果我们在项目中配置了快照拉取的路径,那么直接去配置的路径下就能看到拉去下来的配置文件了,如果在项目中没有指定快照的路径默认在C:\Users\Administrator下有一个nacos的文件夹。
需求分析
在我们之前分析Nacos注册中心的时候已经明确了当程序启动的时候把各个服务注册到了Nacos的服务端,那我们在实时更新各个服务的配置文件的时候,需要去通知对应服务的SDK。然后对应服务的SDK在去Nacos服务端拉取一份新的配置文件即可。
- 需要有服务端(naocs)对客户端注册的信息进行储存和管理
- 需要有客户端向服务器注册自己信息包括IP地址端口号等内容
- 需要有服务端提供的SDK,用来修改服务的配置文件时同时对应的服务
核心功能
- 「 配置管理 」:Nacos 配置中心允许将应用程序的配置信息集中存储和管理。可以通过 Web 控制台或 API 创建、更新、删除和查询配置信息。配置信息可以是键值对、JSON、XML 等格式。
- 「 动态配置 」:Nacos 配置中心支持动态配置,即应用程序可以在运行时动态获取最新的配置信息。当配置信息发生变化时,Nacos 会自动通知应用程序进行更新,无需重启应用程序。
- 「 配置共享 」:多个应用程序可以共享同一份配置信息,避免了配置信息的重复存储和管理。通过配置的分组和命名空间,可以实现不同应用程序之间的配置隔离和共享。
- 「配置监听 」:Nacos 配置中心支持配置的监听功能,应用程序可以注册监听器,当配置信息发生变化时,监听器会收到通知并执行相应的操作。
- 「 其他功能」:Nacos还支持其他的功能例如配置文件版本记录 、版本回滚等功能,本文中没有具体的说明。
代码实现
AService模块
业务部分只需要从nacos中获取IP地址请求其他服务器即可。
import com.example.client.Controller.SDKController;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @BelongsProject: ServiceA
* @BelongsPackage: com.example.servicea.Controller
* @Author: Wuzilong
* @Description: A服务
* @CreateTime: 2023-06-06 18:43
* @Version: 1.0
*/
@RestController
@RequestMapping("/A")
public class ServiceAController {
@Autowired
SDKController sdkController;
@GetMapping("/getServiceIp")
public void getServiceIp() throws JsonProcessingException {
String serviceIp = sdkController.random("B");
String url = "http://"+serviceIp+"/B/receiveMessage";
RestTemplate restTemplate=new RestTemplateBuilder().build();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
if (forEntity.getStatusCode() == HttpStatus.OK) {
System.out.println("调用B服务成功!IP地址为"+serviceIp);
}
}
}
配置文件中需要配置连接nacos服务的地址以及本服务的信息
server:
port: 9001
name: A
url: localhost:9000
key: ip
在pom文件中引入我们自己封装的NacosSDK服务
<dependency>
<groupId>com.example</groupId>
<artifactId>SDK</artifactId>
<version>2.5-20230615.123611-1</version>
</dependency>
BService模块
业务部分只需要编写响应A服务调用B服务的接口即可,说明调用成功
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @BelongsProject: ServiceB
* @BelongsPackage: com.example.serviceb.Controller
* @Author: Wuzilong
* @Description: B服务
* @CreateTime: 2023-06-07 19:08
* @Version: 1.0
*/
@RestController
@RequestMapping("/B")
public class ServiceBController {
@GetMapping("/receiveMessage")
public void receiveMessage(){
System.out.println("B:我被调用了");
}
}
配置文件中需要配置连接nacos服务的地址以及本服务的信息
server:
port: 9002
name: B
url: localhost:9000
key: ip
在pom文件中引入我们自己封装的NacosSDK服务
<dependency>
<groupId>com.example</groupId>
<artifactId>SDK</artifactId>
<version>2.5-20230615.123611-1</version>
</dependency>
B服务可以启动多个为了验证负载均衡。当有高并发请求的时候我们可以把请求的压力分配到每一个B服务上,减少只有一个B服务的压力。还有就是当一个B服务不能正常访问的时候我们访问其他的B服务。
NacosService模块
nacos服务端主要的服务:在注册中心的基础上添加了更新配置文件通知对应服务的SDK拉取最新的配置文件
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* @BelongsProject: Serve
* @BelongsPackage: com.example.controller
* @Author: Wuzilong
* @Description: nacos服务端
* @CreateTime: 2023-06-05 20:23
* @Version: 1.0
*/
@RestController
@RequestMapping("/nacosServe")
public class ConfigCenterController {
Map<String,Map<String,String>> configCenter =new HashMap<>();
Map<String,Map<String,String>> registerCenter =new HashMap<>();
/**
* @Author:Wuzilong
* @Description: 将主机的信息注册进来并通知sdk更新注册列表
* @CreateTime: 2023/6/9 10:25
* @param: 主机信息
* @return: void
**/
@PostMapping(value = {"/setRegisterContext"})
public void setRegisterContext( @RequestBody Map<String, Map<String,String>> registerContext) throws Exception {
registerCenter.putAll(registerContext);
System.out.println(registerContext.keySet().toString().replaceAll("\\[|\\]", "")+"服务,注册成功"+"注册的内容是"+registerCenter);
if(registerCenter.size()>1){
for (Map.Entry<String, Map<String,String>> entry:registerCenter.entrySet()){
for (Map.Entry<String,String> entry1:entry.getValue().entrySet()){
// 发送POST请求
String url = "http://"+entry1.getValue()+"/"+"/configClientServe"+"/getRegisterContext";
RestTemplate restTemplate=new RestTemplateBuilder().build();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
// 处理响应
if (forEntity.getStatusCode() == HttpStatus.OK) {
System.out.println("注册列表更新了,通知了"+entry.getKey()+"服务的SDK");
}
}
}
}
}
/**
* @Author:Wuzilong
* @Description: 更新配置文件并通知配置文件对应的服务拉取新的配置文件
* @CreateTime: 2023/6/9 10:26
* @param: 更新的配置文件
* @return: void
**/
@PostMapping(value = {"/setConfigContext"})
public void setConfigContext( @RequestBody Map<String, Map<String,String>> configContext) throws Exception {
//更新A服务的配置文件
configCenter.putAll(configContext);
//通知对应服务的SDK获取最新的配置文件
Map<String, String> stringStringMap = registerCenter.get(configContext.keySet().toString().replaceAll("\\[|\\]", ""));
String serverIp = stringStringMap.get("ip");
String url = "http://"+serverIp+"/"+"/configClientServe"+"/getConfigContext/?serverName="+configContext.keySet().toString().replaceAll("\\[|\\]", "");
RestTemplate restTemplate=new RestTemplateBuilder().build();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
// 处理响应
if (forEntity.getStatusCode() == HttpStatus.OK) {
System.out.println("配置文件更新了,通知了"+configContext.keySet()+"服务");
}
}
//返回更新后的配置信息
@GetMapping(value = {"/getConfigContext"})
public Map<String,String> getConfigContext(String serviceName){
return configCenter.get(serviceName);
}
//返回更新后的注册列表
@GetMapping(value = {"/getRegisterContext"})
public Map<String,Map<String,String>> getRegisterContext(){
return registerCenter;
}
}
NacosSDK模块
SDK是我们自己封装的用来让其他客户端集成使用的,其中包括了:项目启动把客户端注册到注册列表中、接收到注册列表更新的消息拉取最新的注册列表、负载均衡的两种策略(轮询和随机)和最新的配置文件
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.InetAddress;
import java.util.*;
/**
* @BelongsProject: Client
* @BelongsPackage: com.example.client.Controller
* @Author: Wuzilong
* @Description: nacos提供的sdk
* @CreateTime: 2023-06-06 19:40
* @Version: 1.0
*/
@RestController
@RequestMapping("/configClientServe")
@PropertySource("classpath:application.yml")
public class SDKController implements ApplicationRunner{
public Map<String,String> configCenter =new HashMap<>();
public Map<String, Map<String,String>> registerCenter =new HashMap<>();
int index = 0;
@Value("${server.port}")
private String serverPort;
@Value("${server.name}")
private String serverName;
@Value("${server.url}")
private String serverIp;
@Value("${server.key}")
private String serverKey;
@GetMapping(value = {"/getConfigContext"})
public void getConfigContext(String serverName) throws JsonProcessingException {
String url = "http://"+serverIp+"/nacosServe/getConfigContext?serviceName="+serverName;
RestTemplate restTemplate=new RestTemplateBuilder().build();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
if (forEntity.getStatusCode() == HttpStatus.OK) {
String body = forEntity.getBody();
configCenter = new ObjectMapper().readValue(body, new TypeReference<>() {});
System.out.println("获取了"+serverName+"服务最新的配置文件,配置文件的内容是"+configCenter);
}
}
//获取server中的注册列表
@GetMapping(value = {"/getRegisterContext"})
public void getRegisterContext() throws JsonProcessingException {
System.out.println("注册列表更新了,去拉取一份新的注册列表");
String url = "http://"+serverIp+"/nacosServe/getRegisterContext";
RestTemplate restTemplate=new RestTemplateBuilder().build();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
if (forEntity.getStatusCode() == HttpStatus.OK) {
String body = forEntity.getBody();
ObjectMapper objectMapper = new ObjectMapper();
registerCenter = objectMapper.readValue(body, new TypeReference<>() {});
System.out.println("新的注册列表拉取完毕,注册列表的内容为"+registerCenter);
}
}
//项目启动后把本服务的信息注册到nacosServe上
@Override
public void run(ApplicationArguments args) throws Exception {
String url = "http://"+serverIp+"/nacosServe/setRegisterContext/";
RestTemplate restTemplate=new RestTemplateBuilder().build();
Map<String, Object> requestBody = new HashMap<>();
Map<String,String> param=new HashMap<>();
param.put(serverKey, InetAddress.getLocalHost().getHostAddress()+":"+serverPort);
requestBody.put(serverName, param);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody);
restTemplate.postForEntity(url,request,String.class);
}
//轮询获取服务的IP地址
public String polling(String serverName){
List<String> pollingList = this.getList(serverName);
String ipContext = pollingList.get(index);
index=(index+1)%pollingList.size();
return ipContext;
}
//随机获取可用的IP地址
public String random(String serverName){
List<String> randomList = this.getList(serverName);
Random random =new Random();
int randomIndex = random.nextInt(randomList.size());
return randomList.get(randomIndex);
}
//获取客户端想要请求服务的可用IP地址
public List<String> getList(String serverName){
List<String> list=new ArrayList<>();
for (Map.Entry<String, Map<String,String>> entry:registerCenter.entrySet()){
if(entry.getKey().contains(serverName)){
for (Map.Entry<String,String> entry1:entry.getValue().entrySet()){
list.add(entry1.getValue());
}
}
}
return list;
}
}
注意事项
- 找到一个项目一启动就会触发的操作,保证只要启动就会将此服务的信息同步放到注册表中。
- 想到要写一个SDK服务,将其打成jar包,放到各个业务服务中,通过嵌入的方式和读取配置文件的方式,实现服务之间的调用
- 使用注解,可以获取配置文件中的写的端口号等信息,可以进行灵活变更。
- 使用restTemplate实现服务之间的调用
- 引入SDK服务需要考虑启动类和业务类的路径,确保程序启动能够扫描到引入的业务类
总结提升
- 思想上移,行动下移:之前对nacos配置中心都是在概念,通过手动实现把nacos注册中心写出来之后对于nacos配置中心有了更深入的理解
- 知其然也要知其所以然:之气只是简单的使用,哪里出现了问题也不清楚只能靠蒙和猜来解决问题。现在可以非常明确的知道是哪个环节出现了问题。底层原理明确使用起来也非常的简单。
- 不断迭代完善:这个版本没有添加每次修改配置文件的记录和每个版本配置文件的回滚等相关的内容。