微服务01 - Spring Cloud 与 Spring Cloud Alibaba
文章目录
Spring Cloud
1、Spring Cloud 介绍
1.1、架构演进与微服务
架构演进
- 单体架构:一个项目完成所有功能,适合小型项目。优点:部署简单。缺点:耦合度高,性能较差。
- 垂直架构:将单体架构拆分为多个独立项目,各个项目功能完整互不干扰,本质还是单体架构。缺点:重复功能较多。
- 分布式架构:根据业务功能拆分一个大型项目,每个模块独立开发,称为一个微服务。优点:耦合度低,利于扩展。缺点:部署运维复杂。
集群与分布式的区别
- 集群:多个相同功能的模块,部署在不同的机器上。
- 分布式:多个不同功能的模块,组成一个完整项目。
微服务技术分类
| 功能 | Dubbo(阿里) | Spring Cloud | Spring Cloud Alibaba(兼容 Dubbo、Spring Cloud) |
|---|---|---|---|
| 注册中心 | Zookeeper、Nacos | Eureka、Consul | Eureka、Nacos |
| 服务远程调用 | Dubbo(Dubbo 协议) | Feign(HTTP 协议) | Feign、Dubbo |
| 配置中心 | 无 | Spring Cloud Config | Spring Cloud Config、Nacos |
| 服务网关 | 无 | Spring Cloud Gateway、Zuul | Spring Cloud Gateway、Zuul |
| 服务监控与保护 | Dubbo-Admin | Hystrix | Sentinel |
1.2、Spring Cloud 介绍
Spring Cloud 用于快速构建分布式系统。常用组件包括:服务注册与发现、负载均衡、熔断器、网关、配置中心等。
Spring Cloud 官网:https://spring.io/projects/spring-cloud。
Spring Cloud 项目分类
- Spring Cloud Netflix
- Eureka:服务注册与发现。
- Ribbon:负载均衡。
- Spring Cloud Alibaba:提供微服务开发的一站式解决方案,包括所有必须组件。
- Nacos:服务注册与发现、负载均衡、配置管理、数据存储等。
- Spring Cloud OpenFeign:声明式 HTTP 客户端。
- Spring Cloud Gateway:网关。
- Spring Cloud Zuul:网关。
- Spring Cloud Zookeeper:服务注册与发现。
1.3、Spring Cloud 与 Spring Boot
Spring Cloud 与 Spring Boot 的区别
- Spring Cloud:用于快速构建分布式系统。
- Spring Boot:用于快速构建 Spring 应用程序(微服务)。
Spring Cloud 与 Spring Boot 的版本兼容关系
下面 Spring Cloud 版本使用 Hoxton.SR12,对应 Spring Boot 版本使用 2.3.7.RELEASE。
| Spring Cloud 版本 | Spring Boot 版本 |
|---|---|
| Hoxton | 2.2.x、2.3.x(Starting with Hoxton SR5) |
| Greenwich | 2.1.x |
| Finchley | 2.0.x |
| Edgware | 1.5.x |
| Dalston | 1.5.x |
2、环境搭建与 RestTemplate 远程调用
2.1、环境搭建 - 父项目(springcloud 项目)
<!--打包方式-->
<packaging>pom</packaging>
<!--依赖管理-->
<dependencyManagement>
<dependencies>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
</dependencyManagement>
2.2、环境搭建 - 提供者(user 模块)
步骤
- 一、创建数据库。
- 二、在 user 中,导入依赖、配置 YML、创建启动类。
- 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
- 四、测试。访问 http://localhost:8081/user/1。
示例
- 一、创建数据库。
create database db_user;
use db_user;
create table user (
user_id int auto_increment,
user_name varchar(100),
user_address varchar(100),
primary key (user_id)
);
insert into user (user_name,user_address) values
('乔峰','陕西省西安市'),
('虚竹','河南省郑州市'),
('段誉','云南省大理市');
select * from user;
- 二、在 user 中,导入依赖、配置 YML、创建启动类。
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
server:
port: 8081
spring:
application:
name: user
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_user?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: test123!
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
- 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer userId;
private String userName;
private String userAddress;
}
@Repository
@Mapper
public interface UserDao {
@Select("select * from user where user_id = #{id}")
User selectById(int id);
}
public interface UserService {
User selectById(int id);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User selectById(int id) {
return userDao.selectById(id);
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User selectById(@PathVariable("id") int id) {
return userService.selectById(id);
}
}
- 四、测试。访问 http://localhost:8081/user/1。
{
"userId": 1,
"userName": "乔峰",
"userAddress": "陕西省西安市"
}
2.3、环境搭建 - 消费者(order 模块)
步骤
- 一、创建数据库。
- 二、在 order 中,导入依赖、配置 YML、创建启动类。(注意:使用 User 类需要导入依赖,或存在该实体类)
- 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
- 四、测试。访问 http://localhost:8080/order/1。
示例
- 一、创建数据库。
create database db_order;
use db_order;
create table `order` (
order_id int auto_increment,
order_name varchar(100),
order_price int,
user_id int,
primary key (order_id)
);
insert into `order` (order_name,order_price,user_id) values
('苹果 14',4999,1),
('平板',2499,2),
('手表',2999,3);
select * from `order`;
- 二、在 order 中,导入依赖、配置 YML、创建启动类。(注意:使用 User 类需要导入依赖,或存在该实体类)
<!--User-->
<dependency>
<groupId>com.feng</groupId>
<artifactId>user</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
server:
port: 8080
spring:
application:
name: order
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_order?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: test123!
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Integer orderId;
private String orderName;
private Integer orderPrice;
private Integer userId;
private User user;
}
@Repository
@Mapper
public interface OrderDao {
@Select("select * from `order` where order_id = #{id}")
Order selectById(int id);
}
public interface OrderService {
Order selectById(int id);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Override
public Order selectById(int id) {
return orderDao.selectById(id);
}
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/{id}")
public Order selectById(@PathVariable("id") int id) {
return orderService.selectById(id);
}
}
- 四、测试。访问 http://localhost:8080/order/1。
{
"orderId": 1,
"orderName": "苹果 14",
"orderPrice": 4999,
"userId": 1,
"user": null
}
2.4、RestTemplate 远程调用
RestTemplate 属于 Spring 项目,用于发起远程调用。HTTP 请求不用关心语言,只需提供 IP、端口、路径、请求参数。
步骤
- 一、在 order 的启动类中,注入 RestTemplate。(或使用配置类)
- 二、在 order 的 Service 层方法中,使用 RestTemplate 发起远程调用。
- 三、测试。访问 http://localhost:8080/order/1。
示例
- 一、在 order 的启动类中,注入 RestTemplate(或使用配置类)。
@SpringBootApplication
public class OrderApplication {
@Bean
public RestTemplate resTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 二、在 order 的 Service 层方法中,使用 RestTemplate 发起远程调用。
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private RestTemplate restTemplate;
@Override
public Order selectById(int id) {
// 一、查询订单。
Order order = orderDao.selectById(id);
// 二、使用 RestTemplate 发起 HTTP 请求,查询用户。
String url = "http://localhost:8081/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 三、封装 User。
order.setUser(user);
// 四、返回。
return order;
}
}
- 三、测试。访问 http://localhost:8080/order/1。
{
"orderId": 1,
"orderName": "苹果 14",
"orderPrice": 4999,
"userId": 1,
"user": {
"userId": 1,
"userName": "乔峰",
"userAddress": "陕西省西安市"
}
}
Spring Cloud Netflix
3、Eureka 服务注册与发现
3.1、Eureka 介绍
Eureka 用于服务注册与发现。核心包括:
- Eureka Server(服务端):注册中心。用于存储服务信息,监控服务心跳。如果 Eureka Server 在短时间内丢失过多心跳,则会进入自我保护模式,避免因网络故障或实例故障而误删健康实例的注册信息,称为 Eureka Server 自我保护机制。
- Eureka Client(客户端):服务提供者与消费者。
- 服务提供者:服务注册。注册自身到 Eureka Server 中,每隔 30 秒发送一次心跳到 Eureka Server。例如:user-service。
- 服务消费者:服务发现。从 Eureka Server 中获取服务列表,根据负载均衡策略,选取一个服务发起远程调用。例如:order-service。
3.2、注册中心
步骤
- 一、创建 eureka-server 模块,导入 Eureka Server 依赖。
- 二、配置 Eureka Server。
- 三、在启动类中,使用 @EnableEurekaServer 开启服务。
- 四、测试。访问 http://localhost:8700。
示例
- 一、创建 eureka-server 模块,导入 Eureka Server 依赖。
<!--Eureka Server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 二、配置 Eureka Server。
server:
port: 8700 # 端口号,默认 8761
spring:
application:
name: eureka-server # 名称
eureka:
client:
service-url:
defaultZone: http://localhost:8700/eureka/ # 地址,默认 http://localhost:8761/eureka/
- 三、在启动类中,使用 @EnableEurekaServer 开启服务。
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
- 四、测试。访问 http://localhost:8700。
3.3、服务注册(eureka-user 模块)
步骤
- 一、创建 eureka-user 模块,导入 Eureka Client 依赖。
- 二、配置 Eureka Client。
- 三、测试。访问 http://localhost:8700。
示例
- 一、创建 eureka-user 模块,导入 Eureka Client 依赖。
<!--Eureka Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 二、配置 Eureka Client。
spring:
application:
name: eureka-user
eureka:
client:
service-url:
defaultZone: http://localhost:8700/eureka/
- 三、测试。访问 http://localhost:8700。
3.4、服务发现(eureka-order 模块)
步骤
- 一、创建 eureka-order 模块,导入 Eureka Client 依赖。
- 二、配置 Eureka Client。
- 三、在 RestTemplate 上,使用 @LoadBalanced 负载均衡注解。
- 四、在 Service 层的方法中,使用服务提供者的名称发起远程调用。
- 五、测试。访问 http://localhost:8080/order/1。
示例
- 一、创建 eureka-order 模块,导入 Eureka Client 依赖。
<!--Eureka Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 二、配置 Eureka Client。
spring:
application:
name: eureka-order
eureka:
client:
service-url:
defaultZone: http://localhost:8700/eureka/
- 三、在启动类中,使用 @LoadBalanced 负载均衡注解。
@Bean
@LoadBalanced
public RestTemplate resTemplate() {
return new RestTemplate();
}
- 四、在 Service 层的方法中,使用服务提供者的名称发起远程调用。
String url = "http://eureka-user/user/" + order.getUserId();
- 五、测试。访问 http://localhost:8080/order/1。
3.5、注册中心集群
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/
3.6、IDEA 启动多服务
在 IDEA 中,右键 Services 窗口中的服务,点击 Copy Configuration 修改端口号然后启动,以下方式任选其一。
- VM options:-Dserver.port=8082。
- Program arguments:–server.port = 8083。
4、Ribbon 负载均衡
4.1、负载均衡原理
Ribbon 属于 Spring Cloud Netflix 项目,用于负载均衡。
负载均衡流程
- 一、发起请求。order-service 使用服务名称发起请求,http://user-service/user/1 不是真实地址。
- 二、拉取服务列表,获得真实地址。Ribbon 根据服务名称,从 Eureka Server 中拉取服务列表,获得真实地址(localhost:8081、localhost:8082、localhost:8083)。
- 三、负载均衡。Ribbon 根据负载均衡策略,选择其中一个服务(localhost:8081)发起远程调用。
负载均衡原理
LoadBalancerInterceptor 类实现了 ClientHttpRequestInterceptor 接口,可以拦截 HTTP 请求(RestTemplate)。发起请求之后:
- 一、首先调用 LoadBalancerInterceptor 类的 intercept() 方法,根据请求路径(http://user-service/user/1)获得服务名称(user-service)。
- 二、然后调用 LoadBalancerClient 对象的 execute() 方法,根据服务名称(user-service)获得 ILoadBalancer 对象(DynamicServerListLoadBalancer 包含服务列表)。
- 三、最后调用 ILoadBalancer 的父类 BaseLoadBalancer 的 chooseServer() 方法。
- 四、最终调用 IRule 对象的 choose() 方法,实现负载均衡,默认 ZoneAvoidanceRule。
4.2、负载均衡策略
负载均衡策略分类
Ribbon 负载均衡策略由 IRule 接口定义,其实现类就是具体规则。

| 内置负载均衡规则类 | 说明 |
|---|---|
| RoundRobinRule | 轮询。默认 |
| WeightedResponseTimeRule | 根据服务的响应时间设置权重,再做轮询 |
| ZoneAvoidanceRule | 根据服务的 Zone 进行分类,再做轮询 |
| AvailabilityFilteringRule | 根据服务的可用性进行筛选,再做轮询 |
| RandomRule | 随机 |
负载均衡策略修改
- 方式一、注入 IRule。
- 方式二、配置方式。
示例
- 方式一、注入 IRule。
在 eureka-order 服务的启动类中,注入 IRule(或使用配置类)。在该服务中调用其他服务都使用随机(全局)。
@Bean
public IRule iRule() {
return new RandomRule();
}
- 方式二、配置方式。
在 eureka-order 服务的配置文件中,配置策略名称。在该服务中仅调用 eureka-user 服务使用随机(单体)。
eureka-user:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡策略
4.3、懒加载与饥饿加载
负载均衡默认使用懒加载,在第一次访问时才会创建 LoadBalanceClient,访问时间过长。
开启饥饿加载,则会在项目启动时创建 LoadBalanceClient,降低第一次访问时间。
示例
- 开启饥饿加载
在 eureka-order 服务的配置文件中,配置以下内容。
ribbon:
eager-load:
enabled: true # 开启饥饿加载,默认 false
clients: eureka-user # 指定服务名称
Spring Cloud Alibaba
5、Nacos 服务注册与发现
5.1、Nacos 注册中心(Nacos 安装)
Nacos 官网:https://www.nacos.io/。
Nacos 下载:https://github.com/alibaba/nacos。
- nacos-server-1.4.1.tar.gz:Linux 版解压包。
- nacos-server-1.4.1.zip:Windows 版解压包。
Nacos 安装(Nacos 注册中心)
- 一、启动 Nacos 注册中心。解压 nacos-server-1.4.1.zip,进入 D:\MyTools\nacos\bin,执行 Windows 命令。
- 二、测试。访问 http://localhost:8848/nacos/index.html,默认账号/密码:nacos/nacos。
# Windows 命令,-m standalone 表示单机模式运行
startup.cmd -m standalone
5.2、服务注册(user-service 模块)
- 一、在父项目中,导入 Spring Cloud Alibaba 管理依赖。
- 二、在 user-service 中,注掉 Eureka 依赖,添加 Nacos 依赖。
- 三、在 user-service 中,注掉 Eureka 地址,添加 Nacos 地址。
- 四、测试。在 Nacos 控制台中查看服务列表,访问 http://localhost:8080/user/1。
示例
<!--Spring Cloud Alibaba 管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Nacos 客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
5.3、服务发现(order-service 模块)
- 一、在父项目中,导入 Spring Cloud Alibaba 管理依赖。
- 二、在 order-service 中,注掉 Eureka 依赖,添加 Nacos 依赖。
- 三、在 order-service 中,注掉 Eureka 地址,添加 Nacos 地址。
- 四、测试。在 Nacos 控制台中查看服务列表,访问 http://localhost:8080/order/1。
示例
<!--Spring Cloud Alibaba 管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Nacos 客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
6、Nacos 负载均衡与环境隔离
6.1、服务实例分级存储
如果配置了服务集群,则服务消费者优先访问同集群的服务提供者。
服务实例分级存储模型
- 一级:服务。例如:user-service、order-service。
- 二级:集群。例如:北京、上海。
- 三级:服务实例。例如:在北京机房内部署了 user-service 的服务器。
示例
server:
port: 8081
spring:
application:
name: user-service
cloud:
nacos:
discovery:
cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
server:
port: 8082
spring:
application:
name: user-service
cloud:
nacos:
discovery:
cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
server:
port: 8083
spring:
application:
name: user-service
cloud:
nacos:
discovery:
cluster-name: Shanghai # 集群,默认 DEFAULT,表示所有服务都在同一个集群
6.2、NacosRule 负载均衡
在默认情况下,Nacos 使用 Ribbon 负载均衡(默认轮询)。
另外 Nacos 提供 NacosRule 用于负载均衡,NacosRule 类实现了 AbstractLoadBalancerRule 抽象类。
NacosRule 负载均衡
服务消费者优先访问同集群的服务提供者,随机选择一个。如果同集群找不到,则会访问其他集群,并且会报警告。
步骤
- 一、在 order-service 中配置集群。
- 二、在 order-service 中配置 IRule。
示例
- 一、在 order-service 中配置集群。
server:
port: 8080
spring:
application:
name: order-service
cloud:
nacos:
discovery:
cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
- 二、在 order-service 中配置 IRule。
@Bean
public IRule iRule() {
return new NacosRule();
}
#user-service:
# ribbon:
# NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # Nacos 负载均衡策略
order-service 优先访问 Beijing 的 user-service。如果 Beijing 的 user-service 发生故障,则会访问 Shanghai,并且会报警告(发生跨集群调用)。
2022-03-18 16:27:34.235 WARN 7480 --- [nio-8080-exec-1] c.alibaba.cloud.nacos.ribbon.NacosRule : A cross-cluster call occurs,name = user-service, clusterName = Beijing, instance = [Instance{instanceId='192.168.1.2#8083#Shanghai#DEFAULT_GROUP@@user-service', ip='192.168.1.2', port=8083, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='Shanghai', serviceName='DEFAULT_GROUP@@user-service', metadata={preserved.register.source=SPRING_CLOUD}}]
6.3、权重负载均衡
在实际项目部署过程中,服务器性能存在差异,因此可以调整服务实例权重,权重越高被访问的频率越高。
在 Nacos 控制台中,可以设置服务实例权重。默认 1,通常设置 0~1 之间。例如:设置 0.1 基本不会被访问,设置 0 完全不会被访问。
6.4、Nacos 环境隔离
Nacos 使用 Namespace 用于环境隔离:(Namespace(Group(Service/Data)))。例如可以设置 dev、test 等。环境隔离规则如下:
- 所有服务的 Namespace 默认都是 public,表示在同一个 Namespace 中,互相可见。
- 每个 Namespace 都有唯一 ID,不同 namespace 中的服务不可见。
步骤
- 一、在 Nacoc 控制台中,创建 Namespace,生成命名空间 ID。
- 二、在 order-service 中,配置 Namespace。
- 三、测试 。访问 http://localhost:8080/order/1。
示例
-
一、在 Nacoc 控制台中,创建 namespace,生成命名空间 ID。
-
二、在 order-service 中,配置 namespace。
spring:
cloud:
nacos:
discovery:
namespace: 30d8ab5f-eaea-4f3e-aec9-be468793d481 # 命名空间
- 三、测试。访问 http://localhost:8080/order/1。
java.lang.IllegalStateException: No instances available for user-service
6.5、CAP 原则
在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三个要素最多只能同时实现两点,不能兼得。
- 一致性(Consistency):同一时刻,所有节点的数据必须完全一致。
- 可用性(Availability):如果部分节点出现故障,集群整体必须能够响应客户端的请求。
- 分区容错性(Partition Tolerance):如果网络分区(节点之间无法通信),系统能够继续运行。
三者为什么不能兼得
假如集群中的两个节点 N1 与 N2 之间无法通信,则会出现以下情况:
- 如果保证一致性,则 N2 需要在数据未同步时拒绝响应,牺牲可用性(CP 模式)。
- 如果保证可用性,则 N2 需要响应旧数据,牺牲一致性(AP 模式)。
- 如果放弃分区容错性,则使用单点架构,失去分布式系统的意义(CA 模式)。
6.6、Nacos 与 Eureka 的区别
Nacos 与 Eureka 的相同点
在默认情况下,两者的实例都是临时实例,使用心跳检测模式,如果实例不正常则会被剔除,使用 AP 模式。
Nacos 与 Eureka 的不同点
Nacos 可以设置非临时实例,使用主动检测模式,如果实例不正常不会被剔除,使用 CP 模式。
另外 Nacos 支持服务列表更新的消息推送,服务列表更新更及时。
Nacos 设置非临时实例
spring:
cloud:
nacos:
discovery:
ephemeral: false # 是否为临时实例,默认 ture。(ephemeral 临时)
7、Nacos 配置管理
7.1、配置创建
在 Nacos 控制台中。建议配置经常需要变更的内容。
示例
- Data ID:user-service-dev.yaml。
- Group:DEFAULT_GROUP。
- 描述:user-service 的开发环境配置文件。
- 格式:YAML。(目前支持 YAML、Properties)
pattern:
dateformat: yyyy-MM-dd HH:mm:ss
7.2、微服务配置拉取
配置读取顺序
- 【项目启动】→→【读取 Nacos 中的配置 】→→【读取本地配置】→→【创建 Spring 容器】→→【注入 Bean】
bootstrap.yml
引导文件,优先级高于 application.yml。
步骤
- 一、导入 Nacos 配置管理依赖。
- 二、在 user-service 中创建 bootstrap.yml,属于引导文件,优先级高于 application.yml。
- 三、在 UserController 中,注入 pattern.dateformat 属性。
- 四、测试。访问 http://localhost:8081/user/now。
示例
- 一、导入 Nacos 配置管理依赖。
<!--Nacos 配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 二、在 user-service 中,创建 bootstrap.yml,注掉 application.yml 中的相同内容。
spring:
application:
name: user-service
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址
config:
file-extension: yaml # 配置文件扩展名
- 三、在 UserController 中,注入 pattern.dateformat 属性。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/now")
public String getDate() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
@GetMapping("/{id}")
public User selectById(@PathVariable("id") int id) {
return userService.selectById(id);
}
}
- 四、测试。访问 http://localhost:8081/user/now。
7.3、配置热更新
Nacos 中的配置文件更新后,微服务无需重启就可生效。
- 方式一、在 @Value 注入变量的类上,使用 @RefreshScope 注解。
- 方式二、在配置类上,使用 @Component 与 @ConfigurationProperties 注解。
示例
- 方式一、在 @Value 注入变量的类上,使用 @RefreshScope 注解。
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/now")
public String getDate() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
- 方式二、在配置类上,使用 @Component 与 @ConfigurationProperties 注解。
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
@Autowired
private PatternProperties properties;
@GetMapping("/now")
public String getDate() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
7.4、多环境配置共享
微服务启动时会从 Nacos 读取多个配置文件。例如:
- 在 Nacos 控制台中:user-service-dev.yaml。
- 在 Nacos 控制台中:user-service.yml。
- 在 user-service 中:application.yml。
优先级顺序 user-service-dev.yaml > user-service.yml > application.yml。
7.5、Nacos 集群(了解)
Spring Cloud OpenFeign
8、Feign 远程调用
8.1、Feign 介绍
Feign 使用声明式的 HTTP 客户端接口,封装远程调用过程,可以代替 RestTemplate。
RestTemplate 与 Feign 的区别
- RestTemplate 远程调用:URL 参数复杂难以维护,代码耦合度高且不能实现复用。
String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
- Feign 远程调用:结构优雅,代码耦合度低且可以实现复用。
8.2、Feign 远程调用
步骤
- 一、在 order-service 中,导入 Feign 依赖。
- 二、在 order-service 中的启动类上,使用 @EnableFeignClients 注解开启 Feign 功能。
- 三、创建 Feign 接口(声明式的 HTTP 客户端),使用 Spring MVC 注解声明远程调用的信息。包括:服务名称、请求方式、请求路径、请求参数,返回值类型。
- 四、使用 Feign 接口中的方法发起远程调用,代替 RestTemplate。
示例
- 一、在 order-service 中,导入 Feign 依赖。
<!--Feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 二、在 order-service 中的启动类上,使用 @EnableFeignClients 注解开启 Feign 功能。
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args){
SpringApplication.run(OrderApplication.class, args);
}
}
- 三、创建 Feign 接口(声明式的 HTTP 客户端),使用 Spring MVC 注解声明远程调用的信息。包括:服务名称、请求方式、请求路径、请求参数,返回值类型。
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User selectById(@PathVariable("id") int id);
}
- 四、使用 Feign 接口中的方法发起远程调用,代替 RestTemplate。
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private UserClient userClient;
@Override
public Order selectById(int id) {
// 一、查询订单。
Order order = orderDao.selectById(id);
// 二、使用 RestTemplate 发起 HTTP 请求,查询用户。
// String url = "http://user-service/user/" + order.getUserId();
// User user = restTemplate.getForObject(url, User.class);
// 二、使用 Feign 发起远程调用,代替 RestTemplate。
User user = userClient.selectById(order.getUserId());
// 三、封装 User。
order.setUser(user);
// 四、返回。
return order;
}
}
注意:@FeignClient 与 @RequestMapping 冲突,如果同时存在则 Spring 容器启动报错。
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User selectById(@PathVariable("id") int id);
}
// Spring 容器启动报错
@FeignClient("user-service")
@RequestMapping("/user")
public interface UserClient {
@GetMapping("/{id}")
User selectById(@PathVariable("id") int id);
}
8.3、Feign 远程调用规则
Feign 远程调用规则,要求 FeignClient(消费者)与 Controller(提供者)中方法的请求路径、请求方式、参数列表完全相同,否则会抛出异常。
- FeignClient(消费者)
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User selectById(@PathVariable("id") int id);
}
- Controller(提供者)
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public User selectById(@PathVariable("id") int id) {
return userService.selectById(id);
}
}
Feign 的最佳实践
- 方式一、为 FeignClient 与 Controller 定义统一的父接口作为标准。
- 方式二、将 FeignClient 抽取为独立模块,并将与接口有关的 Pojo、Feign 的配置类都放在这个模块中,提供给消费者使用。
8.4、方式一、继承
一般情况,不建议在消费者与提供者之间共享接口,因为会造成紧耦合,且 Spring MVC 不支持这种方式(@GetMapping、@PathVariable 等注解不能被继承)。
- 统一的父接口
public interface UserAPI {
@GetMapping("/user/{id}")
User selectById(@PathVariable("id") int id);
}
- FeignClient(消费者)
@FeignClient("user-service")
public interface UserClient extends UserAPI {// 继承父接口
}
- Controller(提供者)
@RestController
@RequestMapping("/user")
public class UserController implements UserAPI {// 实现父接口
@GetMapping("/{id}")
public User selectById(@PathVariable("id") int id) {
return userService.selectById(id);
}
}
8.5、方式二、抽取
- 一、创建 feign-api 模块,导入 Feign 依赖。
- 二、将 order-service 中的 UserClient、User 放到该模块中。
- 三、在 order-service 中,导入 feign-api 依赖。
Spring Cloud Gateway
9、Gateway 网关
9.1、介绍
Spring Cloud 网关
- Spring Cloud Gateway:基于 Spring 5 中的 WebFlux,属于响应式编程,性能更好。
- Spring Cloud Zuul:基于 Servlet,属于阻塞式编程。
网关功能
- 身份认证与权限校验。
- 服务路由、负载均衡。
- 请求限流。
流程
【请求】→→→→【Gateway】→→→→【Nacos】→→→→【微服务】
9.2、Gateway 网关(gateway 模块)
步骤
- 一、在 gateway 中,导入 Gateway、Nacos 客户端依赖,创建启动类。
- 二、在 gateway 中,配置 Gateway、Nacos。
- 三、测试。启动并访问 http://localhost:10010/order/1。
示例
- 一、在 gateway 中,导入 Gateway、Nacos 客户端依赖。
<!--Gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--Nacos 客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
- 二、在 gateway 中,配置 Gateway、Nacos。
server:
port: 10010 # 端口,默认 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
gateway: # 网关
routes: # 路由
- id: user-service # 路由 ID,自定义只要唯一即可
# uri: http://localhost:8081 # 路由目标地址,固定地址写法
uri: lb://user-service # 路由目标地址,服务名称写法,lb 表示负载均衡
predicates: # 断言,判断 HTTP 请求是否符合路由规则
- Path=/user/** # 路径匹配
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
- 三、测试。启动并访问 http://localhost:10010/order/1。
9.3、路由断言工厂
网关路由可以配置的内容包括:
- 路由 ID:路由唯一标识。
- URI:路由目标地址。
- Predicates:路由断言,判断是否符合要求。
- Filters:路由过滤器,处理请求与响应。
11 种路由断言工厂(Route Predicate Factory)
详见 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories。
| 工厂 | 说明 | 示例 |
|---|---|---|
| After | 必须在某个时间点之后 | - After=2017-01-20T17:42:47.789-07:00[America/Denver] |
| Before | 必须在某个时间点之前 | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] |
| Before | 必须在两个时间点之间 | - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-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/{segment} |
| Query | 请求参数必须包含指定参数 | - Query=green |
| RemoteAddr | 请求者的 IP 必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
| Weight | 权重处理 |
示例
predicates:
- Path=/order/**
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
9.4、路由过滤器
GatewayFilter(路由过滤器,或网关过滤器),用于对进入网关的请求与微服务返回结果进行处理。
31 种路由过滤器工厂
详见 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories。
| 工厂 | 说明 |
|---|---|
| AddRequestHeader | 为请求添加一个请发头 |
| RemoveRequestHeader | 从请求中移除一个请求头 |
| AddResponseHeader | 为响应结果添加一个响应头 |
| RemoveResponseHeader | 从响应结果中移除一个响应头 |
| … |
示例
- 一、在 gateway 中,配置过滤器。
server:
port: 10010 # 端口,默认 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
gateway:
routes:
- id: user-service # 路由唯一 ID,只要唯一即可,建议使用服务名称
uri: lb://user-service # 路由目标地址,lb 表示负载均衡,服务名称写法(uri: http://localhost:8081 # 路由目标地址,固定地址写法)
predicates: # 断言,判断 HTTP 请求是否符合路由规则
- Path=/user/** # 路径匹配
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
- After=2017-01-20T17:42:47.789-07:00[America/Denver] # 必须在某个时间点之后
filters: # 过滤器
- AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头
default-filters: # 默认过滤器
- AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头
- 二、在 order-service 中,修改方法。
@GetMapping("/{id}")
public Order selectById(@PathVariable("id") int id,
@RequestHeader(name = "truth", required = false) String s) {
System.out.println(s);
return orderService.selectById(id);
}
9.5、全局过滤器
GlobalFilter(全局过滤器),用于对进入网关的请求与微服务返回结果做处理。
GlobalFilter 与 GatewayFilter 的区别
- GlobalFilter:使用代码,处理逻辑可以自定义。实现 GlobalFilter 接口即可。
- GatewayFilter:使用配置,处理逻辑固定。
需求
定义全局过滤器,拦截请求,判断请求参数中是否存在 authorization 且值为 admin,是则放行,否则拦截。
示例
- 一、在 gateway 中,定义全局过滤器。
//@Order(-1)// 全局过滤器执行顺序,值越小则优先级越高。(等价于实现 Ordered 接口)
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 一、获取请求参数。
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 二、获取请求参数中的 authorization 的值。
String auth = params.getFirst("authorization");
// 三、判断值是否为 admin。
if ("admin".equals(auth)) {
// 四、是,放行。
return chain.filter(exchange);
}
// 五、否,拦截请求并设置状态码。
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}
- 二、测试。访问 http://localhost:10010/order/1 与 http://localhost:10010/order/1?authorization=admin。
9.6、过滤链执行顺序
进入网关的请求,会经过三种过滤器:当前路由 Filter、DefaultFilter、GlobalFilter。这三种过滤器会被合并到一个过滤链(集合)中,排序后依次执行。
过滤链执行顺序
- 一、每个过滤器都必须指定一个 int 类型的 order 值,值越小则优先级越高。
- 二、GlobalFilter 使用 @Order 注解或实现 Ordered 接口指定 order 值。
- 三、当前路由 Filter、DefaultFilter 的 order 值由 Spring 指定,默认按照声明顺序从 1 递增。
- 四、如果过滤器的 order 值相同,则按照顺序 DefaultFilter > 当前路由 Filter > GatewayFilter。
filters: # 过滤器
- AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 1
- AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 2
- AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 3
default-filters: # 默认过滤器
- AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 1
- AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 2
- AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 3
9.7、跨域问题处理(了解)
发起请求时域名不同,称为跨域。包括:
- 域名不同:www.taobao.com、www.taobao.org、www.jd.com、miaosha.jd.com。
- 端口不同:localhost:8081、localhost:8082、localhost:8083。
注意:跨域属于浏览器禁止请求的发起者与服务端发生跨域 Ajax 请求,请求被浏览器拦截。order-service 到 user-service 不是跨域。
解决方案:配置 CORS。
spring:
cloud:
gateway:
# CORS 跨越配置
globalcors: # 全局 CORS
add-to-simple-url-handler-mapping: ture # 解决 Options 请求被拦截的问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许的 URL
- http://localhost:8090
allowedMethods: # 允许的 Ajax 请求方式
- GET
- POST
- DELETE
- PUT
- OPTIONS
allowedHeader: * # 允许的头信息
allowedCredentials: true # 是否允许 Cookie
maxAge: 360000 # 有效期,单位秒
更多推荐


所有评论(0)