springcloud

springcloud

cccs7 Lv5

1_pNG0JHmOr3Zp83_hkGyR_A

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g.
configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state).
Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns.
They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.

Spring Cloud提供了一些工具,供开发人员快速构建分布式系统中的常见模式(例如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态等)。
分布式系统的协调会导致样板式的模式,并且使用Spring Cloud,开发人员可以快速搭建实现这些模式的服务和应用程序。
它们在任何分布式环境中都能很好地工作,包括开发人员自己的笔记本电脑、裸机数据中心和诸如Cloud Foundry之类的托管平台。

官方文档: https://spring.io/projects/spring-cloud

特性

Spring Cloud专注于为典型的使用情况提供良好的开箱即用体验,并提供可扩展性机制以覆盖其他使用情况。

  • 分布式/版本化配置
  • 服务注册和发现
  • 路由
  • 服务间调用
  • 负载均衡
  • 断路器
  • 全局锁
  • 领导选举和集群状态
  • 分布式消息传递

springcloud 入门

服务拆分与远程调用

本节提供的 demo 在 github 仓库:https://github.com/cs7eric/cccs7-demo-repo 下的 cloud-demo

任何的分布式架构都离不开服务的拆分,微服务也一样

服务拆分原则

微服务拆分原则是在设计和实施微服务架构时,将整个系统拆分成多个小而独立的服务的指导原则。以下是一些常见的微服务拆分原则:

  1. 单一职责原则:每个微服务应该只负责一个特定的业务功能或领域。这样可以确保每个微服务的职责清晰,易于维护和扩展。
  2. 高内聚原则:每个微服务内部的组件和功能应该高度相关,共同完成一个明确的任务。这样可以提高代码的可读性和可维护性。
  3. 松耦合原则:微服务之间应该尽量减少依赖关系,通过定义明确定义的接口进行通信。这样可以降低微服务之间的耦合度,使系统更加灵活和可扩展。
  4. 可独立部署原则:每个微服务应该可以独立地进行部署和升级,而不会影响其他微服务的正常运行。这样可以提高系统的可用性和可维护性。
  5. 水平扩展原则:根据业务需求和负载情况,可以通过增加相同类型的微服务实例来实现水平扩展。这样可以提高系统的性能和容错能力。
  6. 业务边界原则:微服务的拆分应该根据业务领域的边界进行,每个微服务应该关注特定的业务功能。这样可以使团队更加专注和高效地开发和维护各自的微服务。
  • 不同微服务,不要开发 重复 的业务
  • 微服务数据独立,不要访问其他微服务的数据库
  • 微服务可以将自己的业务暴露为 接口,供其他的微服务调用
image-20230809195644664

服务拆分示例

以微服务cloud-demo为例,其结构如下:

image-20210713211009593

cloud-demo:父工程,管理依赖

  • order-service:订单微服务,负责订单相关业务
  • user-service:用户微服务,负责用户相关业务

要求:

  • 订单微服务和用户微服务都必须有各自的数据库,相互独立
  • 订单服务和用户服务都对外暴露Restful的接口
  • 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库

实现远程调用 案例

order-service 服务中,有一个 根据 id 查询 订单的 接口

image-20230809200051391

根据 id 查询订单,返回值是 order 对象,如图:

image-20230809200139844

这里是因为我已经实现了 远程调用,用 user.id 查询到了 user 信息,所以数据中是有 user 信息的,未实现之前应该是 null

user-service 中有一个 根据 user.id 查询 用户的接口

image-20230809200341439

查询结果如下:

image-20230809200431805
案例需求

修改order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回。

image-20230809200507747

因此,我们需要 在 order-service 中 向user-service 中 发起一个 http 请求,调用 http://localhost:8081/user/{userId} 这个接口

步骤 如下:

  1. 注册一个RestTemplate的实例到Spring容器
  2. 修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User
  3. 将查询的User填充到Order对象,一起返回
注册 RestTemplate

首先,在 order-service 服务中 的 OrderApplication 启动类中, 注册 RestTemplate 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.itcast.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

}
实现远程调用

修改order-service服务中的 OrderService 类中的queryOrderById方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.itcast.order.service;

import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);

