1 系统架构演进

随着互联网行业的发展,对服务的要求也越来越高,服务架构也从单体架构逐渐演变为现在流行的微服务架构

1.1 单体架构

早期的软件系统通常是基于单体应用架构设计的,也就是将整个系统作为一个单一的、可执行的应用程序来构建和维护。

如下图所示:

image-20230503091204863

单体架构具有以下优点:

  1. 简单:单体架构模式相对于其他复杂的架构来说,其结构简单易用,便于新手学习和应用。
  2. 易于维护:由于整个应用程序都在一个代码库中,因此很容易对其进行维护和更新。
  3. 易于部署:单个可执行文件可以在任何支持运行该语言的环境中运行,并且部署也相对轻松。

然而,单体架构也存在一些缺点:

  1. 扩展性差:单体应用程序所有功能都在一个程序中实现,因此扩展功能时需要新增或修改源代码,并重新部署整个应用程序,这可能会导致系统不稳定和长时间停机。
  2. 可靠性低:由于单体应用程序集成了所有的逻辑和组件,因此如果其中有一个非常重要的组件出现故障,则可能导致从整个系统崩溃。
  3. 风险高:单体应用程序中的错误往往比较难以调试,因为代码复杂度高且耦合度强。 综上所述,单体架构适用于小型、简单的软件系统,但是对于大型、复杂的系统来说,单体架构面临诸多挑战,需要采用其他更加灵活和可扩展的架构模式。

1.2 微服务架构

随着互联网的不断发展,软件系统的架构也是在不断的更新。由原先的单体架构逐渐演变成分布式系统架构,再到目前非常主流的微服务系统架构。

分布式系统架构是指将一个软件系统分割成多个独立的服务,并且这些服务可以在不同的计算机或服务器上运行,并通过网络进行通信。

微服务系统架构:本质上也属于分布式系统架构,在微服务系统架构中,更加重视的是服务拆分粒度。

如下图所示:

image-20230503095741321

微服务架构的特点:

  1. 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  2. 自治:团队独立、技术独立、数据独立,独立部署和交付
  3. 面向服务:服务提供统一标准的接口,与语言和技术无关

微服务系统架构的优点:

  1. 可扩展性好:由于系统中的不同组件可以独立地进行扩展和升级,从而提高了整个系统的扩展性和可靠性。
  2. 容错性高:由于系统中的组件可以在不同的计算机或服务器上运行,因此即使某些节点出现故障也不会影响整个系统的运行。
  3. 高效性强:分布式系统可以将负载和任务分配到不同的节点上,从而提高系统的并发能力和处理速度。
  4. 灵活性强:分布式系统可以支持多种编程语言和应用程序框架,并且可以利用各种云计算技术,如Docker、Kubernetes等。

微服务系统架构的存在的问题

  1. 微服务的管理:这些微服务如果没有进行统一的管理,那么维护性就会极差。
  2. 服务间的通讯:微服务之间肯定是需要进行通讯,比如购物车微服务需要访问商品微服务。
  3. 前端访问问题:由于每一个微服务都是部署在独立的一台服务器的,每一个微服务都存在一个对应的端口号,前端在访问指定微服务的时候肯定需要指定微服务的ip地址和端口号,难道需要在前端维护每一个微服务的ip地址和端口号?
  4. 配置文件管理:当构建服务集群的时候,如果每一个微服务的配置文件还是和微服务进行绑定,那么维护性就极差。

1.3 分布式和集群

分布式:由多台服务器构成的网络环境,在分布式环境下每一台服务器的功能是不一样的。

集群: 由多台服务器构成的网络环境,在集群环境下每一台服务器的功能是一样的。

分布式环境中每一台服务器都可以做集群,如下图所示:

image-20230503101843092

2 Spring Cloud Alibaba概述

针对微服务系统架构所存在的问题,肯定是需要有具体的技术来解决,而所使用到的技术就是Spring Clouad Alibaba。那么想要了解Spring Clouad Alibaba,那么就需要先了解一下Spring Cloud。

2.1 Spring Cloud

1、Spring Cloud 是一系列框架的有序集合。在Spring Cloud这个项目中包含了很多的组件【子框架】,每一个组件都是用来解决问题系统架构中所遇到的问题,因此Spring Cloud可以看做是一套微服务的解决方案。

2、Spring Cloud中常见的组件:Eureka(服务注册中心)、Openfeign(服务远程调用)、Gateway(服务网关)、Spring Cloud Config(统一配置中心)等。

3、Spring Cloud项目官方网址:https://spring.io/projects/spring-cloud

4、Spring Cloud依赖于Spring Boot,并且有版本的兼容关系,如下所示:

image-20230503102618925

2.2 Spring Cloud Alibaba

Spring Cloud Alibaba是阿里针对微服务系统架构所存在的问题给出了一套解决方案,该项目包含了微服务系统架构必须的一些组件。

常见的组件可以参看官网地址:https://spring-cloud-alibaba-group.github.io/github-pages/2021/en-us/index.html

注意:

1、Spring Cloud Alibaba中所提供的组件是遵循Spring Cloud规范的,两套技术所提供的组件是可以搭配使用的。

2、在现在企业开发中往往是两套技术组件搭配进行使用:Nacos(服务注册中心和配置中心)、Openfeign(远程调用)、Ribbon(客户端负载均衡器)、Gateway(服务网关)、Sentinel(服务保护组件)等。

3 微服务环境准备

要想学习Spring Cloud Alibaba,那么此时就需要有一个微服务的系统环境。本章节我们就来使用Spring Boot来搭建两个微服务,分别是用户微服务和订单微服务。

工程结构说明

在创建微服务工程的时候都需要先提供一个父工程,使用父工程来管理多个微服务所需要的依赖。我们的微服务系统结构如下所示:

image-20230503104825462

父工程搭建

具体步骤如下所示:

1、创建一个spzx-cloud-parent的maven项目

2、在pom.xml文件中加入如下依赖

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
<!-- 定义属性 -->
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2022.0.2</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
<mybatis-spring-boot.version>3.0.1</mybatis-spring-boot.version>
<mysql.version>8.0.30</mysql.version>
</properties>

<!-- 指定spring boot父工程 -->
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>3.0.5</version>
</parent>

<dependencyManagement> <!-- 在dependencyManagement标签中所定义的依赖不会被子工程直接进行继承 -->
<dependencies>

<!-- spring cloud的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- spring cloud alibaba的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- mysql的驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>

<!-- mybatis和spring boot整合的起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>

</dependencies>

</dependencyManagement>

3、删除src目录

用户微服务搭建

基础环境搭建

步骤:

1、导入课程资料中所提供的user.sql数据库脚本。

2、在spzx-cloud-parent下面创建一个子模块spzx-cloud-user

3、在pom.xml文件中加入如下依赖

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
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>

<!-- spring boot的web开发所需要的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- mysql的驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mybatis和spring boot整合的起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<!-- lombok依赖,快速生成getter和setter方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

</dependencies>

4、在resources目录下创建一个application.yml文件,文件的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 配置服务端口号
server:
port: 10100

# 配置数据库的连接信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spzx-cloud-user?characterEncoding=UTF8
username: root
password: root

