文章目录
- 1.什么是配置管理
- 2.配置共享
- 添加共享配置
- 拉取共享配置
- 3.配置热更新
- 添加配置到Nacos
- 配置热更新
- 4.动态路由
- 监听Nacos配置变更
1.什么是配置管理
到目前为止我们已经解决了微服务相关的几个问题:
- 微服务远程调用
- 微服务注册、发现
- 微服务请求路由、负载均衡
- 微服务登录用户信息传递
不过,现在依然还有几个问题需要解决:
- 网关路由在配置文件中写死了,如果变更必须重启微服务
- 某些业务配置在配置文件中写死了,每次修改都要重启服务
- 每个微服务都有很多重复的配置,维护成本高
这些问题都可以通过统一的配置管理器服务解决。而Nacos不仅仅具备注册中心功能,也具备配置管理的功能:
微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。
网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。
2.配置共享
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
- 在Nacos中添加共享配置
- 微服务拉取配置
添加共享配置
以cart-service为例,我们看看有哪些配置是重复的,可以抽取的:
首先是jdbc相关配置:
注意这里的jdbc的相关参数并没有写死,例如:
- 数据库ip:通过
${hm.db.host:192.168.101.129}
配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值 - 数据库端口:通过
${hm.db.port:3306}
配置了默认值为3306,同时允许通过${hm.db.port}
来覆盖默认值 - 数据库database:可以通过
${hm.db.database}
来设定,无默认值
拉取共享配置
基于NacosConfig拉取共享配置代替微服务的本地配置.
刚开始我们的项目还没有进行拉取,是一个普通的SpringBoot项目,内部加载配置文件是这样的一个顺序:项目一启动,直接去读取application.yml,它会基于这些配置去完成ApplicationContext的初始化,我们知道ApplicationContext 是Spring的bean工厂或者叫整个Spring上下文或者叫容器.
一旦引入了配置管理,那这样的项目就跟传统的SpringBoot项目不一样了,它就会加入一个SpringCloud上下文,这个SpringCloud上下文在已启动会先尝试去拉取Nacos配置,基于这些配置会去完成SpringCloud上下文的ApplicatonContext的初始化.当这个初始化完成了才会去完成SpringBoot的配置文件加载和SpringBoot上下文初始化.但是问题来了: 我们的Nacos地址是配置在application.yml文件中的,现在你一上来你就要去拉取Nacos配置还没有去读取application.yml呢,这样就产生矛盾.
基于这个问题,SpringCloud引出一个新的文件bootstrap.yml [引导配置文件],项目启动先读取这个文件,我们可以把Nacos地址配置到这个文件中
文件好创建,但是文件将来被拉取,加载配置,初始化,这个过程谁来处理呢?
因此,微服务整合Nacos配置管理的步骤如下:
1)引入依赖:
在cart-service模块引入依赖:
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2)新建bootstrap.yaml
在cart-service中的resources目录新建一个bootstrap.yaml文件:
内容如下:
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
3)修改application.yaml
由于一些配置挪到了bootstrap.yaml,因此application.yaml需要修改为:
server:
port: 8082
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持
hm:
swagger:
title: 购物车服务接口文档
package: com.hmall.cart.controller
db:
database: hm-cart
重启服务,发现所有配置都生效了。
3.配置热更新
配置热更新: 当修改配置文件中的配置时,微服务无需重启
即可使配置生效.
有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10,对应代码如下:
现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。
但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?
这就要用到Nacos的配置热更新能力了,分为两步:
- 在Nacos中添加配置
- 在微服务读取配置
添加配置到Nacos
首先,我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中:
注意文件的dataId格式:
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
- 服务名:我们是购物车服务,所以是cart-service
- spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
- 后缀名:例如yaml
这里我们直接使用cart-service.yaml这个名称,则不管是dev还是local环境都可以共享该配置。
配置内容如下:
hm:
cart:
maxAmount: 1 # 购物车商品数量上限
提交配置,在控制台能看到新添加的配置:
配置热更新
接着,我们在微服务中读取配置,实现配置热更新。
在cart-service中新建一个属性读取类:
package com.hmall.cart.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxItems;
}
接着,在业务中使用该属性加载类:
重启后进行测试即可.
4.动态路由
动态路由是Nacos配置管理的一个应用,目前我们的路由不是动态的,而是写死在网关的配置文件中的,当网关启动的时候就会去加载配置文件路由信息,保存在内存中的一个路由表里面,其实就是一个缓存,那么下次有请求直接读取缓存就可以了,提高速度.那么这也带来了一个问题,假设我们项目运行过程中,我们的路由信息需要发生变更,那这个时候光去改配置文件就不行了,因为它已经被缓存起来了,所以你必须去重启网关服务才行,但是网关可是整个服务的入口,它怎么能随便停机呢?它停机可想而知对整个微服务会带来非常大的影响,所以我们想实现的效果是,我们改路由立马生效不用重启网关,这就是动态路由.
监听Nacos配置变更
在Nacos官网中给出了手动监听Nacos配置变更的SDK:
https://nacos.io/zh-cn/docs/sdk.html
这里核心的步骤有2步:
- 创建ConfigService,目的是连接到Nacos
- 添加配置监听器,编写配置变更的通知处理逻辑
由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配,因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigManager中自动创建好了:
这里面还提供了getConfigService()方法
最终代码:
package com.hmall.gateway.routers;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.Executor;
@Component
@Slf4j
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final NacosConfigManager nacosConfigManager;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
@PostConstruct //在这个bean初始化之后执行
public void initRouteConfigListener() throws NacosException {
//1.项目启动时,先拉取一个配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {//这里逻辑比较简单就不使用线程池了
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
//2.监听到配置变更,需要去更新路由表
updateConfigInfo(configInfo);
}
});
//3.第一次读取到配置,也需要更新到路由表
updateConfigInfo(configInfo);
}
public void updateConfigInfo(String configInfo){
//TODO
}
}
package com.hmall.gateway.routers;
import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
@Component
@Slf4j
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter writer;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
private final Set<String> routeIds = new HashSet<>();
@PostConstruct //在这个bean初始化之后执行
public void initRouteConfigListener() throws NacosException {
//1.项目启动时,先拉取一个配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {//这里逻辑比较简单就不使用线程池了
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
//2.监听到配置变更,需要去更新路由表
updateConfigInfo(configInfo);
}
});
//3.第一次读取到配置,也需要更新到路由表
updateConfigInfo(configInfo);
}
public void updateConfigInfo(String configInfo){
log.debug("监听到路由配置信息:{}",configInfo);
// 1.解析配置信息,转为RouteDefinition
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
//2.删除旧的路由表
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
//3.更新路由表
for (RouteDefinition routeDefinition : routeDefinitions) {
//3.1 更新路由表
writer.save(Mono.just(routeDefinition)).subscribe();//响应式编程处理方案
//3.2 记录路由id,便于下一次更新时删除
routeIds.add(routeDefinition.getId());
}
}
}
重启网关,任意访问一个接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1:
发现是404,无法访问。
接下来,我们直接在Nacos控制台添加路由,路由文件名为gateway-routes.json,类型为json:
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/carts/**"}
}],
"filters": [],
"uri": "lb://cart-service"
},
{
"id": "user",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
}],
"filters": [],
"uri": "lb://user-service"
},
{
"id": "trade",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/orders/**"}
}],
"filters": [],
"uri": "lb://trade-service"
},
{
"id": "pay",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/pay-orders/**"}
}],
"filters": [],
"uri": "lb://pay-service"
}
]
无需重启网关,稍等几秒钟后,再次访问刚才的地址:
成功了!