// 远程查询 user
//url 地址
String url = "http://localhost:8081/user/" + order.getUserId();
//调用
User user = restTemplate.getForObject(url, User.class);
// 存入 order
order.setUser(user);
// 4.返回
return order;
}
}
提供者与消费者

在服务调用关系中,会有两个不同的角色

在服务调用关系中,会有两个不同的角色:

服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)

服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

image-20210713214404481

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。

如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?

  • 对于A调用B的业务而言:A是服务消费者,B是服务提供者
  • 对于B调用C的业务而言:B是服务消费者,C是服务提供者

因此,服务B既可以是服务提供者,也可以是服务消费者。

Eureka 注册中心

假如 服务提供商 user-service 部署了多个实例:

image-20230810190734891

会有以下几个问题:

  • order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?
  • 有多个user-service实例地址,order-service调用时该如何选择?
  • order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

Eureka 的结构与作用

以上的问题都需要利用 Springcloud 中的注册中心来进行解决,Eureka 就是其中之一

image-20230810190900555

解决以上问题:

问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

  • user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
  • eureka-server保存服务名称到服务实例地址列表的映射关系
  • order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取

问题2:order-service如何从多个user-service实例中选择具体的实例?

  • order-service从实例列表中利用负载均衡算法选中一个实例地址
  • 向该实例地址发起远程调用

问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

  • user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
  • 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
  • order-service拉取服务时,就能将故障实例排除了

注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端

所以,接下来步骤为:

image-20230810190956641

搭建 Eureka-server

搭建 注册中心服务端,这必须是一个独立的 微服务

引入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
编写启动类

eureka-server 服务 编写一个启动类,一定要添加一个 @EnableEurekaServer 注解,开启 Eureka 的注册中心功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cccs7.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
* <p> Eureka 启动类 </p>
*
* @Author cccs7/cs7eric - csq020611@gmail.com
* @Description Eureka 启动类
* @Date 2023/8/9 16:12
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
编写配置文件

编写一个 application.yml 配置文件

1
2
3
4
5
6
7
8
9
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动服务

在浏览器访问 : http://127.0.0.1:10086

image-20230810193004907

服务注册

user-service 注册到 eureka-server

引入依赖

user-service 的 pom 文件中,添加以下依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件

在user-service中,修改application.yml文件,添加 服务名称eureka地址

1
2
3
4
5
6
7
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动多个 user-service 实例

直接复制application 更改端口号即可

Edit Configuration —— VM optioins

-Dserver.port=8082

然后启动示例,查看 eureka-server 的管理页面

image-20230810193410808

服务发现

接下来,我们 将 order-service 的逻辑修改: 向 eureka-server 拉取 user-service 的信息,实现服务发现

引入依赖

服务发现、服务注册 统一封装在 ·eureka-client 依赖,因此与 服务注册时一致,先添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件

服务发现也需要知道 eureka 的 服务地址,因此第二步也是配置 Eureka 信息

order-service application.yml 中 配置 Eureka 信息: 添加 服务名称、 Eureka 地址

1
2
3
4
5
6
7
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
服务拉取与负载均衡

最后,我们需要去 eureka-server 中拉取 user-service 服务的实例列表,并实现负载均衡

只需要我们添加 注解即可

order-serviceOrderApplication ,给 RestTemplate 添加一个 @LoadBalanced 注解即可

1
2
3
4
5
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径,用服务名代替ip、端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);

// 远程查询 user
//url 地址
// String url = "http://localhost:8081/user/" + order.getUserId();
String url = "http://userservice/user/" + order.getUserId();
//调用
User user = restTemplate.getForObject(url, User.class);
// 存入 order
order.setUser(user);
// 4.返回
return order;
}

spring 会自动帮我们 从eureka-server 端, 根据 userservice 这个服务名称,获取实例列表,而后 完成负载均衡

Ribbon 负载均衡

上面说到,我们添加了 @LoadBalanced 注解,即可实现 负载均衡功能,这是什么原理?

概述

Ribbon是Spring Cloud中的一个客户端负载均衡组件,它可以在多个服务提供者之间进行负载均衡,从而实现服务的高可用性和可伸缩性。Ribbon的负载均衡原理是通过在客户端中维护一个服务列表,并根据一定的负载均衡策略选择其中一个服务提供者来处理请求。Ribbon的负载均衡过程如下:

  1. 客户端向服务注册中心获取服务列表。
  2. 客户端维护服务列表,并根据一定的负载均衡策略选择其中一个服务提供者来处理请求。
  3. 客户端向所选的服务提供者发送请求,并等待响应。
  4. 如果所选的服务提供者无法响应请求,客户端会重新选择另一个服务提供者来处理请求。