# 配置mybatis的相关信息
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.atguigu.spzx.cloud.user.entity
mapper-locations: classpath:/mapper/*.xml

3.3.2 基础代码编写

需求:在user微服务中提供一个根据用户的id查询用户详情的接口

具体步骤:

1、编写启动类

1
2
3
4
5
6
7
// com.atguigu.spzx.cloud.user
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class , args) ;
}
}

2、编写实体类

1
2
3
4
5
6
7
8
9
// com.atguigu.spzx.cloud.user.entity
@Data
public class User {

private Long id ;
private String userName ;
private String address ;

}

3、编写UserMapper接口

1
2
3
4
5
6
7
8
// com.atguigu.spzx.cloud.user.mapper;
@Mapper // 该注解可以通过在启动类上的@MapperScan注解进行替换
public interface UserMapper {

// 根据用户的id查询用户详情
public abstract User findUserByUserId(Long userId) ;

}

4、编写UserMapper.xml映射文件

在resources目录下创建目录mapper,在mapper目录下创建UserMapper.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.spzx.cloud.user.mapper.UserMapper">

<!-- 根据用户的id查询用户详情 -->
<select id="findUserByUserId" resultType="com.atguigu.spzx.cloud.user.entity.User">
SELECT * FROM tb_user WHERE id = #{userId}
</select>

</mapper>

5、编写service接口以及实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// com.atguigu.spzx.cloud.user.service
public interface UserService {

// 根据用户的id查询用户详情
public abstract User findUserByUserId(Long userId) ;

}

// com.atguigu.spzx.cloud.user.service.impl;
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper ;

@Override
public User findUserByUserId(Long userId) {
return userMapper.findUserByUserId(userId);
}

}

6、编写controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// com.atguigu.spzx.cloud.user.controller
@RestController
@RequestMapping(value = "/api/user")
public class UserController {

@Autowired
private UserService userService ;

@GetMapping(value = "/findUserByUserId/{userId}")
public User findUserByUserId(@PathVariable(value = "userId") Long userId) {
return userService.findUserByUserId(userId) ;
}

}

启动服务进行测试。

订单微服务搭建

3.4.1 基础环境搭建

步骤:

1、导入课程资料中所提供的order.sql数据库脚本。

2、在spzx-cloud-parent下面创建一个子模块spzx-cloud-order

3、在pom.xml文件中加入如下依赖

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
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>

<!-- spring boot的web开发所需要的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- mysql的驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mybatis和spring boot整合的起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<!-- lombok依赖,快速生成getter和setter方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

</dependencies>

4、在resources目录下创建一个application.yml文件,文件的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 配置服务端口号
server:
port: 10200

# 配置数据库的连接信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spzx-cloud-order?characterEncoding=UTF8
username: root
password: root

# 配置mybatis的相关信息
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.atguigu.spzx.cloud.order.entity
mapper-locations: classpath:/mapper/*.xml

3.4.2 基础代码编写

需求:在order微服务中提供一个根据订单的id查询订单详情的接口

具体步骤:

1、编写启动类

1
2
3
4
5
6
7
8
9
// com.atguigu.spzx.cloud.order
@SpringBootApplication
public class OrderApplication {

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

}

2、编写实体类

1
2
3
4
5
6
7
8
9
10
11
// com.atguigu.spzx.cloud.order.entity
@Data
public class Order {

private Long id ;
private Long userId ;
private String name ;
private BigDecimal price ;
private Integer num ;

}

3、编写OrderMapper接口

1
2
3
4
5
6
7
8
// com.atguigu.spzx.cloud.order.mapper;
@Mapper // 该注解可以通过在启动类上的@MapperScan注解进行替换
public interface OrderMapper {

// 根据订单的id查询订单数据
public abstract Order findOrderByOrderId(Long orderId) ;

}

4、编写OrderMapper.xml映射文件

在resources目录下创建目录mapper,在mapper目录下创建OrderMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.spzx.cloud.order.mapper.OrderMapper">
<!-- 定义sql片段 -->
<sql id="base_field">
id , userId , name , price , num
</sql>
<!-- 根据用户的id查询用户详情 -->
<select id="findOrderByOrderId" resultType="com.atguigu.spzx.cloud.order.entity.Order">
SELECT <include refid="base_field"></include> FROM tb_order WHERE id = #{orderId}
</select>
</mapper>

5、编写service接口以及实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// com.atguigu.spzx.cloud.order.service
public interface OrderService {

// 根据订单的id查询订单数据
public abstract Order findOrderByOrderId(Long orderId) ;

}

// com.atguigu.spzx.cloud.order.service.impl;
@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderMapper orderMapper ;

@Override
public Order findOrderByOrderId(Long orderId) {
return orderMapper.findOrderByOrderId(orderId);
}

}

6、编写controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// com.atguigu.spzx.cloud.order.controller
@RestController
@RequestMapping(value = "/api/order")
public class OrderController {

@Autowired
private OrderService orderService ;

@GetMapping(value = "/findOrderByOrderId/{orderId}")
public Order findOrderByOrderId(@PathVariable("orderId") Long orderId) {
return orderService.findOrderByOrderId(orderId) ;
}

}

启动服务进行测试。

4 服务远程调用

4.1 需求说明

需求:在查询订单时候需要将订单所属用户的信息也一并查询出来。

如下图所示:

image-20230503122219277

注意:被调用方法常常也将其称之为服务的提供方,调用方常常将其称之为服务的消费方

4.2 传统调用

4.2.1 代码实现

要完成上述的需求,我们就需要在order微服务中向user微服务发起一个http的请求,调用

http://localhost:10100/api/user/findUserByUserId/{userId}这个接口。

步骤:

1、在order微服务中定义一个User的实体类,注意包结构需要和user微服务保持一致

image-20230809094341929

2、在order微服务的Spring容器中注册一个RestTemplate

1
2
3
4
5
6
7
8
// com.atguigu.spzx.cloud.order.config;
@Configuration
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate() ;
}
}

3、修改order服务中的OrderService类中的findOrderByOrderId方法,根据Order对象中的userId查询User

  • Order类添加User属性,用于封装用户信息
1
2
3
4
5
6
7
8
9
10
11
@Data
public class Order {

private Long id ;
private Long userId ;
private String name ;
private BigDecimal price ;
private Integer num ;

private User user;
}
  • 具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class OrderServiceImpl implements OrderService {

@Autowired // 注入RestTemplate远程调用工具
private RestTemplate restTemplate ;

@Autowired
private OrderMapper orderMapper ;

@Override
public Order findOrderByOrderId(Long orderId) {

// 根据id查询订单数据
Order order = orderMapper.findOrderByOrderId(orderId);

// 发起远程调用
User user = restTemplate.getForObject("http://localhost:10100/api/user/findUserByUserId/" + order.getUserId(), User.class);
order.setUser(user);

// 返回订单数据
return order;
}

}

4.2.2 问题说明

上述的调用方式存在问题:

1、维护性差:服务提供方的ip地址发生了改变,那么此时服务的消费方就需要更改代码

2、缺少负载均衡机制:负载均衡就是负载【请求】通过多台服务器进行处理

如下图所示:

没有负载均衡机制的调用流程图:

image-20230503123503920

用户发送了3次请求,3次请求是通过一个用户微服务进行处理的,在高并发访问的情况下,用户微服务很有可能出现宕机。

有负载均衡机制的调用流程图:

image-20230503123633358

用户发送了3次请求,3次请求是通过多个用户微服务进行处理的,在高并发访问的情况下,每一个用户微服务只需要承担一小部分的请求即可。

5 Nacos注册中心

要想解决上述远程调用所存在的问题,就需要使用到Spring Cloud Alibaba中的Nacos注册中心。

5.1 注册中心简介

通过注册中心可以对服务提供方和服务消费方进行解耦。具体的工作模式如下图所示:

image-20230503124046820

工作流程说明:

1、服务提供方在启动的时候,会向注册中心注册自己服务的详情信息(ip、端口号等)。在注册中心中会维护一张服务清单,保存这些注册信息,注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

2、服务消费方向服务注册中心咨询服务,并获取所有服务的实例清单,然后按照指定的负载均衡算法从服务清单中选择一个服务实例进行访问。

5.2 注册中心产品

本小结主要给大家来介绍一下常见的注册中心的产品。

5.2.1 Eureka

Eureka是Netflix开源的一个基于REST的服务治理框架,主要用于实现服务注册、发现和负载均衡。通过Eureka,我们可以将微服务的各个实例注册到服务中心,并根据需要进行负载均衡和调用,从而实现整个微服务架构的高可用和弹性。

Eureka的架构图如下所示:

image-20230503124809297

Eureka包含两个组件:Eureka Server和Eureka Client。

服务提供者在启动时会通过Eureka Client向Eureka Server注册自己的信息(包括IP地址、端口号和服务名等),并且每隔一段时间会发送心跳来告诉Eureka Server它仍然存活。服务消费者可以通过Eureka Client从Eureka Server获取服务提供者的列表,并对这些服务进行负载均衡和调用。

Eureka的优点包括:

1、简单易用:Eureka框架非常简单易用,便于快速上手和部署。

2、高可用性:Eureka支持多节点部署,并会自动将失效的节点剔除,确保整个系统的高可用性和弹性。

3、动态扩展性:Eureka可以根据实际需求进行扩展,通过添加新的服务提供者可以很容易地增加应用程序的处理能力。

4、易于集成:Eureka可以与Spring Cloud等流行的微服务框架进行无缝集成,从而提供更完善的微服务体系支持。

Eureka的不足之处:

1、Eureka Server 为单点故障问题,虽然可以通过多节点部署来优化和缓解,但是在高并发场景下仍可能成为限制系统扩展的瓶颈。

2、Eureka的服务注册中心本身也需要高可用环境,一旦出现问题,可能影响到整个微服务的正常运行。

官网地址:https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/

5.2.2 Nacos

Nacos官网地址:https://nacos.io/zh-cn/

image-20230503130148076

Nacos是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos架构图如下所示:

image-20230503130501197

Nacos Server:服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。Nacos Server需要独立的部署。

Nacos Client: Nacos Client负责和Nacos Server进行通讯完成服务的注册和服务的发现。

Nacos Console:是Nacos的控制模块,Nacos提供了可视化的后台管理系统,可以很容易的实现服务管理操作。

Nacos的优点包括:

1、高可用性:Nacos支持多节点部署,通过选举算法实现了高可用和故障转移能力,在节点宕机或网络异常情况下仍能保证整个系统的稳定运行。

2、动态扩展性:Nacos可以根据实际需求进行快速扩展和缩容,支持集群、多数据中心、地域感知等特性。

3、完备的功能支持:Nacos支持服务注册与发现、配置管理、流量管理、DNS解析、存储KV对等功能,并且提供了Web界面和RESTful API等多种方式来使用这些功能。

4、易于集成:Nacos提供了多种语言和框架的集成方案,并且支持Spring Cloud等流行的微服务框架。

总的来说,Nacos是一个功能齐全、易于使用和高可用的分布式服务治理平台,可以为分布式系统提供高效、稳定的运行环境。

5.3 Nacos入门

5.3.1 Nacos安装

(1)使用docker安装Nacos(先不做)

本章节主要给搭建演示一下如下使用Docker来部署Nacos Server。Docker部署Nacos的项目命令如下所示:

1
2
3
4
5
6
7
# 拉取镜像
docker pull nacos/nacos-server:v2.2.2

# 创建容器
docker run --name nacos -e MODE=standalone -p 8848:8848 -p 9848:9848 -d nacos/nacos-server:v2.2.2

# nacos2.x的版本新增了一个客户端与服务端的gRpc的通讯端口号9848

打开浏览器访问nacos的所提供的后端管理界面:http://192.168.136.142:8848/nacos

用户名和密码:nacos/nacos

登录成功以后会进入到nacos的主页面:

image-20230503164647894

如果可以看到上述界面,就说明nacos的环境搭建好了。

(2)Windows环境安装Nacos

  • 下载Nacos安装文件

image-20230809095554425

  • 解压Nacos安装文件到没有中文和空格目录

  • 进入bin目录,使用cmd打开,通过命令启动Nacos务

startup.cmd -m standalone

image-20230809095749386

用户名和密码:nacos/nacos,登录成功以后会进入到nacos的主页面

5.3.2 微服务集成naocs

需求:将两个微服务(user、order)注册到nacos中

实现步骤:

1、在两个子工程中引入如下依赖

1
2
3
4
5
<!-- nacos作为注册中心的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、在application.yml文件中添加如下配置

1
2
3
4
5
6
7
8
spring:
# 配置nacos注册中心的地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
application:
name: spzx-cloud-user # 每一个服务注册到nacos注册中心都需要提供一个服务名称,order微服务注册的时候需要更改微服务名称

3、启动两个微服务:就可以在nacos的后台管理系统中,看到如下的注册信息:

image-20230809100204178

5.3.3 更改远程调用

当我们把微服务都注册到注册中心以后,那么此时就可以根据服务的名称从注册中心获取服务的ip地址和端口号了,进而就可以更改远程调用代码!

方案一

使用到Spring Cloud中所提供的一个服务发现的客户端对象:DiscoveryClient

代码实现:

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
@Service
public class OrderServiceImpl implements OrderService {

@Autowired // 注入RestTemplate远程调用工具
private RestTemplate restTemplate ;

@Autowired
private DiscoveryClient discoveryClient ;

@Autowired
private OrderMapper orderMapper ;

@Override
public Order findOrderByOrderId(Long orderId) {

// 根据id查询订单数据
Order order = orderMapper.findOrderByOrderId(orderId);

// 根据服务名称从注册中心中获取服务实例列表
ServiceInstance serviceInstance = chooseServiceInstance("spzx-cloud-user");

// 发起远程调用
User user = restTemplate.getForObject("http://" + serviceInstance.getHost() +":" + serviceInstance.getPort() +"/api/user/findUserByUserId/" + order.getUserId(), User.class);
order.setUser(user);

// 返回订单数据
return order;
}

// 根据服务的名称从注册中心中获取服务地址信息
public ServiceInstance chooseServiceInstance(String applicationName) {

// 获取服务实例列表
List<ServiceInstance> instances = discoveryClient.getInstances(applicationName);

// 编写一个简易的随机负载均衡算法
int size = instances.size();
Random random = new Random() ;
int instanceIndex = random.nextInt(size);
ServiceInstance serviceInstance = instances.get(instanceIndex);

// 返回服务实例
return serviceInstance ;
}

}

可以启动多个user微服务实例进行负载均衡的测试。

方案二

使用Spring Cloud中所提供的一个组件:spring-cloud-loadbalancer,关于该组件本小结先使用一下,在后面章节会重点讲解。

使用步骤:

1、在order微服务中添加依赖

1
2
3
4
5
<!-- spring cloud 所提供的负载均衡器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

2、在声明RestTemplate的方法上添加**@LoadBalanced**注解

1
2
3
4
5
@Bean
@LoadBalanced // 让RestTemplate具有负载均衡的能力
public RestTemplate restTemplate() {
return new RestTemplate() ;
}

3、更改远程调用代码

1
2
// 服务提供方的服务ip地址和端口号可以使用服务提供方的服务名称进行替换
restTemplate.getForObject("http://spzx-cloud-user/api/user/findUserByUserId/" + order.getUserId(), User.class);

注意:默认使用的负载均衡算法就是轮询【依次调用对应服务】

5.4 高级特性

5.4.1 服务集群

集群概述

在实际生产环境中,为了保证每一个服务的高可用,那么此时就需要去构建服务集群,但是并不是说把所有的服务都部署在一个机房里。而是将多个服务分散的部署到不同的机房中,每一个机房的服务可以看做成是一个集群。如下所示:

image-20230503182339408

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

image-20230503182806794

集群配置

  • 修改spzx-cloud-user的application.yml文件,添加集群配置:
1
2
3
4
5
spring:
cloud:
nacos:
discovery:
cluster-name: SH # 配置服务所属集群

启动三个服务user微服务实例,实例所属集群分配情况:实例1属于SH,实例2和实例3属于BJ

  • 通过添加添加JVM参数更改服务实例所属集群,启动实例2和实例3

image-20230809102346561

实例2:10101

1
-Dserver.port=10101 -Dspring.cloud.nacos.discovery.cluster-name=BJ

实例3:10103

1
-Dserver.port=10103 -Dspring.cloud.nacos.discovery.cluster-name=BJ
  • 启动三个用户微服务实例,查看实例分配情况:

image-20230503183655565

image-20230503183721175

集群访问

需求:当order服务优先访问SH集群中的user微服务实例,当SH集群中的user微服务实例出现问题以后,在访问BJ集群中的实例。

步骤:

1、给order微服务的application.yml文件,添加集群配置:

1
2
3
4
5
spring:
cloud:
nacos:
discovery:
cluster-name: SH # 配置服务所属集群

2、order微服务在loadbalancer组件中集成nacos

1
2
3
4
5
6
spring:
# 配置nacos注册中心的地址
cloud:
loadbalancer:
nacos: # 集成nacos的负载均衡算法
enabled: true

5.4.2 权重配置

实际部署中会出现这样的场景:服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下Nacos的负载均衡算法是同集群内随机挑选,不会考虑机器的性能问题。

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

  • 在Nacos控制台,找到spzx-cloud-user的实例列表,点击编辑,即可修改权重:

image-20230503185416608

权重取值范围:0~100

  • 在配置文件中进行权重配置:
1
2
3
4
5
spring:
cloud:
nacos:
discovery:
weight: 0.1

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

5.4.3 环境隔离

环境隔离概述

在实际的开发过程中,可能会存在很多个软件环境:开发环境、测试环境、生产环境。

nacos也是支持多环境隔离配置的,在nacos是通过namespace来实现多环境的隔离。

完整的服务注册数据存储结构如下所示:

image-20230503185847770

namespace + group 才可以确定具体的微服务实例。默认情况下,所有service、group都在同一个namespace,名为public。如下所示:

image-20230503190738675

创建名称空间

我们也可以创建新的名称空间,来将不同的服务隔离到不同的环境下面,如下所示:

image-20230503191050511

微服务配置名称空间

给微服务添加名称空间的配置,来指定该微服务所属环境。

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

1
2
3
4
5
6
spring:
# 配置nacos注册中心的地址
cloud:
nacos:
discovery:
namespace: 4a88035e-acf3-45a9-924f-2421acbff67a # 配置服务实例所属名称空间

此时order微服务所对应的服务实例就属于新的名称空间,user微服务所对应的服务实例属于public的名称空间,那么此时在进行远程调用的时候,就会出现如下的错误:

image-20230503191655562

5.4.4 实例类型

Nacos中的服务实例存在两种类型:

1、临时实例:如果实例宕机超过一定时间,会从服务列表剔除,并且实例会定时上报自身的健康状态给Nacos注册中心,默认的类型。

2、非临时实例:如果实例宕机,不会从服务列表剔除,Nacos注册中心会主动询问实例的健康状态,也可以叫永久实例。

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

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

6 LoadBalancer

6.1 LoadBalancer简介

Spring Cloud LoadBalancer是Spring Cloud中负责客户端负载均衡的模块,其主要原理是通过选择合适的服务实例来实现负载均衡。

客户端负载均衡:就是负载均衡算法由客户端提供

如下图所示:

image-20230503213502251

注意:Spring Cloud LoadBalancer的使用在《5.3.3更改远程调用方案二》中已经讲解过了,本章节主要来介绍一下Spring Cloud LoadBalancer其他的一些高级知识点。

6.2 LoadBalancer原理

Spring Cloud LoadBalancer的底层采用了一个拦截器【LoadBalancerInterceptor】,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:

image-20230503222331245

执行流程说明:

1、通过LoadBalancerInterceptor请求拦截器拦截我们的RestTemplate请求:http://spzx-cloud-user/api/user/findUserByUserId/1

2、获取请求的url,然后从请求的url中获取服务提供方的主机名称

3、然后调用LoadBalancerClient中的execute方法,将服务提供方的名称传递过去

4、在LoadBalancerClient的choose方法中通过ReactiveLoadBalancer.Factory从Nacos注册中心中获取服务列表以及负载均衡算法实例对象

5、通过ReactiveLoadBalancer从服务列表中选择一个服务实例地址,然后发起远程调用

6.3 源码跟踪

LoadBalancerInterceptor

核心源码如下所示:

image-20230503223822056

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

1、request.getURI():获取请求uri,本例中就是 http://spzx-cloud-user/api/user/findUserByUserId/1

2、originalUri.getHost():获取uri路径的主机名,其实就是服务id,spzx-cloud-user

3、this.loadBalancer.execute():处理服务id,和用户请求。

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

BlockingLoadBalancerClient

核心源码如下所示:

image-20230503224702411

ReactiveLoadBalancer.Factory的getInstance方法做了两件事情:

1、获取了一个具体的负载均衡算法对象

2、根据服务的id从Nacos注册中心中获取服务地址列表

紧跟着调用了RoundRobinLoadBalancer#choose方法,从服务列表中选择一个服务实例对象。

默认的负载均衡算法:RoundRobinLoadBalancer

6.4 更改负载均衡算法

LoadBalancer默认的负载均衡算法是RoundRobinLoadBalancer,如果想更改默认的负载均衡算法,那么此时需要向Spring容器中注册一个Bean,并且配置负载均衡的使用者。

代码如下所示:

1、在Spring容器中注册一个Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CustomLoadBalancerConfiguration {

/**
* @param environment: 用于获取环境属性配置,其中LoadBalancerClientFactory.PROPERTY_NAME表示该负载均衡器要应用的服务名称。
* @param loadBalancerClientFactory: 是Spring Cloud中用于创建负载均衡器的工厂类,通过getLazyProvider方法获取ServiceInstanceListSupplier对象,以提供可用的服务列表。
* ServiceInstanceListSupplier:用于提供ServiceInstance列表的接口,可以从DiscoveryClient或者其他注册中心中获取可用的服务实例列表。
* @return
*/
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}

