Nacos
Nacos 简介
Nacos 是阿里巴巴开源的一款动态服务发现、配置管理和服务管理平台,支持 DNS 和 RPC 服务发现,提供分布式配置管理功能。适用于微服务架构中的服务注册、配置中心和服务治理场景。
Nacos 核心功能
服务发现与注册
Nacos 支持动态服务发现,允许服务实例自动注册和注销。服务提供者启动时向 Nacos 注册自身信息,消费者通过 Nacos 查询可用服务列表,实现服务间的动态调用。支持 DNS 和 RPC 两种服务发现模式,兼容 Spring Cloud、Dubbo 等主流框架。
配置管理
提供分布式系统的配置中心功能,支持配置的发布、更新、删除和监听。配置变更实时推送到客户端,无需重启应用。支持多环境(如开发、测试、生产)配置隔离,以及配置版本管理和回滚。
动态 DNS 服务
通过权重管理和健康检查机制,实现流量的动态路由。支持基于权负载均衡策略(如轮询、随机、一致性哈希),帮助优化服务调用分布。
服务健康监测
主动检测注册服务的健康状态,自动剔除异常实例。支持 TCP、HTTP 和 MySQL 等多种健康检查方式,确保服务调用的可靠性。
集群管理与容灾
支持多节点集群部署,具备 AP(高可用)和 CP(强一致性)两种集群模式。内置 Raft 协议保证数据一致性,并能跨机房容灾,保障高可用性。
命名空间与分组
通过命名空间(Namespace)实现多租户隔离,分组(Group)用于进一步细分服务或配置。适用于大型企业多团队协作场景。
扩展性与插件机制
提供 SPI 扩展接口,支持自定义实现如认证、配置加密等插件。可集成 Prometheus 等监控工具,增强可观测
Nacos结构图
nacos安装
在本地部署安装
首先下载nacos压缩包,可以在发布历史 | Nacos 官网上下载,链接里包含历史多个版本挑选安装。
点击版本链接即可下载压缩包
打开bin目录, 输入命令:startup.cmd -m standalone 即可开启成功
最后访问http://localhost:8848/nacos 打开nacos界面,账户密码:默认都为nacos;
在虚拟机上部署
我们基于Docker来部署Nacos的注册中心
利用docker run 命令,
docker run -d \ --name nacos \ -p 8848:8848 \
-p 9848:9848 \ -p 9849:9849 \ --restart=always \ nacos/nacos-server:v2.1.0-slim
启动完成后,访问下面地址:http://192.168.150.101:8848/nacos/,注意将192.168.150.101
替换为你自己的虚拟机IP地址。
Nacos 服务注册
引入依赖
配置Nacos地址
重启
添加依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置Nacos
在cart-service
的application.yml
中添加nacos地址配置:
spring:
application:
name: cart-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
启动服务实例
为了测试一个服务多个实例的情况,我们再配置多个;cart-service
的部署实例:
注意:一定要更改更改端口号,防止端口占用。
成功将服务启动后,我们可以返回到nacos界面,找到服务管理->服务列表,即可看到我们部署的项目
Nacos 服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
引入依赖
配置Nacos地址
发现并调用服务
引入依赖
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
我们在cart-service
中的pom.xml
中添加下面的依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
可以发现,这里Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
配置Nacos地址
在cart-service
的application.yml
中添加nacos地址配置:
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
配置中心
配置共享
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
在Nacos中添加共享配置
微服务拉取配置
添加共享配置
我们在nacos控制台分别添加这些配置。
首先是jdbc相关配置,在配置管理
->配置列表
中点击+
新建一个配置:
在弹出的表单中填写信息:
注意这里的jdbc的相关参数并没有写死,例如:
数据库ip
:通过${hm.db.host:192.168.150.101}
配置了默认值为192.168.150.101
,同时允许通过${hm.db.host}
来覆盖默认值数据库端口
:通过${hm.db.port:3306}
配置了默认值为3306
,同时允许通过${hm.db.port}
来覆盖默认值数据库database
:可以通过${hm.db.database}
来设定,无默认值
拉取共享配置
接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml
配置合并,完成项目上下文的初始化。
SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml
(或者bootstrap.properties
)的文件,如果我们将nacos地址配置到bootstrap.yaml
中,那么在项目引导阶段就可以读取nacos中的配置了。
因此,微服务整合Nacos配置管理的步骤如下:
1)引入依赖:
在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>
新建bootstrap.yaml
在service中的resources目录新建一个bootstrap.yaml文件:
最后更改application.yaml文件
重启服务,发现所有配置都生效了
配置热更新
修改了配置不用重启,直接生效
分为两步:
在Nacos中添加配置
在微服务读取配置
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
服务名
:我们是购物车服务,所以是cart-service
spring.active.profile
:就是spring boot中的spring.active.profile
,可以省略,则所有profile共享该配置后缀名
:例如yaml
在微服务读取配置类似:
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}
扩展
DiscoveryClient
loadBalancerClient
DiscoveryClient 和 LoadBalancerClient 是 Spring Cloud 中用于服务发现和负载均衡的两个核心组件。它们通常结合使用以实现微服务架构中的动态服务调用。
DiscoveryClient
- 负责从注册中心(如 Eureka、Consul、Zookeeper)获取服务实例列表。
- 提供查询服务实例的基本接口,例如
getInstances(serviceId)
。 - 是服务发现的核心抽象,允许应用动态发现其他服务。
LoadBalancerClient
- 基于 DiscoveryClient 获取的服务实例列表,实现客户端负载均衡。
- 提供
choose(serviceId)
方法选择具体的服务实例,并通过execute(serviceId, request)
执行请求。 - 默认实现为
RibbonLoadBalancerClient
(基于 Netflix Ribbon)。
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
@Override
public Order createOrder(Long userId, Long productId) {
//1.远程调用商品服务,获取商品信息
Product product = getProductFromRemote(productId);
Order order = new Order();
order.setId(1L);
// TOOO 总金额
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("zhangsan");
order.setAddress("加菲猫大街 1 号");
order.setProductList(Arrays.asList(product));
return order;
}
//
private Product getProductFromRemoteWithLoadBalancer(Long productId) {
// 使用负载均衡器获取商品服务的实例
ServiceInstance serviceInstance = loadBalancerClient.choose("services-product");
//2.拼接请求地址url
String url ="http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" +productId;
//3.发送远程请求
log.info("请求商品服务,url:{}", url);
return restTemplate.getForObject(url, Product.class);
}
private Product getProductFromRemote(Long productId) {
//1.获取到商品服务器所有机器IP+port
List<ServiceInstance> instances = discoveryClient.getInstances("services-product");
ServiceInstance serviceInstance = instances.get(0);
//2.拼接请求地址url
String url ="http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" +productId;
//3.发送远程请求
log.info("请求商品服务,url:{}", url);
return restTemplate.getForObject(url, Product.class);
}
//基于注解的负载均衡
private Product getProductFromRemoteWithLoadBalancerAnnotation(Long productId) {
String url ="http://services-product/product/" +productId;
//3.发送远程请求
log.info("请求商品服务,url:{}", url);
return restTemplate.getForObject(url, Product.class);
}
}
Openfeign
引入依赖
在cart-service
服务的pom.xml中引入OpenFeign
的依赖和loadBalancer
依赖:
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
启用OpenFeign
接下来,我们在cart-service
的CartApplication
启动类上添加注解,启动OpenFeign功能:
编写OpenFeign客户端
在cart-service
中,定义一个新的接口,编写Feign客户端:
其中代码如下:
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
@FeignClient("item-service")
:声明服务名称@GetMapping
:声明请求方式@GetMapping("/items")
:声明请求路径@RequestParam("ids") Collection<Long> ids
:声明请求参数List<ItemDTO>
:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。
我们只需要直接调用这个方法,即可实现远程调用了。
连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
HttpURLConnection:默认实现,不支持连接池
Apache HttpClient :支持连接池
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
引入依赖
在cart-service
的pom.xml
中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
在cart-service
的application.yml
配置文件中开启Feign的连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。
OpenFeign拦截器
微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
public interface RequestInterceptor {
/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
定义一个配置类:实现RequestInterceptor接口
public class DefaultFeignConfig {
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}
}
最后要实现拦截器,要在openFilent客户端的启动类上加上类名
日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
定义日志级别
新建一个配置类,定义Feign的日志级别:
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
配置
接下来,要让日志级别生效,还需要配置这个类。有两种方式:
局部生效:在某个
FeignClient
中配置,只对当前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
全局生效:在
@EnableFeignClients
中配置,针对所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
网关路由(Gateway)
网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
前端请求不能直接访问微服务,而是要请求网关:
网关可以做安全控制,也就是登录身份校验,校验通过才放行
通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
Gateway
内部工作的基本原理
如图所示:
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为Filter
)。图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。微服务返回结果后,再倒序执行
Filter
的post
逻辑。最终把响应结果返回。
Gateway快速入门
网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。
创建网关微服务
引入依赖
SpringCloudGateway、NacosDiscovery
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
编写启动类
网关路由配置
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
路由过滤
路由规则的定义语法如下:
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
default-filters:
-AddRequestHeader=truth, anyone long-press like button will be rich
四个属性含义如下:
id
:路由的唯一标示predicates
:路由断言,其实就是匹配条件filters
:路由过滤条件uri
:路由目标地址,lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。default-filters 为网关配置全局过滤
断言 predicates
名称 |
说明 |
示例 |
---|---|---|
After |
是某个时间点后的请求 |
- After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before |
是某个时间点之前的请求 |
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between |
是某两个时间点之前的请求 |
- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie |
请求必须包含某些cookie |
- Cookie=chocolate, ch.p |
Header |
请求必须包含某些header |
- Header=X-Request-Id, \d+ |
Host |
请求必须是访问某个host(域名) |
- Host=**.somehost.org,**.anotherhost.org |
Method |
请求方式必须是指定方式 |
- Method=GET,POST |
Path |
请求路径必须符合指定规则 |
- Path=/red/{segment},/blue/** |
Query |
请求参数必须包含指定参数 |
- Query=name, Jack或者- Query=name |
RemoteAddr |
请求者的ip必须是指定范围 |
- RemoteAddr=192.168.1.1/24 |
weight |
权重处理 |
过滤器 Filter
网关过滤器链中的过滤器有两种:
GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
.GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
自定义过滤器
无论是GatewayFilter
还是GlobalFilter
都支持自定义,只不过编码方式、使用方式略有差别。
自定义GlobalFilter
则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:
//自定义全局过滤器
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 在这里可以添加自定义的过滤逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println("header = " + headers);
//放行
return chain.filter(exchange)
/*.doOnSuccess(aVoid -> {
// 可以在这里添加后置逻辑
System.out.println("请求处理完成");
})
.doOnError(throwable -> {
// 可以在这里添加错误处理逻辑
System.err.println("请求处理出错: " + throwable.getMessage());
})*/;
}
// 设置过滤器的执行顺序
@Override
public int getOrder() {
return 0;
}
}
自定义 无参的 GatewayFilter
自定义GatewayFilter
不是直接实现GatewayFilter
,而是实现AbstractGatewayFilterFactory
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("PrintAnyGatewayFilterFactory: 过滤器被调用");
return chain.filter(exchange);
}
},1);
}
}
自定义 带参的 GatewayFilter
需要在无参的基础上添加config内部类
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("PrintAnyGatewayFilterFactory: 配置参数 a = " + config.getA());
System.out.println("PrintAnyGatewayFilterFactory: 配置参数 b = " + config.getB());
System.out.println("PrintAnyGatewayFilterFactory: 配置参数 c = " + config.getC());
System.out.println("PrintAnyGatewayFilterFactory: 过滤器被调用");
return chain.filter(exchange);
}
},1);
}
@Data
public static class Config{
private String a ;
private String b;
private String c;
}
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
}