Ribbon支持多种负载均衡策略,包括轮询策略、随机策略、加权轮询策略、加权随机策略、最少连接数策略等。客户端可以根据自己的需求选择不同的负载均衡策略。

负载均衡原理

springcloud 底层其实是利用 一个名为 Ribbon 的组件,来实现负载均衡功能的。

image-20230810220610291

源码追踪

那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢?

为什么我们只输入了 service 的名称,就可以访问了呢?

——> 是有人帮我们根据 service 名称,获取到了 服务实例的 ip 和 端口。它就是 LoadBalancerInterceptor ,这个类会在 对RestTemplate 的请求进行拦截,然后从 eureka 根据服务 id 获取服务列表,然后 利用负载均衡算法得到真实的服务地址信息,替换 服务id

源码追踪

LoadBalancerInterceptor

image-20230810221139357

可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:

  • request.getURI():获取请求uri,本例中就是 http://user-service/user/8
  • originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service
  • this.loadBalancer.execute():处理服务id,和用户请求。

这里的this.loadBalancerLoadBalancerClient类型,我们继续跟入。

LoadBalancerClient

继续跟入 execute 方法

image-20230810221539384

代码是这样的:

  • getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。
  • getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8082端口的服务

放行后,再次访问并跟踪,发现获取的是 8081

image-20230810221636379

综上,实现了负载均衡

负载均衡策略 IRule

在刚才的代码中,可以看到获取服务使通过一个getServer方法来做负载均衡:

1525620835911

我们继续跟入:

1544361421671

继续跟踪源码chooseServer方法,发现这么一段代码:

1525622652849

我们看看这个rule是谁:

1525622699666

这里的rule默认值是一个RoundRobinRule,看类的介绍:

1525622754316

这不就是轮询的意思嘛。

到这里,整个负载均衡的流程我们就清楚了。

总结

SpringCloudRibbon 的底层采用了一个拦截器,拦截了 RestTemplate 发出的请求,对地址做了修改。

image-20230810223248098

基本流程如下:

  1. 拦截我们 的 RestTemplate 请求 http://userservice/user/1
  2. RibbonLoadbalancerClient 会从请求 url 中获取服务名称,也就是 userservice
  3. DynamicServerListLoadBalancer 根据 user-serviceeureka 拉取服务列表
  4. eureka 返回列表: localhost:8081、localhost:8082
  5. IRuler 利用内置负载均衡策略,从列表中选择一个
  6. RibbonLoadBalancerClient 修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

负载均衡策略

负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:

image-20210713225653000

不同规则的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

默认的实现就是ZoneAvoidanceRule,是一种轮询方案

自定义负载均衡策略

通过定义IRule实现可以修改负载均衡规则,有两种方式:

  1. 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule
1
2
3
4
@Bean
public IRule randomRule(){
return new RandomRule();
}
  1. 配置文件方式:在order-serviceapplication.yml文件中,添加新的配置也可以修改规则:
1
2
3
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

注意,一般用默认的负载均衡规则,不做修改。

饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
ribbon:
eager-load:
enabled: true
clients: userservice

Nacos 注册中心

Nacos是一个服务注册中心和配置中心,用于微服务架构中的服务注册和服务发现。它可以通过添加Nacos客户端依赖和配置来使用作为注册中心。启动Nacos客户端后,它会自动将服务注册到注册中心。Nacos还可以作为配置中心使用,用于存储和管理微服务的配置数据。它支持多种格式的配置数据,包括JSON、YAML和属性文件。

官方文档:https://nacos.io/zh-cn/

服务注册到nacos

nacos 是 springcloudalibaba 的组件,而 springcloudalibaba 也遵循 springcloud 中定义 的服务注册、服务发现规范。因此 使用nacos 与 使用 Eureka 对于微服务来说,并没有太大区别

主要差异在于:

  1. 依赖不同
  2. 服务地址不同
引入依赖