2、配置负载均衡算法的使用者

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@LoadBalancerClients(value = {
@LoadBalancerClient(name = "spzx-cloud-user" , configuration = CustomLoadBalancerConfiguration.class) // 将负载均衡算法应用到指定的服务提供方中
})
public class RestTemplateConfiguration {

@Bean
@LoadBalanced // 让RestTemplate具有负载均衡的能力
public RestTemplate restTemplate() {
return new RestTemplate() ;
}

}

7 OpenFeign组件

7.1 OpenFeign简介

概述:feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign其作用就是帮助我们优雅的实现http请求的发送。

image-20230624093622996

使用RestTemplate进行远程调用代码回顾:

image-20230624093650991

存在的弊端:参数传递不太方便

7.2 OpenFeign入门

OpenFeign的使用步骤如下:

1、我们在spzx-cloud-order服务的pom文件中引入OpenFeign的依赖

1
2
3
4
5
<!-- 加入OpenFeign的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、在启动类上添加**@EnableFeignClients**开启OpenFeign的功能支持

3、编写OpenFeign的客户端

1
2
3
4
5
6
7
@FeignClient(value = "spzx-cloud-user")		// 声明当前接口是一个访问user-service的feign的客户端
public interface UserFeignClient {

@GetMapping("/api/user/findUserByUserId/{userId}")
public abstract User queryById(@PathVariable("userId") Long userId) ; // 根据userId查询用户信息的接口方法

}

这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:

① 请求方式:GET

② 请求路径:/api/user/findUserByUserId/{userId}

③ 请求参数:Long userId

④ 返回值类型:User

这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。

4、修改OrderService中的远程调用代码,使用Feign客户端代替RestTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderMapper orderMapper ;

@Autowired
private UserFeignClient userFeignClient ;

@Override
public Order findOrderByOrderId(Long orderId) {
Order order = orderMapper.findOrderByOrderId(orderId);

// 远程调用
User user = userFeignClient.queryById(order.getUserId());
order.setUser(user);
return order ;
}
}

7.3 OpenFeign自定义配置

7.3.1 日志配置

OpenFeign可以支持很多的自定义配置,如下表所示:

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign.Contract 支持的注解格式 默认是SpringMVC的注解
feign.Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

下面以日志为例来演示如何自定义配置,支持两种方式的配置:

基于配置文件的方式

基于配置文件修改feign的日志级别可以针对单个服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 将feign包下产生的日志的级别设置为debug
logging:
level:
com.atguigu.spzx.cloud.order.feign: debug

# openfeign日志级别配置
spring:
cloud:
openfeign:
client:
config:
spzx-cloud-user:
loggerLevel: full

也可以针对所有服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 将feign包下产生的日志的级别设置为debug
logging:
level:
com.atguigu.spzx.cloud.order.feign: debug

# openfeign日志级别配置
spring:
cloud:
openfeign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: full

而日志的级别分为四种:

① NONE:不记录任何日志信息,这是默认值。

② BASIC:仅记录请求的方法,URL以及响应状态码和执行时间

③ HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息

④ FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Java代码的方式

也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

1
2
3
4
5
6
public class DefaultFeignConfiguration  {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

如果是局部生效,则把它放到对应的@FeignClient这个注解中:

1
@FeignClient(value = "spzx-cloud-user", configuration = DefaultFeignConfiguration .class) 

7.3.2 超时配置

超时机制概述:Feign 的超时机制是指在使用 Feign 进行服务间的 HTTP 调用时,设置请求的超时时间。当请求超过设定的超时时间后,Feign 将会中断该请求并抛出相应的异常。

超时机制的意义

1、防止长时间等待:通过设置适当的超时时间,可以避免客户端在请求服务时长时间等待响应而导致的性能问题。如果没有超时机制,客户端可能会一直等待,从而影响整个系统的吞吐量和响应时间。

2、避免资源浪费:超时机制可以帮助及时释放占用的资源,例如连接、线程等。如果请求一直处于等待状态而不超时,将导致资源的浪费和系统的负载增加。

3、优化用户体验:超时机制可以防止用户长时间等待无响应的情况发生,提供更好的用户体验。当请求超时时,可以及时给出错误提示或进行相应的处理,以提醒用户或采取其他措施。

feign默认的超时配置为:

image-20230624103625541

超时时间越长,资源浪费的时间就越长,系统的稳定性就越差,因此需要设置为一个较为合理的超时时间,设置防止如下所示:

1
2
3
4
5
6
7
8
9
spring:
cloud:
openfeign:
client:
config:
default:
loggerLevel: full
read-timeout: 2000 # 读取数据的超时时间设置为2s
connect-timeout: 2000 # 建立连接的超时时间设置为2s

7.3.2 重试配置

feign一旦请求超时了,那么此时就会直接抛出SocketTimeoutException: Read timed out的异常。请求超时的原因有很多种,如网络抖动、服务不可用等。如果由于网络暂时不可用导致触发了超时机制,那么此时直接返回异常信息就并不是特别的合理,尤其针对查询请求,肯定希望得到一个结果。合理的做法:触发超时以后,让feign进行重试

具体步骤:

1、自定义重试器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FeignClientRetryer implements Retryer {

// 定义两个成员变量来决定重试次数
private int start = 1 ;
private int end = 3 ;

@Override
public void continueOrPropagate(RetryableException e) { // 是否需要进行重试取决于该方法是否抛出异常,如果抛出异常重试结束
if(start >= end) {
throw new RuntimeException(e) ;
}
start++ ;
}

@Override
public Retryer clone() { // 框架底层调用该方法得到一个重试器
return new FeignClientRetryer();
}
}

2、配置重试器

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
openfeign:
client:
config:
default:
loggerLevel: full
read-timeout: 2000
connect-timeout: 2000
retryer: com.atguigu.spzx.cloud.order.feign.FeignClientRetryer # 配置自定义重试器

8 Nacos配置中心

Nacos除了可以做注册中心,同样可以做配置管理来使用。

统一配置管理

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就显得十分的不方便,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。

image-20230624171403235

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

Nacos入门

Nacos中添加配置

在Nacos服务端创建一个配置,如下所示:

image-20230624171530387

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

image-20210714164856664

微服务集成配置中心

微服务需要进行改造,从Nacos配置中心中获取配置信息进行使用。

步骤:

1、在spzx-cloud-user微服务中,引入spring-cloud-starter-alibaba-nacos-config依赖

1
2
3
4
5
<!-- nacos作为配置中心时所对应的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、在spzx-cloud-user项目的 /src/main/resources/application.yml 配置文件中配置 Nacos Config 地址并引入服务配置

1
2
3
4
5
6
7
8
9
# 配置数据库的连接信息
spring:
cloud:
nacos:
config:
server-addr: 192.168.136.142:8848
config:
import:
- nacos:spzx-cloud-user-dev.yml

读取自定义配置

@Value

通过@Value注解读取自定义配置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping(value = "/api/user")
@Slf4j
public class UserController {

@Autowired
private UserService userService ;

@Value("${pattern.dateformat}")
private String pattern ;

@GetMapping(value = "/findUserByUserId/{userId}")
public User findUserByUserId(@PathVariable(value = "userId") Long userId , @RequestHeader(name = "Truth")String header) {
log.info("UserController...findUserByUserId方法执行了... ,header: {} , dateformat: {} " , header , pattern);
return userService.findUserByUserId(userId) ;
}

}

@ConfigurationProperties

也可以通过实体类,配合@ConfigurationProperties注解读取自定义配置,代码如下所示:

1、定义一个实体类,代码如下所示:

1
2
3
4
5
6
7
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {

private String dateformat ;

}

2、在启动类上添加@EnableConfigurationProperties注解,如下所示:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableConfigurationProperties(value = { PatternProperties.class })
public class UserApplication {

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

}

3、使用该实体类,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping(value = "/api/user")
@Slf4j
public class UserController {

@Autowired
private UserService userService ;

@Value("${pattern.dateformat}")
private String pattern ;

@Autowired // 注入实体类
private PatternProperties patternProperties ;

@GetMapping(value = "/findUserByUserId/{userId}")
public User findUserByUserId(@PathVariable(value = "userId") Long userId , @RequestHeader(name = "Truth")String header) {
log.info("UserController...findUserByUserId方法执行了... ,header: {} , dateformat: {} " , header , patternProperties.getDateformat());
return userService.findUserByUserId(userId) ;
}

}

配置热更新

我们最终的目的,是修改Nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。实现配置的热更新有两种方式:

方式一:在@Value注入的变量所在类上添加注解**@RefreshScope**

image-20230624200928589

方式二:通过实体类,配合@ConfigurationProperties注解读取配置信息,自动支持热更新

配置优先级

思考问题:如果在application.yml文件中和Nacos配置中心中都定义了相同的配置内容,那么哪一个配置的优先级较高呢?

优先级顺序:Nacos配置中心的配置(后导入的配置 > 先导入的配置) > application.yml

9 Gateway组件

Gateway简介

官网

image-20230721090940955

概述

  • Gateway是在spring生态系统之上构建的API网关服务,基于Spring5,SpringBoot2和Project Reactor等技术。Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试
  • SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring5.X+SpringBoot2.X和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式
  • 为了提升网关的性能,SpringCloud Gatway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通讯框架Netty。
  • SpringCloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全、监控/指标、和限流。

架构图

image-20230624161556300

三大核心概念

Route(路由)

路由是构建网关的基本模块,它由ID,目标URI,一系列的断言过滤器组成,如果断言为true则匹配该路由

image-20230721091801547

Predicate(断言)

参考的是java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

image-20230721091835236

Filter(过滤)

指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

image-20230721091858141

工作流程

image-20230721092104682

image-20230721092129655

  • 客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求匹配的路由,将其发送到Gateway Web Handler.
  • Handler再通过指定的过滤器链来将请求发送给我们实际的服务执行业务逻辑,然后返回。
  • 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(”pre”)或之后(“post”)执行业务逻辑。
  • Filter在”pre“类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在”post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量控制等有着非常重要的作用

Gateway入门

下面,我们就演示下网关的基本路由功能。基本步骤如下:

  • 1、在spzx-cloud-parent下创建子模块spzx-cloud-gateway

image-20230721101439700

  • 2、引入如下依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--网关-->
<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>

<!-- 负载均衡组件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

image-20230721092532222

  • 3、编写启动类
1
2
3
4
5
6
7
8
// com.atguigu.spzx.cloud.gateway
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
  • 4、在application.yml配置文件中编写基础配置和路由规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8222
spring:
application:
name: spzx-cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: spzx-cloud-user # 路由id,可以自定义,只要唯一即可
uri: lb://spzx-cloud-user # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates:
- Path=/*/user/** # 路径匹配
- id: spzx-cloud-order
uri: lb://spzx-cloud-order
predicates:
- Path=/*/order/** # 路径匹配
nacos:
discovery:
server-addr: 127.0.0.1:8848
  • 5、重启测试

重启网关,访问http://localhost:8222/api/user/findUserByUserId/1时,符合`/api/user/**`规则,

请求转发到uri:http://spzx-cloud-user/api/user/findUserByUserId/1,得到了结果:

image-20230624163155679

Predicate的使用

  • 启动网关服务后,在控制台可以看到如下信息:

image-20230721101903404

  • 思考问题:我们在配置文件中只是配置了一个访问路径的规则,怎么就可以实现路由呢?

底层原理:框架底层会自动读取配置文件中的内容,然后通过制定的路由工厂将其转换成对应的判断条件,然后进行判断。在Gateway中提供了很多的路由工厂如下所示:https://docs.spring.io/spring-cloud-gateway/docs/4.0.6/reference/html/#gateway-request-predicates-factories

image-20230624163811030

大致有12个,每一种路由工厂的使用Spring Cloud的官网都给出了具体的示例代码,我们可以参考示例代码进行使用。以After Route Predicate

Factory路由工厂举例,如下所示:

1
2
3
4
5
6
7
8
9
spring:
cloud:
gateway:
routes:
- id: spzx-cloud-user
uri: lb://spzx-cloud-user
predicates:
- Path=/api/user/**
- After=2023-07-21T10:23:06.978038800+08:00[Asia/Shanghai] # 系统时间在2023-07-21之后才可以进行访问
1
2
3
//获取当前时区时间代码
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
  • 总结

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapper基础框架的一部分。
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给 Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and 。

过滤器

过滤器简介

在gateway中要实现其他的功能:权限控制、流量监控、统一日志处理等。就需要使用到gateway中所提供的过滤器了。过滤器,可以对进入网关的请求和微服务返回的响应做处理:

image-20230624164230054

内置过滤器

spring gateway提供了30多种不同的过滤器。

官网地址:https://docs.spring.io/spring-cloud-gateway/docs/4.0.x/reference/html/#gatewayfilter-factories

例如:

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

在Gateway中提供了三种级别的类型的过滤器:

1、路由过滤器:只针对当前路由有效

2、默认过滤器:针对所有的路由都有效

3、全局过滤器:针对所有的路由都有效,需要进行自定义

路由过滤器

需求:给所有进入spzx-cloud-user的请求添加一个请求头:Truth=atguigu

实现:

1、修改gateway服务的application.yml文件,添加路由过滤

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: spzx-cloud-user
uri: lb://spzx-cloud-user
predicates:
- Path=/api/user/**
filters:
- AddRequestHeader=Truth, atguigu # 配置路由基本的过滤器,给访问user微服务的所有接口添加Truth请求头

当前过滤器写在spzx-cloud-user路由下,因此仅仅对访问spzx-cloud-user的请求有效。

2、在spzx-cloud-user的接口方法中读取请求头数据,进行测试

1
2
3
4
5
@GetMapping(value = "/findUserByUserId/{userId}")
public User findUserByUserId(@PathVariable(value = "userId") Long userId , @RequestHeader(name = "Truth")String header) {
log.info("UserController...findUserByUserId方法执行了... ,header: {} " , header);
return userService.findUserByUserId(userId) ;
}

默认过滤器

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: spzx-cloud-user
uri: lb://spzx-cloud-user
predicates:
- Path=/api/user/**
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
default-filters:
- AddRequestHeader=Truth, atguigu is good

全局过滤器

  • 概述

上述的过滤器是gateway中提供的默认的过滤器,每一个过滤器的功能都是固定的。但是如果我们希望拦截请求,做自己的业务逻辑,默认的过滤器就没办法实现。此时就需求使用全局过滤器,全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

  • 需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

请求参数中是否有username,如果同时满足则放行,否则拦截

  • 步骤分析:

1、定义一个类实现GlobalFilter接口

2、重写filter方法

3、将该类纳入到spring容器中

4、实现Ordered接口定义该过滤器的顺序

  • 实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class AuthorizationFilter implements GlobalFilter, Ordered {

//实现过滤器逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String username = exchange.getRequest().getQueryParams().getFirst("username");
if(!StringUtils.hasText(username)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

//定义该过滤器的顺序
@Override
public int getOrder() {
return 0;
}
}

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:

image-20230624170925571

排序的规则是什么呢?

1、按照order的值进行排序,order的值越小,优先级越高,执行顺序越靠前。

2、路由过滤器和默认过滤器会按照order的值进行排序,这个值由spring进行指定,默认是按照声明顺序从1递增

3、当过滤器的order值一样时,会按照 globalFilter > defaultFilter > 路由过滤器的顺序执行

核心源码分析:org.springframework.cloud.gateway.handler.FilteringWebHandler#handle方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = (Route)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);

// 获取路由级别的过滤器和默认过滤器的集合
List<GatewayFilter> gatewayFilters = route.getFilters();

// 获取全局过滤器的集合
List<GatewayFilter> combined = new ArrayList(this.globalFilters);

// 将取路由级别的过滤器和默认过滤器的集合中的元素添加到全局过滤器的集合中
combined.addAll(gatewayFilters);

// 进行排序
AnnotationAwareOrderComparator.sort(combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}

// 调用过滤器链中的filter方法
return (new DefaultGatewayFilterChain(combined)).filter(exchange);
}

10 Sentinel组件

初识sentinel

雪崩效应

概述:在微服务系统架构中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。一个服务的不可用导致整个系统的不可用的现象就被称之为雪崩效应。

如下图所示:

image-20230624203044831

当服务D出现了问题了以后,调用服务D的服务A的线程就得不到及时的释放,在高并发情况下,随着时间的不断推移服务A的系统资源会被线程耗尽,最终导致服务A出现了问题,同理就会导致其他的服务也不能进行访问了。

解决方案

超时处理

超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待

image-20230624203153340

隔离处理

隔离处理:将错误隔离在可控的范围之内,不要让其影响到其他的程序的运行。

这种设计思想,来源于船舱的设计,如下图所示:

image-20230624203222353

船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。于此类似,我们业务系统也可以使用这种思想来防止出现雪崩效应,常见的隔离方式:线程隔离

image-20230624203256590

熔断处理

熔断处理:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。

断路器会统计访问某个服务的请求数量,异常比例如下所示:

image-20230624203334370

请求了三次,两次出现异常,一次成功。当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断:

image-20230624203409785

触发熔断了以后,当在访问服务A的时候,就不会在通过服务A去访问服务D了,立马给用户进行返回,返回的是一种默认值,这种返回就是一种兜底方案。这种兜底方案也将其称之为降级逻辑。

流量控制

流量控制:限制业务访问的QPS(每秒的请求数),避免服务因流量的突增而故障。

image-20230624203508014

限流是一种预防措施,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。其他的处理方式是一种补救措施,在部分服务故障时,将故障控制在一定范围,避免雪崩。

sentinel介绍

官网地址:https://sentinelguard.io/zh-cn/

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

  • Sentinel 的历史:

- 2012 年,Sentinel 诞生,主要功能为入口流量控制。

- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。

- 2018 年,Sentinel 开源,并持续演进。

- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对 Service Mesh 场景也推出了 Envoy 集群流量控制支持,以解决 Service Mesh 架构下多语言限流的问题。

- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。

  • Sentinel 分为两个部分:

- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。

- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

image-20230624203655208

具有的特征:

image-20230624203730680

sentinel入门

下载sentinel控制台

sentinel管理后台下载地址:https://github.com/alibaba/Sentinel/releases

image-20230624215112184

下载完毕以后就会得到一个jar包

image-20230624215403344

3.2.2 启动sentinel

  • 将jar包放到任意非中文目录,执行命令:
1
java -jar sentinel-dashboard-2.0.0-alpha-preview.jar
  • 如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
配置项 默认值 说明
server.port 8080 服务端口
sentinel.dashboard.auth.username sentinel 默认用户名
sentinel.dashboard.auth.password sentinel 默认密码
  • 例如,修改端口:
1
java -Dserver.port=8090 -jar sentinel-dashboard-2.0.0-alpha-preview.jar

3.2.3 访问sentinel

访问http://localhost:8080页面,就可以看到sentinel的控制台了:

image-20230624215635555

需要输入账号和密码,默认都是:sentinel

登录后,发现一片空白,什么都没有:因为还没有监控任何服务。另外,sentinel是懒加载的,如果服务没有访问,看不到该服务信息。

image-20230624215704921

3.2.4 整合sentinel

我们在spzx-cloud-user中整合sentinel,并连接sentinel的控制台,步骤如下:

1、引入sentinel依赖

1
2
3
4
5
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2、配置控制台

修改application.yaml文件,添加下面内容

1
2
3
4
5
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # 配置sentinel控制台地址

3、访问spzx-cloud-user的任意接口

打开浏览器,访问http://localhost:10100/api/user/findUserByUserId/1,这样才能触发sentinel的监控。然后再访问sentinel的控制台,查看效果:

image-20230624220303385

流量控制

雪崩问题虽然有四种方案,但是限流是避免服务因突发的流量而发生故障,是对微服务雪崩问题的预防。我们先学习这种模式。

相关概念

簇点链路:当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。

资源:簇点链路中被监控的每一个接口就是一个资源,流控、熔断等都是针对簇点链路中的资源来设置的。

默认情况下sentinel会监控spring mvc的每一个端点(Endpoint,也就是controller中的方法),因此spring mvc的每一个端点就是调用链路中的一个资源。

例如,我们刚才访问的spzx-cloud-user中的UserController中的端点:/api/user/findUserByUserId/{userId}

image-20230624220603571

我们可以点击对应资源后面的按钮来设置规则:

1、流控:流量控制

2、降级:降级熔断

3、热点:热点参数限流,是限流的一种

4、授权:请求的权限控制

快速入门

需求:给 /api/user/findUserByUserId/{userId}这个资源设置流控规则,QPS不能超过 5,然后测试。

步骤:

1、首先在sentinel控制台添加限流规则

image-20230628090407483

2、利用jmeter测试(模拟并发请求)

Apache JMeter 是 Apache 组织基于 Java 开发的压力测试工具,用于对软件做压力测试

下载地址:https://archive.apache.org/dist/jmeter/binaries/

课前资料提供了编写好的Jmeter测试样例

image-20230628115300889

通过如下命令打开jmeter

1
java -jar ApacheJMeter.jar

导入课前资料提供的测试样例

image-20220320111824238

选择流控入门

image-20220320111955904

10个线程,1秒内运行完,QPS是10,超过了5。

选中流控入门,QPS<5右键运行

image-20220320112040803

注意:不要点击菜单中的执行按钮来运行。

点击查看结果树,理想的请求执行结果应该如下所示:

image-20220320112142379

可以看到,成功的请求每次只有5个。

注意:如果测试结果不是上述情况,那是因为sentinel在统计请求的时候,把一部分的请求统计到了下一秒中导致的。

流控模式

流控模式简介

在添加限流规则时,点击高级选项,可以选择三种流控模式

1、直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式

2、关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流

3、链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流

如下所示:

image-20230628091856122

快速入门测试的就是直接模式。

关联模式

关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流

配置方式:

image-20230628092034118

语法说明:对/api/user/updateUserById资源的请求进行统计,当访问流量超过阈值时,就对/api/user/findUserByUserId/{userId}进行限流,避免影响/api/user/updateUserById资源。

使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。

案例实现:

1、在UserController新建一个端点:/api/user/updateUserById,无需实现业务

1
2
3
4
5
// 修改用户数据端点
@GetMapping(value = "/updateUserById")
public String updateUserById() {
return "修改用户数据成功";
}

2、重启服务,访问对应的端点,让其产生簇点链路

image-20230628092515666

3、配置流控规则,当/api/user/updateUserById资源被访问的QPS超过5时,对/api/user/findUserByUserId/1请求限流。对哪个端点限流,就点击哪个端点后面的按钮。我们是对用户查询/api/user/findUserByUserId/1限流,因此点击它后面的按钮:

image-20230628092751648

4、在Jmeter中进行测试

选择《流控模式-关联》:

image-20220320114459422

可以看到1000个线程,100秒,因此QPS为10,超过了我们设定的阈值:5

查看http请求:

image-20230628093126793

请求的目标是/api/user/updateUserById,这样这个端点就会触发阈值。但限流的目标是/api/user/findUserByUserId/1,我们在浏览器访问,可以发现:

image-20230628093300378

确实被限流了。

关联流控模式的使用场景:

1、两个有竞争关系的资源

2、一个优先级较高,一个优先级较低

对高优先级的资源的流量进行统计,当超过阈值对低优先级的资源进行限流。

链路模式

链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值,如果超过阈值对从该链路请求进行限流。

配置方式:

1、/api/user/save –> users

2、/api/user/query –> users

如果只希望统计从/api/user/query进入到users的请求,并进行限流操作,则可以这样配置:

image-20230628095013326

案例实现:

1、在UserService中添加一个queryUsers方法,不用实现业务

1
2
3
public void queryUsers(){
System.err.println("查询用户");
}

2、在UserController中,添加两个端点,在这两个端点中分别调用UserService中的queryUsers方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping(value = "/save")
public String save() {
userService.queryUsers();
System.out.println("保存用户");
return "订单保存成功" ;
}

@GetMapping(value = "/query")
public String query() {
userService.queryUsers();
System.out.println("查询用户");
return "查询用户成功" ;
}

4、通过**@SentinelResource**标记UserService中的queryUsers方法为一个sentinel监控的资源(默认情况下,sentinel只监控controller方法)

1
2
3
4
@SentinelResource("users")
public void queryUsers(){
System.err.println("查询用户");
}

5、更改application.yml文件中的sentinel配置

链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入spring mvc的所有请求设置同一个root资源,会导致链路模式失效。因此需要关闭这种资源整合。

1
2
3
4
spring:
cloud:
sentinel:
web-context-unify: false # 关闭context整合

6、重启服务,访问/api/user/save和/api/user/query,可以查看到sentinel的簇点链路规则中,出现了新的资源

image-20230628094306219

7、添加流控规则

点击users资源后面的流控按钮,在弹出的表单中填写下面信息:

image-20230628094433574

只统计从/api/user/query进入/users的资源,QPS阈值为2,超出则被限流。

8、jmeter测试

选择《流控模式-链路》

image-20220320150559229

可以看到这里200个线程,50秒内发完,QPS为4,超过了我们设定的阈值2。

一个http请求是访问/api/user/save

image-20230628094648097

另一个是访问/api/user/query

image-20230628094713655

运行测试,察看结果树:

访问/api/user/save,没有进行限流

image-20230628094814795

访问/api/user/query,进行限流了

image-20230628094857374

流控效果

在流控的高级选项中,还有一个流控效果选项

image-20230628095109686

流控效果是指请求达到流控阈值时应该采取的措施,包括三种:

1、快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常,是默认的处理方式

2、warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常,但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值

3、排队等待:让所有的请求按照先后次序进入到一个队列中进行排队,当某一个请求最大的预期等待时间超过了所设定的超时时间时同样是拒绝并抛出异常

warm up

阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。

warm up也叫预热模式,是应对服务冷启动的一种方案。阈值会动态变化,从一个较小值逐渐增加到最大阈值。

工作特点:请求阈值初始值是 maxThreshold / coldFactor, 持续指定时长(预热时间)后,逐渐提高到maxThreshold值,而coldFactor的默认值是3。

例如,我设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10。

image-20220320152944101

案例需求:给/api/user/findUserByUserId/{userId}这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒

1、配置流控规则

image-20230628095505037

2、jmeter测试

选择《流控效果,warm up》

image-20220320153409220

QPS为10

刚刚启动时,大部分请求失败,成功的只有3个,说明QPS被限定在3:

image-20220320153522505

随着时间推移,成功比例越来越高

image-20220320153646510

到sentinel控制台查看实时监控

image-20230628095925921

排队等待

排队等待:让所有的请求按照先后次序进入到一个队列中进行排队,当某一个请求最大的预期等待时间超过了所设定的超时时间时同样是拒绝并抛出异常

例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。

那什么叫做预期等待时长呢?

比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:

1、第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms

2、第12个请求的预期等待时长 = 200 * (12-1) = 2200ms

现在,第1秒同时接收到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的:

image-20230628100019712

如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑

image-20230628100049968

平滑的QPS曲线,对于服务器来说是更友好的。

案例需求:给/api/user/findUserByUserId/{userId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s

1、添加流控规则

image-20230628100313331

2、jmeter测试

image-20220320154801992

QPS为15,已经超过了我们设定的10。

运行测试用例,察看结果树:

image-20220320155103019

全部都通过了。

再去sentinel查看实时监控的QPS曲线

image-20220320155202523

QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。

热点参数限流

配置介绍

之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。

例如,一个根据id查询商品的接口:

image-20230628100914491

访问/goods/{id}的请求中,id参数值会有变化,热点参数限流会根据参数值分别统计QPS,统计结果:

image-20230628101012945

当id=1的请求触发阈值被限流时,id值不为1的请求不受影响。

配置方式(点击资源中的热点按钮):

image-20230628101216576

代表的含义是:对hot这个资源的0号参数(第一个参数)做统计,每1秒相同参数值的请求数不能超过2。这种配置是对查询商品这个接口的所有商品一视同仁,QPS都限定为5。而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:

image-20230628101331468

案例演示

案例需求:给/api/user/findUserByUserId/{userId}这个资源添加热点参数限流,规则如下:

1、默认的热点参数规则是每1秒请求量不超过2

2、给2这个参数设置例外:每1秒请求量不超过4

3、给3这个参数设置例外:每1秒请求量不超过10

注意事项:热点参数限流对默认的spring mvc资源无效,需要利用@SentinelResource注解标记资源

实现步骤:

1、标记资源

给UserController中的/api/user/findUserByUserId/{userId}资源添加注解:

1
2
3
4
5
6
7
@SentinelResource("hot")  // 声明资源名称
@GetMapping(value = "/findUserByUserId/{userId}")
public User findUserByUserId(@PathVariable(value = "userId") Long userId ,
@RequestHeader(name = "Truth" , required = false)String header) {
log.info("UserController...findUserByUserId方法执行了... ,header: {} , dateformat: {} " , header , patternProperties.getDateformat());
return userService.findUserByUserId(userId) ;
}

2、热点参数限流规则

访问该接口,可以看到我们标记的hot资源出现了

image-20230628101715773

这里不要点击hot后面的按钮,页面有BUG

点击左侧菜单中热点规则菜单:

image-20230628102031276

3、jmeter测试

选择《热点参数限流 QPS1》

image-20220320162420189

这里发起请求的QPS为5。

包含三个请求,参数分别为:101 , 102 , 103,运行测试程序,察看结果树:

101 image-20220320162905731
102 image-20220320163002195
103 image-20220320163023729