cloud-demo 父工程 pom 文件中的 <dependencyManagement> 中引入 是springcloudalibaba 的依赖

1
2
3
4
5
6
7
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

然后在 user-serviceorder-service 的 pom 文件中 引入 nacos-discovery 依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

记得注释掉 eureka 的依赖

配置 nacos 地址

user-serviceorder-serviceapplication.yaml 中 添加 nacos 地址

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:8848

同样的 需要注释掉 eureka地址

重启

重启微服务后,登陆 nacos 的 管理页面,可以看到相关的信息

image-20230811120147478

服务分级存储模型

一个服务 可以有多个实例,例如我们的user-service,可以有:

  • 127.0.0.1:8081
  • 127.0.0.1:8082
  • 127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例 划分为一个集群

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

image-20210713232522531

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

image-20210713232658928

杭州机房内的order-service应该优先访问同机房的user-service。

给 user-service 配置集群

修改 user-service 中的 application.yaml 文件,添加集群配置

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称

重启两个实例后,可以在 nacos 控制台看到以下信息

image-20230811120655137

我们再复制一个 user-service 启动配置,添加 以下属性

1
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH

再次查看控制台

image-20230811121055889

同集群优先的负载均衡

默认的 ZoneAvoidanceRule 并不能实现根据同集群优先 来实现负载均衡

因此 nacos 中 提供了一个 NacosRule 的 实现,可以优先从集群中挑选实例

  1. order-service 配置集群信息

    修改order-service的application.yml文件,添加集群配置:

    1
    2
    3
    4
    5
    6
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848
    discovery:
    cluster-name: HZ # 集群名称
  2. 修改负载均衡的规则

    修改 order-serviceapplication.yml 文件,修改负载均衡规则

    1
    2
    3
    userservice:
    ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

权重配置

在我们实际开发中,可能会出现这样的场景:

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。

因此,nacos 提供了 权重配置来控制访问频率,权重越大则访问频率越高

在nacos 控制台,找到 user-service 的实例列表,点击编辑,即可修改:

image-20230811133314706

注意: 如果权重修改为 0 ,则该实例永远不会被访问

环境隔离

nacos 提供了 namespace 来实现环境隔离

  • nacos 中 可以有多个 namespace
  • namespace 下可以有 groupservice
  • 不同的 namespace 之间相互隔离,不同的 namespace 服务相互不可见
image-20230811134501163
创建命名空间

默认情况下,所有service、data、group都在同一个namespace,名为public:

image-20210714000414781

我们可以点击页面新增按钮,添加一个namespace:

image-20210714000440143

然后,填写表单:

image-20210714000505928

就能在页面看到一个新的namespace:

image-20210714000522913

给微服务配置 namespace

给微服务配置namespace只能通过修改配置来实现。

例如,修改order-service的application.yml文件:

1
2
3
4
5
6
7
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID

重启order-service后,访问控制台,可以看到下面的结果:

image-20210714000830703

image-20210714000837140

此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:

image-20210714000941256

nacos 与 eureka 的区别

Nacos的服务实例分为两种l类型:

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。

  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例:

1
2
3
4
5
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例

Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:

image-20210714001728017
  • Nacos与eureka的共同点

    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测
  • Nacos与Eureka的区别

    • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

nacos 配置管理

nacos 除了可以做注册中心,同样可以做配置管理

统一配置管理

当微服务部署的实例越来越多,达到 数十个、数百个的时候,逐个修改微服务配置就很麻烦,也很容易出错,这时候,我们就需要一个统一配置管理方案,可以集中管理所有实例的配置

image-20230811151340355

nacos 一方面可以将配置集中管理,另一方面可以在配置变更的时候,及时通知微服务,实现配置的热更新

在 nacos 中添加配置文件

如何在nacos中管理配置呢?

image-20210714164742924

然后在弹出的表单中,填写配置信息:

image-20210714164856664

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

从微服务拉取配置

微服务要拉取 nacos 中管理的配置,并且与本地 的application.yml 合并,才能完成项目的启动

但如果尚未读取 application.yml ,又如何得知 nacos 地址呢? ——> 因此 spring 引入了一种 新的 配置文件: bootstrap.yml ,会在 application.yaml 之前被读取,流程如下:

image-20230811151851166

引入 nacos-config 依赖

首先,在 user-service 服务中,引入 nacos-config 的客户端依赖

1
2
3
4
5
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
添加 bootstrap.yml

然后,在 user-service 中添加一个 bootstrap.yml 文件,内容如下

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名

这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据

${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。

本例中,就是去读取userservice-dev.yaml

image-20210714170845901

读取 nacos 配置

user-service 中的 usercontroller 添加业务,读取 pattern.dateformat 配置:

image-20230811153012076

配置热更新

我们最终的目的,是修改 nacos 中的配置后,微服务中 无需重启即可让配置生效,也就是 配置热更新

要实现配置热更新,可以使用两种方式

方式一

@value 注入的变量所在的类上 添加注解 @RefreshScope

image-20230811153922622

方式二

使用 @ConfigurationProperties 注解代替 @Value 注解

user-service 服务中,添加一个类,读取 pattern.dateformate 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.itcast.user.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* <p> pattern属性 </p>
*
* @Author cccs7/cs7eric - csq020611@gmail.com
* @Description pattern属性
* @Date 2023/8/11 15:43
*/
@Data
@Component
@ConfigurationProperties
public class PatternProperties {

private String dateformat;
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package cn.itcast.user.web;

import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

@Autowired
private PatternProperties patternProperties;

/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}

@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}

配置共享

其实微服务启动时,会去 nacos 读取 多个配置文件,例如:

  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml

  • [spring.application.name].yaml,例如:userservice.yaml

[spring.application.name].yaml不包含环境,因此可以被多个环境共享。

添加一个环境共享配置

我们在nacos中添加一个userservice.yaml文件:

image-20210714173233650

在user-service中读取共享配置

在user-service服务中,修改PatternProperties类,读取新添加的属性:

image-20210714173324231

在user-service服务中,修改UserController,添加一个方法:

image-20210714173721309

运行两个UserApplication,使用不同的profile

修改UserApplication2这个启动项,改变其profile值:

image-20210714173538538

image-20210714173519963

这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。

启动UserApplication和UserApplication2

访问http://localhost:8081/user/prop,结果:

image-20210714174313344

访问http://localhost:8082/user/prop,结果:

image-20210714174424818

可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。

配置共享的优先级

当nacos、服务本地同时出现相同属性时,优先级有高低之分:

image-20210714174623557

搭建 nacos 集群

集群结构图

官方给出的Nacos集群图:

image-20210409210621117

其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。

我们计划的集群结构:

image-20210409211355037

三个nacos节点的地址:

节点 ip port
nacos1 192.168.150.1 8845
nacos2 192.168.150.1 8846
nacos3 192.168.150.1 8847
搭建集群

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载nacos安装包
  • 配置nacos
  • 启动nacos集群
  • nginx反向代理
初始化数据库

Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。

官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库可以参考传智教育的后续高手课程。

这里我们以单点的数据库为例来讲解。

首先新建一个数据库,命名为nacos,而后导入下面的SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
下载nacos

nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。

本例中才用1.4.1版本:

image-20210409212119411

配置Nacos

将这个包解压到任意非中文目录下,如图:

image-20210402161843337

目录说明:

  • bin:启动脚本
  • conf:配置文件

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

image-20210409212459292

然后添加内容:

1
2
3
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847

然后修改application.properties文件,添加数据库配置

1
2
3
4
5
6
7
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123
启动

将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3

image-20210409213335538

然后分别修改三个文件夹中的application.properties,

nacos1:

1
server.port=8845

nacos2:

1
server.port=8846

nacos3:

1
server.port=8847

然后分别启动三个nacos节点:

1
startup.cmd
nginx反向代理

找到课前资料提供的nginx安装包:

image-20210410103253355

解压到任意非中文目录下:

image-20210410103322874

修改conf/nginx.conf文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}

server {
listen 80;
server_name localhost;

location /nacos {
proxy_pass http://nacos-cluster;
}
}

而后在浏览器访问:http://localhost/nacos即可。

代码中application.yml文件配置如下:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址
优化
  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.

  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

  • Title: springcloud
  • Author: cccs7
  • Created at: 2023-08-09 18:16:11
  • Updated at: 2023-08-14 21:44:50
  • Link: https://blog.cccs7.icu/2023/08/09/springcloud/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments