0%

SpringCloud

SpringCloud微服务架构

一、基础概念

基于分布式的微服务架构—整体架构

分布式的整体维度:

  • 服务注册与发现
  • 服务调用
  • 服务熔断
  • 负载均衡
  • 服务降级
  • 服务消息队列
  • 配置中心管理
  • 服务网关
  • 服务监控
  • 全链路追踪
  • 自动化构建部署
  • 服务定时任务调度操作

1.SpringCloud简介

SpringCloud=分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶

由n多个springboot组成

2.基本使用技术

(2020.2之后每部分技术都有所更新)

image-20220824102603

image-20220906104143705

3.版本选择

SpringBoot版本使用数字标识
SpringCloud版本使用字母标识(采用伦敦地铁站的名字命名)

由 SpringCloud 版本选择 SpringBoot版本

boot 和 cloud 适配的版本

按照官网给的标准来(解析json)
https://start.spring.io/actuator/info

4.教程使用版本

5.组件停更说明

基本全部组件停更不停用,有新的组件去替换

SpringCloud 中文知道手册

Spring Cloud Greenwich 中文文档 参考手册 中文版

二、聚合父工程

创建项目

1.新建项目,选择Maven版本

2.字符编码

3.注解生效激活

4.Java版本选8

5.file type 过滤

父工程pom文件

删掉生成的src文件夹

1.修改打包方式为pom (默认是jar)

2.统一jar包版本
3.配置 dependencyManagement

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
<!-- 统一管理 jar 包版本 -->  
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>8.0.21</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>

<!-- 子块基础之后,提供作用(并不是引入依赖,子类还需自己导入需要的依赖):锁定版本 + 子module不用写 groupId 和 version -->
<dependencyManagement>
<dependencies>
<!-- 下面三个基本是微服务架构的标配 -->
<!--spring boot 2.2.2--> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<!--scope=import 来实现多继承-->
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud 阿里巴巴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<!--junit 单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.2</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>

如果导入的依赖爆红,先把 dependencyManagement 注释掉再导入(因为dependencyManagement只是锁定版本)
需要修改的话,就是改变三个标配的版本

通常在最顶层的父 pom 会看到 dependencyManagement
Maven会沿着父子层次向上走,直到找到一个拥有 dependencyManagement 元素的项目,然后会使用这个 dependencyManagement 元素中指定的版本号
如果子项目需要其他版本,自己加version即可

dependencyManagement 里只声明依赖,并不实现引入;只有在子项目中写入该依赖,并没有指定具体版本,才会从父项目中继承该项。并且version和scope都读取自父pom

跳过测试单元

三、构建支付模块

微服务模块:

  1. 建module
  2. 改POM
  3. 写YML
  4. 主启动
  5. 业务类

1. 建moudle

image-20220826090714
选择maven项目

查看父pom发现已有改变

2. 改子类的pom

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
    <dependencies>  
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口、健康检测-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- 这里写了版本号就用子类的,没写就使用父类的-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3. 配置yml文件(自己新建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:  
port: 8001

spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/reboot?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
username: root
password: root

mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.atguigu.springcloud.entities # 所有entity别名类所在包

4. 写启动类

1
2
3
4
5
6
@SpringBootApplication  
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}

5.业务类

1> 创数据库和表

image-20220829091535923

image-20220829091545876

2> 创建实体类

主实体Payment

json封装体CommonResult

image-20220829091610705

实现 Serializable 对象序列化接口;作用:

  1. 便于存储
  2. 便于传输
  3. 保障数据可读性;(内存不够时,暂时存储到硬盘中)
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
// Serializable 对象序列化的接口
public class Payment implements Serializable { // 实体类序列化(后头分布式中要用到)
private Long id;
private String serial;
}
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;

public CommonResult(Integer code,String message) {
this(code,message,null);
}
}

3> dao

@Mapper Apache的,推荐使用

@Repository spring的,容易出现问题

1
2
3
4
5
6
7
8
@Mapper
public interface PaymentDao {

public int create(Payment payment);

public Payment getPaymentById(@Param("id") Long id);

}

XML文件

namespace —- 强调关联到哪个dao

parameterType —- 映射实体类(传参) 可传Java对象,数组,map…

useGeneratedKeys —– 返回一个数字,成功—1;失败—0

映射数据库和Java实体类

1
<id column="order_status" property="orderStatus" javaType="INT"/>

字段名 —- 变量名 —- 类型(类型有的时候加了会报错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.dao.PaymentDao">
<!--Payment 为自定义的实体类-->
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values (#{serial})
</insert>

<resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id=#{id}
</select>

</mapper>

4> service

1
2
3
4
5
6
7
public interface PaymentService {

public int create(Payment payment);

public Payment getPaymentById(@Param("id") Long id);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class PaymentServiceImpl implements PaymentService {

// 和 @Autowired 一样也是自动装填,但 Resource 是Java自己的方法
@Resource
private PaymentDao paymentDao;

public int create(Payment payment) {
return paymentDao.create(payment);
}

public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}

}

5> controller

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
@RestController
@Slf4j
public class PaymentController {

@Resource
private PaymentService paymentService;

// 写操作 post
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment) {
// int 是因为有 useGeneratedKeys 成功返回1,失败返回0
int result = paymentService.create(payment);
log.info("***插入结果:"+result);

if (result > 0) {
return new CommonResult(200,"插入成功",result);
} else {
return new CommonResult(444,"插入失败",null);
}
}

// 读操作 get
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("***插入结果:"+payment);

if (payment != null) {
return new CommonResult(200,"查询成功",payment);
} else {
return new CommonResult(444,"没有对应记录,查询ID:"+id,null);
}
}
}

6> 测试

浏览器可支持Get请求,但不太支持Post请求,需要使用postma

测就完了

image-20220829103904955

6.热部署Devtools

部署后就不需要每次修改完项目都手动重启

只在开发测试阶段使用,上线后关闭

1> 添加依赖(在子工程的pom文件中)

指定了依赖(第三方jar包)的 作用范围

A项目添加该选项后,在B中就不会出现此依赖,除非删掉或写为false

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

2> 添加插件到父工程 pom 中

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.2</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>

3> 开启自动编译选项

image-20220829105717430

4> 更新值

Ctrl + Shift + Alt + /

选择 Register

image-20220829110507456

2021新版 compiler automake allow when app running 移到setting里了

image-20220829110908379

5> 重启 idea

四、客户端(在cloud下创建另一个工程)

1. 还是老样子,5步走

  1. 建 moudle
  2. 导 pom
  3. 写 yml
  4. 写启动类
  5. 业务类

image-20220829143940939

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
<dependencies>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
server:
port: 8002

主启动类

1
2
3
4
5
6
@SpringBootApplication
public class OrderMain8002 {
public static void main(String[] args) {
SpringApplication.run(OrderMain8002.class,args);
}
}

把实体类的内容从8001项目里copy过来,创建controller

image-20220829144448634

2. 配置RestTemplat

该项目的目的是调用8001端口,来实现客户端的功能

RestTemplat:提供了多种便捷访问远程Http服务的方法;是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集

image-20220829150325562

1
2
3
4
5
6
7
8
@Configuration
public class ApplicationContextConfig {

@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

3. 业务类

只要一个controller就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@Slf4j
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:8001";

@Resource
private RestTemplate restTemplate;

@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL + "/payment/create",payment,CommonResult.class);
}

@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
}

postman测试

image-20220829154514651

4. 启动多个微服务

image-20220829155336240

五、工程重构

1. 问题

多个工程有很多相同的部分,比如实体类,大家都要用到,所以为了避免冗余,可以集成到一个项目中

2. 创建新 moudle

image-20220829161634237

3. 改 pom 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- HuTool工具包(一个非常好用的工具包)-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.7</version>
</dependency>
</dependencies>

4. 把entities粘贴过来

image-20220829161807860

5. maven命令 clean,install

点闪电符号跳过测试模块

image-20220829161910496

6. 删除8001、8002两个项目的entities;并在各自pom中导入依赖

maven install 是把自定义的maven项目导入到本地仓库

导入新建的maven项目

1
2
3
4
5
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>

8001和8002的pom都要导入,然后可以删除实体类的文件夹了

启动后,测试——-没有问题

六、 Eureka 服务注册与发现

1、 Eureka基础知识

Eurkea采用C/S的设计架构

避免单点故障,所以使用集群

1
2
3
4
什么是服务治理:
springcloud封装了Netflix公司开发的Eureka模块来实现服务治理

当客户端和服务端过多时,管理每个服务之间的依赖关系比较复杂,需要使用服务治理。可以实现服务调用、负载均衡、容错等

image-20220829170936143

系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接

客户端好比学生,服务端集群好比老师们 Eureka Client,Eureka Server 就是学校;心跳接口就是学校会看老师和学生是否在正常上课 学生也可以作为 Eureka Client 注册到学校,然后找老师们寻求服务

1
“心跳”指的是一段定时发送的自定义信息,让对方知道自己“存活”,以确保连接的有效性。大部分 CS 架构的应用程序都采用了心跳机制,服务端和客户端都可以发心跳。通常情况下是客户端向服务器端发送心跳包,服务端用于判断客户端是否在线。

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

  • Eureka Server:Eureka 服务注册中心,主要用于提供服务注册功能。当微服务启动时,会将自己的服务注册到 Eureka Server。Eureka Server 维护了一个可用服务列表,存储了所有注册到 Eureka Server 的可用服务的信息,这些可用服务可以在 Eureka Server 的管理界面中直观看到。
  • Eureka Client:Eureka 客户端,通常指的是微服务系统中各个微服务,主要用于和 Eureka Server 进行交互。在微服务应用启动后,Eureka Client 会向 Eureka Server 发送心跳(默认周期为 30 秒)。若 Eureka Server 在多个心跳周期内没有接收到某个 Eureka Client 的心跳,Eureka Server 将它从可用服务列表中移除(默认 90 秒)。

2、 单机版Eureka构建

按照图示一点一点构建整个项目

image-20220830095125327

1> IDEA生成 eurekaServer端服务中心

新建moudle工程,改pom,写yml,添加主启动类

eurekaServer端结构:

image-20220830094139380

pom依赖:

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
    <dependencies>
<!-- eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001
eureka:
instance:
hostname: localhost # eureka服务端的实例名称
client:
# false表示不向注册中心注册自己(因为当前项目就是注册中心)
register-with-eureka: false
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索任务
fetch-registry: false
service-url:
# 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
# 即 http://localhost:7001/eureka/
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

主启动类:

1
2
3
4
5
6
7
8
@SpringBootApplication
// 标明到底是eureka的哪个组件
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}

页面测试:http://localhost:7001

image-20220830094539713

2> payment8001注册进eurekaServer

EurekaClient 端cloud-provider-payment8001将注册进 EurekaServer 成为服务提供者 provider,供8002使用

在8002的pom文件中引入新的jar包,eureka-client

1
2
3
4
5
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改yml文件,在原基础上添加 eureka 配置:

1
2
3
4
5
6
7
8
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka/

给主启动类加注释,标名为客户端

image-20220830100617223

启动测试:先启动 eureka-server

image-20220830100725735

localhost:7001

image-20220830100745261

image-20220830100840371

3> consmer8002注册进eurekaServer

EurekaClient 端cloud-consmer-8002将注册进 EurekaServer 成为服务消费者 consumer,调用8001业务

跟上面一样,导 jar 包,改 yml,改启动类

1
2
3
4
5
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

yml:加个名字和配置

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: cloud-order-service

eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka/

给主启动类加注释,@EnableEurekaClient

测试,先启动 EurekaServer7001,再启动服务提供者provider-8001;

localhost:7001

image-20220830111230924

爆红是Eurkea的自我保护机制

postman正常,哦耶

image-20220830111309396

1
2
3
4
# 不想入驻到 eurekaServer 就把yml属性改为false
eureka:
client:
register-with-eureka: false

3、 集群Eureka构建

1> 集群原理

image-20220830145141623

image-20220830150807223

如果是3个的话,就两两互相注册 (有个问题:那要几百个这么一个一个操作得累死,应该有什么便捷方法)

2> 集群环境搭建

新建一个 Eureka Server (moudle)

项目结构:

image-20220830155138463

pom;和 cloud-eureka-server7001 相同;贴过来就行

配置文件,需要改为集群式的配置文件 互相注册

​ 本地的话,集群互相注册需要不同的主机名,所以本地想模拟需要修改 host 文件(因为都是localhost,只是端口不同)

image-20220830155451222

​ 把需要的2个加进去

image-20220830155911198

7001的 application.yml

​ 其他不变,defaultZone路径要变为要注册的新地址

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 7001

eureka:
instance:
hostname: eureka7001.com # eureka服务端的实例名称
client:
# false表示不向注册中心注册自己(因为当前项目就是注册中心)
register-with-eureka: false
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索任务
fetch-registry: false
service-url:
defaultZone: http://eureka7002.com:7002/eureka/

7002的 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7002

eureka:
instance:
hostname: eureka7002.com # eureka服务端的实例名称
client:
# false表示不向注册中心注册自己(因为当前项目就是注册中心)
register-with-eureka: false
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索任务
fetch-registry: false
service-url:
# 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka/

测试

localhost:7001 或 eureka7001.com:7001

image-20220830161104155

localhost:7002 或 eureka7002.com:7002

image-20220830161247935

3> 把微服务注册到 Eureka 集群中

把 8001 和 8002 的 yml 配置修改即可,将注册的地址改为集群版本

1
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版

image-20220830162636677

4> 测试-01

先启动 eureka 集群 7001 和 7002;再启动 8001 ,然后 8002

image-20220830163031671

image-20220830163044516

postman 测试调用业务功能

image-20220830163114476

5> 搭建服务端集群(支付者)

新建 moudle cloud-provider-payment8002 模拟多个服务端集群的效果

image-20220831132018155

跟8001除了端口号其他完全一样,粘过来即可

image-20220830194022234

在controller中添加便于查看端口的

image-20220830194412111

把客户端端口号改一下,和服务端这个冲突了(我改成888了)

  1. 修改服务端的 controller 服务端集群后,客户端调用服务时只需要关注名字;不用在乎到底调用的是哪一个端口

image-20220830194952818

  1. 修改后会出现错误;因为同名下有两个微服务,并不知道该调用哪个。

  2. 在 ApplicationContextConfig 使用 @LoadBalanced

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class ApplicationContextConfig {
    @Bean
    // 使 RestTemplate 具有负载均衡能力
    @LoadBalanced
    public RestTemplate getRestTemplate() {
    return new RestTemplate();
    }
    }

测试:

先启动EurekaServer     7001/7002

再启动服务端     8001/8002

客户端测试    888

image-20220830195245720

image-20220830195318203

不停请求服务,8001和8002会相继出现,说明实现负载均衡

负载均衡默认是轮询机制,所以会依次调用不同服务端

image-20220830195542321

image-20220830195605928

ApplicationContextBean Ribbon的负载均衡功能

Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号,且服务还有负载均衡功能

6> actuator微服务信息完善

对整体并没有影响,但是一些细节的修改

  1. 主机名称:服务名称修改

​ 问题:主机名称直接暴露在外

  1. 访问信息时,希望有IP显示

​ 找业务时,一般是,哪个IP,哪个端口,哪个微服务名称

8001、8002的 yml 文件修改 eureka 下添加配置 8002对照着改相关信息即可

1
2
3
instance:
instance-id: payment8001 # 修改主机名
prefer-ip-address: true # 访问路径可以显示IP地址

全部配置

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
server:
port: 8001

spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/oldchen?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
username: root
password: root

eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
# defaultZone: http://localhost:7001/eureka/ # 单机版
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
instance:
instance-id: payment8001 # 修改主机名
prefer-ip-address: true # 访问路径可以显示IP地址


mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.atguigu.springcloud.entities # 所有entity别名类所在包

image-20220831112138825

注:服务端使用时,web 和 actuator 依赖一般绑定同时使用

image-20220831112437481

actuator 的基本命令

​ 监控端口是否正常运行

image-20220831112451743

7> 服务发现 discovery

用来让对方直观的看到有哪些微服务

8001端为例(8002相同的)

1
2
3
// 注: DiscoveryClient 是 org.springcloud 的 不是 netflix
@Resource
private DiscoveryClient discoveryClient;

添加新的 GetMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 查看微服务信息
@GetMapping(value = "/payment/discovery")
public Object discovery() {
// 获取在eureka中注册好的服务有哪些
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("*****element: " + element);
}

// 一个微服务下面的全部具体实例(某个名称下的)
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}

全部controller

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
@RestController
@Slf4j
public class PaymentController {

@Resource
private PaymentService paymentService;

@Value("${server.port}")
private String serverPort;

@Resource
private DiscoveryClient discoveryClient;

// 写操作 post
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment) {
// int 是因为有 useGeneratedKeys 成功返回1,失败返回0
int result = paymentService.create(payment);
log.info("***插入结果:"+result);

if (result > 0) {
return new CommonResult(200,"插入成功,serverPort "+serverPort,result);
} else {
return new CommonResult(444,"插入失败",null);
}
}

// 读操作 get
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("***插入结果:"+payment);

if (payment != null) {
return new CommonResult(200,"查询成功,serverPort "+serverPort,payment);
} else {
return new CommonResult(444,"没有对应记录,查询ID:"+id,null);
}
}

// 查看微服务信息
@GetMapping(value = "/payment/discovery")
public Object discovery() {
// 获取在eureka中注册好的服务有哪些
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("*****element: " + element);
}

// 一个微服务下面的全部具体实例(某个名称下的)
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}
}

主启动类下添加 @EnableDiscoveryClient (以后会常用)

image-20220831135657289

image-20220831140310971

image-20220831140422862

在 客户端 888 添加该接口后,以后可以直接知道有哪些服务;这样 服务端 也只对外展示一个整体的信息

8> Eureka 自我保护机制

概述:保护模式主要用于一组客户端和Eureka Server 之间存在网络分区场景下的保护

进入保护模式后,Eureka Server 会尝试保护注册表中的内容,不再删除服务注册表中的内容,就是不会注销任何微服务

看到这段提示,既是进入保护模式

image-20220831141538329

一句话概括:某时刻某一微服务不能用了,Eureka 不会立刻清理,依旧会对该微服务的信息进行保存

属于CAP里面的AP分支

为什么会产生Eureka自我保护机制

​ 防止当EurekaClient正常运行,但 EurekaServer 网络不通的情况下,EurekaServe不会立即将EurekaClient服务删除刷

什么是自我保护模式

​ 默认情况下,如果 EurekaServer 在一定时间内没有收到么某个微服务实例的心跳,EurekaServer 将会注销该实例(默认90秒)。但当网络分区发生故障(延时、卡顿、拥挤)时,微服务与 EurekaServer 之间无法正常通信,以上行为就变得很危险——–因为微服务本身是正常的,此时不应该被注销掉。Eureka通过”自我保护模式”来解决这个问题——当EurekaServer 节点短时间丢失过多客户端时(可能发生网络分区故障),那么这个节点会进入自我保护模式

它的设计哲学是宁可保留错误的注册信息,也不盲目注销任何可能健康的服务实例 好死不如赖活

综上:自我保护是一种应对网络异常的保护措施

9> 关闭自我保护机制

7001端—–EurekaServer端

yml 修改配置 eureka.server.enable-self-preservation 默认为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
eureka:
instance:
hostname: eureka7001.com # eureka服务端的实例名称
client:
# false表示不向注册中心注册自己(因为当前项目就是注册中心)
register-with-eureka: false
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索任务
fetch-registry: false
service-url:
# 集群只想其他 eureka
defaultZone: http://eureka7002.com:7002/eureka/
# 单机就是自己
#defaultZone: http://eureka7001.com:7001/eureka/


server:
# 关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
# 等待时间
eviction-interval-timer-in-ms: 2000

8001端—–EurekaClient端

修改 8001 yml

1
2
3
4
# Eureka 客户端向服务端发送心跳的时间间隔,单位为秒(默认30s)
lease-renewal-interval-in-seconds: 1
# Eureka 服务端收到最后一次心跳后等待的时间上限,单位为秒(默认90s),超时将踢除服务
lease-expiration-duration-in-seconds: 2

image-20220831150808413

七、 Zookeeper注册与发现

SpringCloud整合Zookeeper代替Eureka

机构和 Eureka 是完全相同的

1. 注册中心zookeeper

在虚拟机,centos上安装zookeeper;关闭防火墙

查下zookeeper的 ip 地址,保证可以和 win 系统进行 ping 通

2. 服务提供者

建新的 moudle cloud-provider-payment8004

pomimage-20220902090547076

yml

1
2
3
4
5
6
7
8
9
server:
port: 8001

spring:
application:
name: cloude-provider-payment
cloud:
zookeeper:
connect-string: 192.168.111.144:2181 # 一个zookeeper的地址,如果是集群,就直接逗号然后加

main 函数

1
2
@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向使用 consul 或 zookeeper 作为注册中心时注册服务

controller 可以不带日志的注释,后头可能会报错

image-20220902092304620

1
UUID.randomUUID().toString();   // 随机生成的流水号

启动zookeeper服务,再启动该微服务

启动会有bug出现,原因:8成是环境问题;maven 导入的 jar 包和虚拟机的 zookeeper 版本不一样

image-20220902093934658

image-20220902094214985

​ zookeeper 作为注册中心,微服务是临时节点。当微服务停止发送心跳后,在一定的等待时间后,注册中心会把微服务踢掉

3. 服务消费者

和提供者差不多;跟Eureka逻辑都一样

八、 Consul注册

1. Consul简介

Consul 是一套开源的分布式服务发现和配置管理系统,由Go语言开发

​ 提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据单独需要使用,也可以一起使用以构建全方位的服务网格。总之 Consul 是一套完整的服务网格解决方案

​ 优点:基于 raft 协议,比较简介;支持健康检查,同时支持 HTTP 和 DNS 协议;支持跨数据中心的 WAN 集群;提供图形界面;跨平台。支持 Linux、Mac、Windows

怎么用:Spring Cloud Consul 中文文档 参考手册 中文版

2. Consul 下载和安装

官网下载,解压后直接就是一个exe文件

我把他放在 D 盘了image-20220902155947402

运行 cmd image-20220902160017193

image-20220902160105554

1
consul agent -dev        启动服务

image-20220902160215632

localhost:8500 访问图形化界面

image-20220902160341024

3. 服务提供者注册进consul

熟悉的5步走

新的工程名:cloud-providerconsul-payment8006

image-20220905091222052

pom 依赖:

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
<dependencies>
<!-- SpringCloud consul-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8006

spring:
application:
name: consul-provider-payment
## consul 注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
# hostname 127.0.0.1
service-name: ${spring.application.name}

主启动类:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class,args);
}
}

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;

// 接口用来测试微服务(用随机数生成流水号来测试)
// RequestMapping包含各种请求头,可以指定
// GetMapping =》 RequestMapping(method=RequestMethod.GET)
@RequestMapping("/payment/consul")
public String paymentConsul(){
return "springcloud with consul: " + serverPort + "\t" + UUID.randomUUID().toString();
}
}

测试: 启动微服务和consul

image-20220905091747093

点进去可以看到它的安全状态

image-20220905091802113

image-20220905091848320

4. 服务消费者注册进consul

建moudle、改pom、写yml、主启动、写config、业务层

cloud-consumerconsul-order889

image-20220905104006516

pom:(和服务提供者是一样的)

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
<dependencies>
<!-- SpringCloud consul-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:(就换了一个名字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 889

spring:
application:
name: consul-consumer-order
## consul 注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
# hostname 127.0.0.1
service-name: ${spring.application.name}

config:

1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationContextConfig {

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

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class OrderConsulController {
public static final String INVOKE_URL = "http://ApplicationContextConfig";

@Resource
private RestTemplate restTemplate;

@GetMapping("/consumer/payment/consul/")
public String paymentInfo() {
// RestTemplate 实现服务端向外网发送请求的场景 .get...即使用get方法请求
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
return result;
}
}
image-20220905104416423 image-20220905104724771

九、 三种注册中心的异同点

组件名 语言 CAP 服务健康检查 对外暴露接口 SpringCloud集成
Eureka Java AP 可配支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper Java CP 支持 客户端 已集成

A:高可用

C:数据一致

P:分区容错性

CAP理论:关注粒度是数据,而不是整体系统设计的策略

最多只能较好的满足两个(实际情况好像要看网络情况来判断)

CA:单点集群,满足一致性,高可用系统,可扩展性上不太强大

CP:满足一致性,分区容灾性的系统,一般性能不是太高

AP:满足一致性,分区容灾性的系统,对一致性要求比较低

十、Ribbon负载均衡服务调用

1. 概述

Ribbon是一套客户端,实现负载均衡的工具

​ 主要功能是提供负载均衡算法和服务调用;Ribbon会自动帮助你基于某种规则(简单的轮询或随机连接等)连接机器。也可以使用Ribbon实现自定义的负载均衡算法

​ Ribbon目前进入维护状态,但依然大规模部署(目前依然主流和成熟) 未来–> springcloud loadbalancer

LB(负载均衡): 集中式LB和进程内LB

LB负载均衡(Load Balance)是什么

​ 是将用户请求平摊分到多个服务响应端,来实现系统高可用的目的(HV)

​ 常见的负载均衡软件有:Nginx、LVS(Linux虚拟服务器)、硬件F5等

Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别

​ Nginx是服务器负载均衡(集中式LB),客户端所有的请求都发送给Nginx;然后由Nginx实现转发请求。即负载均衡由服务器实现

​ Ribbon本地负载均衡(进程内LB),在调用微服务接口时,会在注册中心获取服务列表并缓存到JVM本地;在本地实现RPC远程调用技术

集中式LB:在服务的消费方和提供方之间使用的独立的LB设施(硬件如:F5;软件如:Nginx)。该设施通过某种策略把请求转发到服务提供方(类似于一个医院,进去之后,去哪由医院来分配)

进程式LB:将LB逻辑集成到消费方,消费方从注册中心得知哪些地址可用,自己挑选一个可用服务器(类似于一个科室,大夫是现成的,进去自己挑)

​ Ribbon属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它获取服务提供方地址

Ribbon:负载均衡 + RestTemplate调用

2. 怎么用

架构:Ribbon是一个软负载均衡客户端组件(软负载均衡即软件负载均衡)

可以和其他所需请求客户端结合使用,和eureka结合只是一个实例

1
2
3
4
5
<!--引入该依赖时,会自带ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

image-20220906165435093

RestTemplate的使用

常用: getForObject/getForEntity postForObject/postForEntity

Object:返回对象为响应体中数据转换的对象,基本上可以理解为json

Entity:返回对象为ResponseEntity对象,包含了响应中的一些重要信息;响应头、响应状态码、响应体(想知道详细信息就用entity,调用get方法可获得详细的信息)

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
@RestController
@Slf4j
public class OrderController {
// public static final String PAYMENT_URL = "http://localhost:8001"; // 单机版(写死了)

// 变为集群时,不关注某一端口,只通过hostname去寻找
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

@Resource
private RestTemplate restTemplate;

@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment) {
// Object 和 Entity 方法
//return restTemplate.postForObject(PAYMENT_URL + "/payment/create",payment,CommonResult.class);
return restTemplate.postForEntity(PAYMENT_URL + "/payment/create",payment,CommonResult.class).getBody();
}

@GetMapping("/consumer/payment/get/{id}")
public CommonResult getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}

@GetMapping("/consumer/payment/getEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult<>(444,"操作失败");
}
}
}

测试结果,两者是一样的;只是Entity可以返回单个详细的部分

image-20220906190830406

3. Ribbon 默认的负载规则

默认规则是:轮询

IRule接口:

image-20220906191202261

该接口的实现类:

image-20220906191906784

image-20220906191630967

自带的常见的用法:

image-20220906191716372

4. 替换Ribbon的规则

这个自定义配置类不能放在@ComponentScan所扫描的子包下面,否则达不到特殊化的目的

​ 即 @SpringBootApplication 注释中带有的 @ComponentScan 会扫描本包下的所有配置;本例中就是在com.atguigu.springcloud 之外建包

image-20220906192926893

换一种替换规则需要自己指定

1
2
3
4
5
6
7
@Configuration
public class MySelfRule {
@Bean
public IRule myRule() {
return new RandomRule(); // 定义为随机
}
}

在主启动中配置,当启动时,Ribbon执行自定义的规则

1
2
// name:找这个服务名的服务;  configuration是使用该配置类
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
image-20220906194520853

5. 轮询规则的原理

负载均衡算法: rest接口第几次发起请求 % 服务器集群总数 = 实际调用服务器的位置下标

当服务重启时,计数从1算起

1
2
3
4
5
6
7
8
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
如:List[0] instances = 127.0.0.1:8002
List[1] instances= 127.0.0.1:8001
80028001 组成集群,按照轮询的算法:
1 % 2 = 1 --》 index = 1 list.get(index); 获得服务地址就是 127.0.0.1:8001
2 % 2 = 0 --》 index = 0 list.get(index); 获得服务地址就是 127.0.0.1:8002
3 % 2 = 1 --》 index = 1 list.get(index); 获得服务地址就是 127.0.0.1:8001
...

源码-查看:

image-20220906202010221

6. 手写轮询算法

启动7001,7002 eureka 集群

给8001,8002 添加一个测试接口;启动

1
2
3
4
@GetMapping("/payment/lb")
public String getPaymentLB() {
return serverPort;
}

888 消费者改造

  1. 把 ApplicationContextConfig 里的 @LoadBalanced 注释掉;(因为接下来使用自己写的轮询规则)
  2. 新建接口类,以及实现类
image-20220907153019499

接口类

1
2
3
4
public interface LoadBalancer {
// 得到机器的列表(有哪些微服务可以提供)
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

实现类

源码中也使用了自旋锁,例如:for( ; ; ) 内部有自己跳出循环的条件

​ 这里自定义的方法使用 do-while 实现自旋锁

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
@Component
public class MyLB implements LoadBalancer {

// 提供一个原子类,初始值为0
// 原子类可在高并发场景下高效处理程序;并保证线程安全(Java语言中,++i和i++操作并不是线程安全的)
private AtomicInteger atomicInteger = new AtomicInteger(0);

// 得到并增加
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1; // 2147483647:int的最大值
}while (!this.atomicInteger.compareAndSet(current,next)); // 自旋,一直取到想要的值为之
System.out.println("******第几次访问,next: "+next);
return next;
}

@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
// getAndIncrement 代表第几次访问
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
  1. OrderController 新增接口

@Resource 和 @Autowired

​ @Autowired:属于Spring,按类型匹配,byType

缺点:当实现类有两个以上时,容易有冲突;要使用它注入要确保只有一个实现类

​ @Resource:属于J2EE,按名称匹配,byName。更准确一些,减少了与spring的耦合。查名字的速度也要更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Resource
private LoadBalancer loadBalancer;

@Resource
private DiscoveryClient discoveryClient;

@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {
// 通过名字得到的 List,就是 LoadBalancer 接口中需要的List
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() <= 0) {
return null;
}

ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();

return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
  1. 测试
image-20220907153454719

十一、OpenFeign的服务调用

用在客户端,使用方法是定义一个服务接口,然后在上面添加注解。

Feign可以和Eureka和Ribbon组合使用来实现负载均衡

Feign干什么:(服务接口绑定器)

​ 旨在使编写Java Http客户端变得容易,实际情况中,一个接口会被多次调用,所以需要对每个微服务都封装一些客户端调用。在Feign的实现下,我们只需要创建一些接口,使用接口调接口的方式,实现服务方的接口绑定

​ 看微服务的service中使用了哪些服务,那客户端就创建对应的接口

​ 通过Feign,只需要定义服务绑定接口并声明方法,就可实现服务调用

Feign已经被OpenFeign代替

1. 使用步骤

建新的工程 cloud-consumer-feign-order890

image-20220910104355945

OpenFeign对Ribbon进行了集成,依赖中包含负载均衡内容

image-20220910104913398

OpenFeign是Spring官方推出的一种声明式服务调用与负载均衡组件

核心:微服务调用接口 + @FeignClient

pom.xml 新内容只多了一个openfeign的依赖

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
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
server:
port: 890

eureka:
client:
# 因为当前是消费者,所以不需要注册进注册中心
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
1
2
主启动:  @EnableFeignClients    (激活)
业务类: @FeignClient (启动)

主启动:

1
2
3
4
5
6
7
8
@SpringBootApplication
// 使用 Feign 激活并开启
@EnableFeignClients
public class OrderFeignMain890 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain890.class,args);
}
}

业务类:

Service:使用8001暴露的接口,有什么就调什么

1
2
3
4
5
6
7
8
// 添加为容器内的一个组件
@Component
// 实现负载均衡和服务调用-----value:服务提供者提供的服务名称
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping("/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class OrderFeignController {

@Resource
private PaymentFeignService paymentFeignService;

@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}

测试:

先启动7001、7002;在启动8001、8002

启动890测试,服务调用能力和负载均衡功能都正常

image-20220910105142422

总结:面向接口编程

image-20220910105758746

2. OpenFeign 超时控制

​ 会出现一种情况,消费者需要调用服务提供者服务;服务方完成这次服务需要 3s,但消费者只能等待 2s;那超时的话,消费者方就会直接报错

OpenFeign 客户端默认等待 1s

1> 服务提供方8001故意写一个暂停程序接口

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout() {
// 程序进来后停3s后返回
// 暂停几秒钟线程,模拟假如服务提供端的是一个长业务需要一段时间处理
try{
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}

2> 80 添加超时方法

service:

1
2
 @GetMapping("/payment/feign/timeout")
String paymentFeignTimeout();

controller:

1
2
3
4
5
@GetMapping("/consumer/payment/feign/timeout")
public String paymentFeignTimeout() {
// openfeign 底层 ribbon;默认等待 1s
return paymentFeignService.paymentFeignTimeout();
}

3> 错误页面

image-20220910212843581

4> yml中开启配置

1
2
3
4
5
6
# 设置 feign 客户端超时时间(OpenFile默认支持ribbon)
ribbon:
#处理请求的超时时间,默认为5秒
ReadTimeout: 5000
#连接建立的超时时长,默认5秒
ConnectTimeout: 5000
image-20220910213033768

3. OpenFeign 日志打印功能

我们可以通过调整日志级别,从而了解 Feign 中的 Http 请求细节。

对Feign接口的调用情况进行监控和输出

1
2
3
4
5
日志级别:
NONE: 默认的,不显示日志
BASIC:仅记录请求方法、URL、响应状态码和执行时间
HEADERS:除了 BASIC 中定义的信息外,还有请求和响应的头信息
FULL:除了 HEADERS 中定义的信息外,还有请求响应的正文和元数据

config类:
image-20220910214538911

1
2
3
4
5
6
7
8
9
10
11
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

yml:

1
2
3
4
logging:
level:
# feign 日志以什么级别监控哪个接口 (监控service的服务接口)
com.atguigu.springcloud.Service.PaymentFeignService: debug

测试:调用某一接口后,查看打印的日志信息

image-20220910214907841

十二、 Hystrix 服务熔断器

分布式系统面临的问题:

​ 复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候不可避免的的会失败

​ 服务雪崩:多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或不可用,对微服务A就会占用更多资源,进而引起系统崩溃,所谓“雪崩效应”

​ 对于高流量的应用来说,单一的后端依赖可能导致所有服务器上的所有资源在几秒内饱和。

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖不可避免的会出现调用失败,比如超时、异常。Hystrix能够保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障

​ “断路器”是一种开关,当某个单元发生故障后,向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间等待或抛出调用方无法处理的异常。

Hystrix的主要功能:服务降级、服务熔断、接近实时的监控…

官方宣布,停更进行维护

1. Hystrix重要概念

1
2
3
4
5
6
7
fallback--服务降级;服务器忙,请稍后再试;不让客户端等待,并返回一个友好提示
哪些情况后触发降级:1.程序运行异常 2.超时 3.服务熔断触发服务降级 4.线程池/信号量打满也会导致服务降级

break--服务熔断;类比保险丝,当达到最大访问服务时,直接拒绝访问,拉闸限电,并调用服务降级返回友好提示
服务降级->熔断->恢复链路

flowlimit--秒杀高并发等操作,严禁一窝蜂的拥挤,大家排队,一秒几个

2. 构建

先把 Eureka7001改回单机版,方便调试

新工程:cloud-provider-hystrix-payment8001

image-20220911094344427

pom:引入hystrix的jar包

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
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment

eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
# 没开集群
defaultZone: http://eureka7001.com:7001/eureka/

主启动

1
2
3
4
5
6
7
8
@SpringBootApplication
// 注册进 eureka
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}

service和controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
应该是建一个接口类,在再impl实现的,这里为了省事
*/
@Service
public class PaymentService {

// 正常访问,一切ok
public String payment_OK(Integer id) {
return "线程池: "+Thread.currentThread().getName()+" payment_OK,id: "+id+"\t"+"hhhhh";
}

// 模拟超时;假装它是个复杂的业务流程
public String payment_Timeout(Integer id) {
// 暂停几秒,为了触发服务降级
int timeNumber = 3;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: "+Thread.currentThread().getName()+" payment_Timeout,id: "+id+"\t"+"错了错了,耗时:"+timeNumber;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;

@Value("${server.port}")
private String serverPort;

@GetMapping("/payment/hystrix/ok/{id}")
public String payment_OK(@PathVariable("id") Integer id) {
String result = paymentService.payment_OK(id);
log.info("****result: "+result);
return result;
}

@GetMapping("/payment/hystrix/timeout/{id}")
public String payment_Timeout(@PathVariable("id") Integer id) {
String result = paymentService.payment_Timeout(id);
log.info("****result: "+result);
return result;
}
}

测试:启动7001,再8001;两个接口均正常,timeout会延迟3s后返回;模拟一个复杂进程需要的处理时间

image-20220911094813062 image-20220911094755040

以上述Moudle为根基平台,正确->错误->降级熔断->恢复

3. 高并发测试

​ Jmeter工具–性能测试,20000个线程同时运行,去访问接口,会出现卡顿现象;因为tomcat默认的工作线程数被打满了,没有多余的线程缓解压力和处理(tomcat最大活跃线程数200)

​ 结论:这还只是服务提供者8001自己测试,假如外部消费者也来调用服务,那消费者只能干等,最终导致消费端不满意,服务端被拖死

4. 新建消费端加入

hystrix一般用在消费端做降级(服务端也能用)

建新的消费端:cloud-consumer-feign-hystrix-order891

image-20220911111517704

pom:

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
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
server:
port: 891

eureka:
client:
register-with-eureka: false
service-url:
# 没开集群
defaultZone: http://eureka7001.com:7001/eureka/

主启动:

1
2
3
4
5
6
7
8
@SpringBootApplication
// 激活feign
@EnableFeignClients
public class OrderHystrixMain891 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain891.class,args);
}
}

service接口:

1
2
3
4
5
6
7
8
9
10
@Component // 把该类注入进spring容器中(注入bean)
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") // 服务提供方8001
public interface PaymentHystrixService {

@GetMapping("/payment/hystrix/ok/{id}")
public String payment_OK(@PathVariable("id") Integer id);

@GetMapping("/payment/hystrix/timeout/{id}")
public String payment_Timeout(@PathVariable("id") Integer id);
}

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class OrderHystrixController {

@Resource
private PaymentHystrixService paymentHystrixService;

@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String payment_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.payment_OK(id);
}

@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String payment_Timeout(@PathVariable("id") Integer id) {
return paymentHystrixService.payment_Timeout(id);
}
}

测试:

image-20220911111900321

但是用Jmeter将2W个线程压8001,消费端891要么转圈圈等待,要么返回超时报错

故障现象和导致原因:

​ 8001同一层次的其他接口被困死,因为tomcat线程池里的工作线程已经被挤占,如果891再去访问8001就,客户端就容易出现卡顿。所以出现了降级、容错、限流等技术的诞生

1
2
3
4
5
6
解决问题:
超时导致服务器变慢(转圈)----超时不再等待
出错(宕机或程序运行出错)----出错要有兜底,有一个最终显示

服务(8001)超时或者宕机了,调用者不能卡死一直等待,必须有服务降级
服务(8001)业务ok,但是调用者自己出现故障或有自我要求(自己等待的时间小于服务提供者),自己处理j

5. 服务降级

服务降级一般在消费端

8001 fallback

修改8001:设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,做降级处理fallback

业务类启用:@HystrixCommand

主启动类激活:@EnableCircuitBreaker

当前服务不可用了,就做服务降级

修改8001的 PaymentService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 服务降级注解
// 调用方法的峰值是3s,3s以内就是正常的业务逻辑payment_Timeout;超过3s了就报错,执行兜底的业务逻辑 fallback制定的类: payment_TimeoutHandler
@HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String payment_Timeout(Integer id) {
// 暂停几秒,为了触发服务降级
int timeNumber = 5;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: "+Thread.currentThread().getName()+" payment_Timeout,id: "+id+"\t"+"哈哈^_^,耗时:"+timeNumber;
}
// payment_Timeout 服务出现问题后,找该方法
public String payment_TimeoutHandler(Integer id) {
return "线程池: "+Thread.currentThread().getName()+" 系统繁忙,请稍后再试,id: "+id+"\t"+"oT_To";
}

8001主启动类添加注解:@EnableCircuitBreaker

image-20220913093731894

测试:设置程序运行峰值为3s,但程序需要5s,应该返回兜底逻辑部分

image-20220913094043989

image-20220913094720084

890 fallback

yml:添加配置

1
2
3
feign:
hystrix:
enabled: true

主启动类添加注解:@EnableHystrix

修改890 controller:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeoutFallbackMethod",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String payment_Timeout(@PathVariable("id") Integer id) {
return paymentHystrixService.payment_Timeout(id);
}
public String paymentTimeoutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙,请10秒钟后再试或者检查自己运行程序,oT_To";
}

修改Hystrix属性的话,建议重启,而不是热部署

测试:服务端最多等待3s,但是消费端最多只能等待1.5s,所以消费端直接 fallback

image-20220913101004935

存在问题

  1. 每个业务方法都对应一个兜底方法,导致代码膨胀

统一和自定义的方法分开

解决办法:设置一个全局服务降级配置

image-20220913102718120

测试:服务降级会走全局配置

image-20220913102823561

  1. 降级和业务逻辑混合在一起,耦合性太高,混乱

每个降级处理的业务逻辑都和主要业务混合在一块,非常混乱

服务降级,客户端去调用服务端,碰上服务端宕机或关闭

​ 该案例服务降级处理是在客户端实现,与服务端没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可时间解耦

未来我们要面对的异常:运行、超时、宕机

所有的服务是从PaymentHystrixService接口中调用,新建一个类实现该接口并做服务降级

image-20220913135545890

yml

1
2
3
feign:
hystrix:
enabled: true
1
2
3
4
5
6
7
8
9
10
11
12
13
// 组件的注解,否则springboot容器扫描不到
@Component
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String payment_OK(Integer id) {
return "-----PaymentFallbackService fall back-payment_OK, ·T_T·";
}

@Override
public String payment_Timeout(Integer id) {
return "-----PaymentFallbackService fall back-payment_Timeout, ·T_T·";
}
}

image-20220913135707530

测试:启动7001,8001,891

正常调用:image-20220913140036839

关闭8001模仿宕机:image-20220913135850157

此时服务端已经宕机,但我们做了降级处理,让客户端在服务端不可用也会获得提示,不会挂起耗死服务器

6. 服务熔断

类似保险丝

论文:CircuitBreaker (martinfowler.com)

熔断机制概述:

​ 熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或响应时间过长时,会进行服务降级,进而熔断该节点的微服务调用,快速返回错误的响应信息。当检测到该节点的微服务调用响应正常后,恢复调用链路(一种特殊的服务降级吧)

​ SpringCloud框架中,熔断机制通过Hystrix实现。当失败的调用到一定阈值时,缺省是5秒内20次调用失败,就会启动熔断机制。注解是 @HystrixCommand

修改8001 PaymentService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ======服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), //失败率达到多少跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
// 故意传一个id为负数,作为运行时异常
if (id < 0) {
throw new RuntimeException("******id 不能为负数");
}
// hutool工具类的生成唯一识别码的工具
String serialNumber = IdUtil.simpleUUID(); // 等同于 UUID.randomUUID().toString()
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ToT)/~~ id: "+id;
}

修改controller:

1
2
3
4
5
6
7
// ======服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
log.info("*****result:" + result);
return result;
}

测试:

id为正数:image-20220913151240191

id为负数:image-20220913151302799

多次提交负数后,达到熔断的条件image-20220913151506352

再当id为正数时,返回依旧是服务降级页面,链路还未恢复:image-20220913151626374

等一会链路就会自动恢复:image-20220913151649816

总结:

image-20220913153223459

熔断类型:

  1. 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
  2. 熔断关闭:熔断关闭不会对服务进行熔断
  3. 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断

断路器开启或关闭的条件:

  1. 当满足一定的阀值时(默认是10s 内超过20个请求次数)
  2. 当失败率达到一定的时候(默认10s 内超过50%的请求失败)
  3. 当到达以上阀值,断路器将会开启
  4. 当开启的时候,所有请求都不会转发
  5. 一段时间后(默认是5s),这时断路器是半开状态,会让其中一个请求进行转发。(探子)如果成功,断路器会关闭,若失败,继续开启。重复4和5
1
断路器打开后,再有请求调用时,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现自动地发现错误并将降级逻辑切换为主逻辑,减少应用响应时间

7. Hystrix Dashboard

图形化监控

新建监控平台 moudle:cloud-consumer-hystrix-dashboard9001

pom依赖:

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
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml:只用配置一个9001端口

主启动:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}

访问:http://localhost:9001/hystrix

image-20220913162245171

注:要被监控的话,所有的微服务提供者必须有actuator依赖

修改8001的主启动类:主要是添加下头的配置

新版本 Hystrix 要在主启动类中指定监控路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootApplication
// 注册进 eureka
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}

/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream"
* 只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}

测试:打开9001、7001、8001

image-20220913164018159

访问:http://localhost:8001/payment/circuit/33http://localhost:8001/payment/circuit/-33

多次的请求成功:image-20220913164136533

多次的请求失败:断路器打开 image-20220913164218779

怎么看:

  1. 7色image-20220913164632728

  2. 1圈:通过颜色变化代表实力的健康程度,绿色<黄色<橙色<红色。流量越大,实心圆就越大,以此可以在大量实例中快速发现故障实例和高压力实例image-20220913164942675

  3. 1线 image-20220913165012084

  4. 整图说明image-20220913165053249

十三、服务网关 gateway

(技术快速迭代,构建体系,配置变多了。编码很重要,但得知道是干嘛的)

zuul服务网关和gateway新一代服务网关;zuul不再被维护,所以主要使用gateway

谈谈对微服务网关的了解?

​ 微服务网关是整个微服务API请求的入口,可以实现过滤Api接口。
​ 作用:可以实现用户的验证登录、解决跨域、日志拦截、权限控制、限流、熔断、负载均衡、黑名单与白名单机制等。

过滤器和网关的区别:过滤器适合单个服务实现过滤请求。网关拦截整个的微服务实现过滤请求,能够解决整个微服务中冗余代码。过滤器是局部拦截,网关实现全局拦截

1
2
3
4
网关使用的是哪一个,为什么要用 gateway?比 zuul 好在哪?
Gateway,该项目借助Spring WebFlux的能力,打造了一个API网关。旨在提供一种简单而有效的方法来作为API服务的路由,并为它们提供各种增强功能,例如:安全性,监控和可伸缩性。
Zuul网关属于netflix公司开源产品,属于第一代微服务网关;Gateway属于SpringCloud自研发的网关框架,属于第二代微服务网关。
Zuul网关底层基于Servlet实现,阻塞式Api,不支持长连接。SpringCloudGateway基于Spring5构建,能够实现响应式非阻塞式的Api,支持长连接,能够更好的整合Spring体系的产品,依赖SpringBooot-WebFux

​ Cloud全家桶中有个很重要的组件就是网关,在1.x版本中采用的都是Zuul网关,2.x版本中,zuul的升级一直跟不上,SpringCloud自己研发了一个网关替代Zuulgateway是原zuul1.x版的替代

image-20220914092459847

​ SpringCloud Gateway 作为SpringCloud生态系统中的网关,目标是代替 Zuul,在SpringCloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本集成,仍然使用Zuul 1.x非Reactor模式的老版本。而为了提升网关性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty

​ SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架

1
2
微服务网关能干嘛?
反向代理、鉴权、流量控制、熔断、日志监控
image-20220914094157504
1
2
3
4
5
6
7
8
9
SpringCloud Gateway具有如下特性:
1.基于Spring Framework 5,Project Reactor 和SpringBoot 2.0 进行创建
2.动态路由:能够匹配任何请求属性。(重要)
3.可以对路由指定 Predicate(断言)和 Filter(过滤器)。(重要)
4.集成Hystrix的断路器功能
5.集成 SpringCloud 服务发现功能
6.易于编写 Predicate 和 Filter
7.请求限流功能
8.支持路径重写

Zuul1.x模型:

SpringCloud中所集成的Zuul版本,采用的式Tomcat容器,使用的是传统的Servlet IO处理模型

Servlet的生命周期?servlet由servlet container进行生命周期管理

1
2
3
container启动时创造servlet对象并调用servlet init()进行初始化;
container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service();
container关闭时调用servlet destory()销毁servlet;

image-20220914102528926

上述模式的缺点:

​ servlet是一个简单的网络IO模型,当请求进入servlet container容器中时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的(进来一个请求,就绑定一个线程)。一旦是高并发场景下,线程数量就会上涨,而线程资源代价是昂贵的,严重影响请求的处理时间。在业务场景下,希望只用1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势

​ 所以Zuul 1.x是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet并由该servlet阻塞式处理。

Gateway模型:

​ 传统的web框架,如:struts2,springmvc等都是基于Servlet API与Servlet容器基础上运行的

Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,核心是基于Reactor的相关API实现的。相对于传统web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程

现在玩的都是响应式异步非阻塞模型

响应式布局:一个网站能够兼容多个终端。面对不同分辨率设备灵活性强,能够快捷解决多设备显示适应问题。

异步:多个任务执行或发生,可以并发地执行。(同步:多个任务执行或发生,必须逐个地进行执行)

阻塞:当某个事件或任务执行过程中,发出一个请求,但由于请求操作需要的条件不满足,会一直等待,直至条件满足

非阻塞:当某个事件或任务执行过程中,发出一个请求,如果请求操作条件不满足,会立即返回一个标志信息(如:暂时无法访问,诸如此类)

1. gateway三大核心概念

Router(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由(符合路由的匹配、转发规则)

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

Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或之后对请求进行修改(断言过来后,还可以设置一些过滤条件)

image-20220914111750199

Gateway工作流程

image-20220914112001175

客户端向 SpringCloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。

Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行逻辑中,然后返回。

过滤器间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑

Filter在”pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等

在”post”类型的过滤器可以做响应内容、响应头的修改,日志的输出,流量监控等

核心逻辑:路由转发+执行过滤器链

2. 基本配置

新建网关工程:cloud-gateway-gateway9527

image-20220914141745044

pom:没有 web和actuator 依赖

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
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

gateway 依赖中集成了 webflux 和 reactor-netty (非阻塞式响应式编程的高性能框架)

image-20220914142144565

yml:网关也要注册到注册中心(Eureka、Consul、Zookeeper都行)

​ 实现路由映射:cloud-provider-payment8001 controller的两个接口,get和lb

​ 不想暴漏8001,希望在8001外头套一层9527

image-20220914142455266

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
server:
port: 9527

spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates: # 能找到-----为true,就访问
- Path=/payment/get/** #断言,路径相匹配的进行路由

- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由

eureka:
instance:
hostname: cloud-gateway-service
client: # 服务提供者provider注册进eureka服务列表内
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
image-20220914142622963

主启动:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class,args);
}
}

测试:

启动7001、8001、9527

两个微服务均被注册到Eurka;自测8001接口没问题,把接口改为9527

image-20220914142825008

image-20220914142843243

断言规则:

image-20220914143713912

3. gateway配置动态路由

存在的问题:yml配置中路径是写死的,我们需要实现负载均衡image-20220914152205782

通过微服务名实现动态路由

​ 默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

启动:一个Eureka7001 + 两个服务提供 8001/8002

pom:spring-cloud-starter-netflix-eureka-client

yml配置:uri:统一资源标识符

1
2
3
需要注意的是 uri 的协议为lb,表示启用Gateway的负载均衡功能

lb://serviceName是spring cloud gateway 在微服务中自动为我们创建的负载均衡 uri
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
server:
port: 9527

spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由

- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由

eureka:
instance:
hostname: cloud-gateway-service
client: # 服务提供者provider注册进eureka服务列表内
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/

通过服务名去查找

image-20220914153858596

测试:发现两个接口在不停的互换

image-20220914153957448image-20220914154014752

4. Predicate的使用

启动时会自动扫描配置;即 yml 中配置的 predicates

image-20220914182556731

image-20220914183425573

image-20220914191618007

1
2
3
4
5
6
7
8
predicates:
- After=2022-09-14T18:29:48.268+08:00[Asia/Shanghai] #在这个时间后才能访问(时区是上海时间)
- Cookie=username,yyds # 需要携带Cookie才能访问
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Host=**.atguigu.com
- Method=GET # 必须是get请求
- Path=/payment/lb/** #断言,路径相匹配的进行路由
- Query=username, \d+ # 要有参数名username并且值是正整数才能路由
  1. After Route Predicate

  2. Before Route Predicate

  3. Between Route Predicate

    Before 、Between 和After同理

  4. Cookie Route Predicate :带Cookie和不带Cookie

    需要两个参数,一个是Cookie name,一个是正则表达式。

    路由规则会通过获取对应的Cookie name 值和正则表达式去匹配,如果匹配上就执行路由,没匹配上就不执行

1
测试工具一般有:Jmeter、Postman(图形界面)、curl(看成是postman的底层命令)

使用 curl 测试,cmd中;默认发送的是 Get请求

发送post请求: curl -X POST “http://…”

不带Cookie:curl http://localhost:9527/payment/lb

image-20220914185803489

带Cookie:curl http://localhost:9527/payment/lb –cookie “username=yyds”

image-20220914190132655

  1. Header Route Predicate: 一个属性名称和一个正 表达式,属性值和正则表达式匹配则执行

    测试:请求头包含的是正整数,即可正确匹配

    curl http://localhost:9527/payment/lb -H “X-Request-Id:123”

    image-20220914192656106

  2. Host Route Predicate:加正确的请求地址才能匹配;不加直接报错

    curl http://localhost:9527/payment/lb -H “Host: www.atguigu.com"

    curl http://localhost:9527/payment/lb -H “Host: new.atguigu.com”

  3. Method Route Predicate:规定请求方法(GET/POST…)

  4. Path Route Predicate:路径

  5. Query Route Predicate:加参数名

    curl http://localhost:9527/payment/lb?username=31

总结:Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理

5. gateway的Filter的使用

是什么:路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用

Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂来产生

种类:

​ 1.GatewayFilter 单一的—31钟

​ 2.GlobalFilter 全局的—10种

自定义全局过滤器 GlobalFilter

使用前先把yml里请求中需要携带信息的断言配置注释掉

两个主要接口:implements GlobalFilter,Ordered

能干嘛:全局日志记录,统一网关鉴权…

image-20220915102209467
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
// 容器注入(在包下想要被扫描到一定要加的配置)
@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// chain:过滤器链
log.info("********come in MyLogGatewayFilter: " + new Date());

String uname = exchange.getRequest().getQueryParams().getFirst("uname");

if (uname == null) {
log.info("******用户名为null,非法用户,(T__T)");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}

return chain.filter(exchange);
}

@Override
public int getOrder() {
// 加载过滤器的顺序,数字越小,优先级越高
return 0;
}
}

启动7001、8001、8002、9527

正确地址:localhost:9527/payment/lb?uname=sd

uname 的值是多少都行

image-20220915103140241

image-20220915112337654

image-20220915103228782image-20220915103255817

ServerWebExchange接口:服务网络交换器,存放着重要的请求-响应属性、请求实例和响应实例等等

​ ServerHttpRequest接口:用于承载请求相关的属性和请求体

​ ServerHttpResponse接口:用于承载响应相关的属性和响应体

gateway小结

​ 过滤器是线性结构,只要设置了都会一个一个进入;一般放在全局配置。断言则是过滤器之后设置在接口上的规则,需要哪些请求信息

断言是用来判断访问是否符合路由规则,不符合报错404

过滤器是判断请求中是否有我要的信息,报错406

十四、 SpringCloud Config 分配式配置中心

大多配合使用:Config+Bus 后期逐渐被 Nacos 所替代

当前微服务工程面对的问题:项目越来越多,重复的配置文件也越来越多。我们希望做到一次配置,处处生效

​ 微服务意味着将单体应用中的业务拆分成一个个的子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一个集中式、动态的配置管理设施是必不可少的。

​ SpringCloud提供了ConfigServer来解决这个问题,如果每一个微服务都有一个application.yml,那上百个配置文件。。。想想都恐怖

image-20220915144556950

配置中心是什么:SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持(一般就是Git或Github),配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置

怎么用:SpringCloud Config 分为服务端和客户端两部分

​ 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密、解密信息等访问接口

​ 客户端通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息。配置服务器默认采用git来存储信息,这样有助于对环境配置进行版本处理,并可以通过git客户端工具来方便管理和访问配置内容

能干嘛:

  1. 集中管理配置文件
  2. 不同环境不同配置,动态化更新配置,分环境部署(dev/test/prod/beta/release)–(开发环境/测试/生产/预发布/发布)
  3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
  4. 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  5. 将配置中心以REST接口的形式暴露——post、curl访问刷新均可

和GitHub整合使用:由于SpringCloud Config默认使用Git来存储配置文件(SVN和本地文件也可),但最推荐Git,而且使用的是http/https访问的形式

项目启动时去配置中心拉自己所需的配置

十五、 SpringCloud Bus消息总线

问题:

  1. config只能手动刷新,我们想做到自动刷新;
  2. 假如100台机子,想要实现全部刷新怎么办
  3. 怎么精确刷新,比如只更新98台,剩下2台不刷新

Bus支持两种消息代理:RabbitMQ 和 Kafka

image-20220916145617320

通过消息中间件来推送

SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能

第1、2步,推送新的配置到git,更新到配置中心里;第3步,POST请求广播全局刷新

什么是总线:

​ 微服务架构系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题的消息会被所有实例监听和消费,所有称为消息总线。在总线上的各个实例,都能广播一些让连接在该主题上的其他实例都知道的消息

基本原理:

​ ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus)。当一个服务刷新数据时,它会把这个信息放到topic中,这样其他监听同一个topic的服务就能得到通知,在更新自身配置

安装RabbitMQ

RabbitMQ需要Erlang环境支持

下载Erlang

版本对照表:RabbitMQ Erlang Version Requirements — RabbitMQ

十六、 SpringCloud Stream 消息驱动

消息中间件:基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的信息传输的支撑性软件系统

解决的问题:不同端之间(后端和大数据端使用不同的消息中间件)会存在多种MQ(消息中间件),如:ActiveMQ、RabbitMQ、RocketMQ、Kafka。需要一种新技术,让我们不在关注具体的MQ细节,我们只需要用一种适配绑定的方式,自动可以在各种MQ内切换。

SpringCloud Stream

​ 是一个构建消息驱动微服务的框架,我们通过配置来binding(绑定),而SpringCloud Stream的binder对象负责与消息中间件交互。

目前仅支持RabbitMQ、Kafka

标准的MQ:image-20220919092659451

生产者、消费者之间靠消息媒介传递信息内容——Message

消息必须走特定的通道——–消息通道MessageChannel

消息通道里的消息如何被消费,谁负责收发处理——–消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅(订阅就发送,不订阅就不发送)

引入Stream

image-20220919093818379

这些中间件的差异性,对我们实际项目开发造成一定的困扰性,如果我们用了两个消息队列的其中一种,后面的业务需求需要使用另外一种,很多东西都要推倒重做,因为和系统耦合了,这时SpringCloud Stream给我们提供了一种解耦合的方式

​ 通过定义绑定器作为中间层,完美实现应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使应用程序不需要考虑各种不同的消息中间件实现

通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离

Binder:INPUT对应消费者 OUTPUT对应生产者

1. Stream标准流程套路

image-20220919101013778 image-20220919101043845
  • Binder—–方便的连接中间件,屏蔽差异
  • Channel——通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
  • Source和Sink—– 简单理解为参照对象是SpringCloud Stream自身,从Stream发布消息就是输出,接受消息就是输入

image-20220919101553918

2. 消息驱动生产者

cloud-steam-rabbitmq-provider8801

pom:

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
<dependencies>
<!--stream-rabbit-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:

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
server:
port: 8801

spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
# instance:
# lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
# lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
# instance-id: send-8801.com # 在信息列表时显示主机名称-发送者8801
# prefer-ip-address: true # 访问的路径变为IP地址

主启动:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class CloudStreamRabbitmqProvider8801Application {

public static void main(String[] args) {
SpringApplication.run(CloudStreamRabbitmqProvider8801Application.class, args);
System.out.println("启动成功");
}
}

service: 和消息中间件打交道的逻辑

1
2
3
4
public interface IMessageProviderService {
// 定义消息的推送管道--消息的发送者
String send();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.cloud.stream.messaging.Source;

// 调的是消息中间件的service,不是dao层,所以用的不是@Service注解
@EnableBinding(Source.class)
public class MessageProviderServiceImpl implements IMessageProviderService {
@Resource
private MessageChannel output; // 消息发送管道

@Override
public String send() {
String serial = UUID.randomUUID().toString();
// 消息构造器
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " + serial);
return serial;
}
}

controller:

1
2
3
4
5
6
7
8
9
10
@RestController
public class SendMessageController {
@Resource
private IMessageProviderService messageProviderService;

@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProviderService.send();
}
}

3. 消息驱动消费者

cloud-stream-rabbitmq-consumer8802

pom和上头一样

yml: 主要是在 bindings 处修改为 input

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
server:
port: 8802

spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置


eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
# instance:
# lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
# lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
# instance-id: receive-8802.com # 在信息列表时显示主机名称
# prefer-ip-address: true # 访问的路径变为IP地址

主启动

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.cloud.stream.messaging.Sink;

@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;

@StreamListener(Sink.INPUT) // 作为一个输入源监听
// output 发的时候使用的String,这边收就也是 String
// output 使用的 withPayload,input 收就使用 getPayload
public void input(Message<String> message) {
System.out.println("port:" + serverPort + "\t接受:" + message.getPayload());
}
}

4. 分组消费与持久化 重要*

存在问题:

  1. 重复消费
  2. 消息持久化
image-20220919163535543

​ 默认分组或者还没分组时,提供服务方是集群环境,可能会导致一个服务发送过来后,被两个或多个服务获取到。就会造成数据错误;比如可能会扣除两次款

不同组是可以全面消费的(重复消费)

同一个组内是竞争关系,只有其中一个可以消费

自定义分组:A、B (不自定义就是随机的流水号) 此组即为消费组

image-20220919164846437

image-20220919165343013 image-20220919165409551

同一个组的多个微服务案例,每次只会有一个获取调用,会轮询调用

持久化:避免消息丢失(添加分组后)

​ 在业务死掉后重启,依然可以获取业务停止时请求的服务信息

十七、 SpringCloud Sleuth 分布式请求链路跟踪

为什么会出现这项技术:

​ 在微服务框架中,一个由客户端发起的请求在后端会调用多个不同的服务节点来协同得到最后的结果,每一个前端请求都会形成一个复杂的分布式服务调用链路,链路中任何一环出现高延时或错误都会引起整个请求的最后失败

获得每一个调用节点的完整的轨迹图

SpringCloud Sleuth提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并兼容支持zipkin

image-20220919190446834

使用

下载zipkin jar包Central Repository: io/zipkin/zipkin-server (maven.org)

image-20220919191215337

也就是 sleuth 整合了 zipkin;或者说SpringCloud借鉴了zipkin然后换了个名字

运行jar包

1
java -jar zipkin-server-2.23.9-exec.jar

image-20220919191532515

访问:localhost:9411

image-20220919191557173

​ 一个完整的请求链路,每条链路都用一个Trace Id唯一标识,Span标识发起的请求信息,各Span通过 parent id 关联起来

image-20220919191923670

Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识

span:标识调用链路来源,通俗理解span就是一次请求信息

案例:使用8001和888做示范

image-20220919193604606

pom:两个都新增

1
2
3
4
5
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

8001 yml:

1
2
3
4
5
6
7
8
spring:
zipkin:
# 内容发布到9411
base-url: http://localhost:9411
sleuth:
sampler:
# 采样率值介于 0 到 1 之间,1 表示全部采集;一般不用设为 1
probability: 1

image-20220919193246798

8001 controller: 写一个简单的测试接口

1
2
3
4
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
return "hi, I am paymentZipkin server fall back, welcome to here ^_^";
}

888 yml:

image-20220919193804706

888 controller:

1
2
3
4
5
6
// ====> zipkin + sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin() {
String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class);
return result;
}

依次启动 7001/8001/888

多次调用查看zipkin的内容

image-20220919194743940

image-20220919194752736

image-20220919194840893

十八、 SpringCloud Alibaba简介

SpringCloud Netflix项目进入维护模式(不再开发新的组件)

官网:Spring Cloud Alibaba

英文:https://github.com/alibaba/spring-cloud-alibaba

https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

中文:https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/README-zh.md

十九、 SpringCloud Alibaba Nacos服务注册和配置中心

前四个字母是Naming和Configuration的前两个字母,最后s为Service

1
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos就是注册中心+配置中心的组合 ====》Nacos = Eureka +Config + Bus

1. 下载和安装

官网:https://github.com/alibaba/nacos

下载:https://nacos.io/zh-cn/

下载后解压:image-20220920102721472

进入bin目录下,cmd打开,单机模式下启动: startup.cmd -m standalone

image-20220920102853858

访问地址:localhost:8848/nacos

默认用户名和密码:nacos

image-20220920103015574

image-20220920103038835

2. Nacos 作服务注册中心

1> 服务提供者

new Moudle:cloudAlibaba-provider-payment9001

配置的案例跟官网走就好:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

image-20220920140404693

父pom中:springcloud alibaba 的依赖版本是 2.1.0.RELEASE

pom:

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
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringBoot 整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9001

spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址

# 把要监控的东西暴露出来
management:
endpoints:
web:
exposure:
include: '*'

main启动

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}

controller

1
2
3
4
5
6
7
8
9
10
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;

@GetMapping("/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverport: " + serverPort + "\t id" + id;
}
}

测试:启动 nacos,启动9001

image-20220920140841727

为方便演示负载均衡,创建一个相同的9002

image-20220920142126064

image-20220920142311167

2> 服务消费者

cloudAlibaba-consumer-nacos-order83

image-20220920144757836

pom:都差不多的

nacos默认自带负载均衡

image-20220920144904376

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
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringBoot 整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:把微服务地址直接配置在配置文件中,就不用业务类里定义了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 83

spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址

# 把微服务名称写到配置文件中,业务代码中就不用写了,直接引入
service-url:
nacos-user-service: http://nacos-payment-provider

config: 负载均衡配置

1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationContextConfig {
@Bean
// 通过微服务命令找到了,但其下有很多实例,不知道该用哪个;所以需要该注解
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-user-service}")
private String serverURL;

@GetMapping("/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}

主启动就是正常:

1
2
@SpringBootApplication
@EnableDiscoveryClient

启动 nacos/9001/9002/83

image-20220920145122956

3> Nacos 注册中心对比

支持CP 和 AP 之间切换

Nacos的一致性协议: CP+AP

image-20220920145958869

C:所有节点在同一时间看到的数据是一致的(你有的和我有的是一样的)

A:高可用(行不行的都要给我拽一个)

可以使用命令行在 CP 和 AP 之间切换

3. Nacos 作服务配置中心

cloudAlibaba-config-nacos-client3377

pom: 基本以后用 nacos,就导入 config 和 discovery 依赖

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
<dependencies>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml:两个配置文件

​ Nacos同 springcloud-config 一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置后才能正常启动

springboot中配置文件加载是有优先级的,bootstrap 高于 application

所以全局的放在 bootstrap 自己的放在 application

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#nacos 配置
server:
port: 3377

spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos 服务注册中心地址
# 3377 去 Nacos 上去读后缀名为 yaml的文件
config:
server-addr: localhost:8848 #Nacos 配置中心地址
file-extension: yaml # 指定 yaml 格式的配置

# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

配置中心配置的命名公式:

  • prefix 默认为 spring.application.name 的值,也可以通过配置项spring.cloud.nacos.config.prefix来配置。
  • spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

所以配置文件名为:    nacos-config-client-dev.yaml

1
yml是YAML(YAML Ain’t Markup Language)语言的文件

application.yml

1
2
3
spring:
profiles:
active: dev # 表示开发环境

image-20220921111949481

主启动

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RefreshScope // 支持 Nacos 的动态刷新功能(实现配置自动更新)
public class ConfigClientController {
// 和8848 Nacos 服务器上的配置文件同名
@Value("${config.info}")
private String configInfo;

@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}

@Value 中匹配的是从配置中心拉过来的内容;所以写法是按照配置中心里的配置来写的

详情看下头自己写的 YAML 内容

Nacos 配置

image-20220921092029629

image-20220921092109404

名称对应:image-20220921092158393

启动3377测试:image-20220921093001459

自带动态刷新:修改Nacos中的yaml配置文件,再次调用查看配置的接口,发现配置已经刷新

​ nacos中修改配置信息

image-20220921093140109

4. Nacos 多环境配置

​ 就像Maven用groupId、artifactId、version三者来定位jar包在仓库中的位置一样,Nacos也提供了 Namespace 、Data ID 、Group 来确定一个配置文件(或者叫配置集)

实现多环境配置方案:

  1. 命名空间 (namesapce)区分:一个命名空间对应一个环境
  2. 配置组 (group)区分:命名空间默认public即可,一个组对应一种环境
  3. 配置集ID (Data ID)区分:命名空间和组用默认的,通过文件名来区分
1
2
3
4
5
多环境多项目管理:
问题1:
实际开发中,一个系统通常会有dev,test,prod环境;需要保证指定环境启动时能正确读取到Nacos上相应的配置文件
问题2:
一个大型分布式微服务会有很多微服务子项目,每个子项目又会有很多相应的环境

配置管理:image-20220921105209346

public 命名空间是兜底的,不能删(想删也删不掉)

image-20220921105723620

最外层 namespace 是用于区分部署环境的,Group 和 DataID逻辑上区分目标对象

image-20220921105511915
1
2
3
4
5
默认情况:
Namespace = public
Group = DEFAULT_GROUP
默认Cluster(集群)是DEFAULT
Instance(实例)

1> DataID配置

image-20220921111429650

2> Group 分组

DataID相同,组不同

image-20220921112104705

bootstrap中设置group,设哪个就找哪个

image-20220921112233585

3> Namespace

image-20220921112747601

image-20220921112723376

在不同的 namespace 下建配置就好

使用:在不同 namespace 下创建配置就好

image-20220921135949238

层级结构:namespace > group > data id

5. Nacos 集群和持久化(重要)

部署生产集群模式,要在 Linux 环境下部署;使用 MySQL(高可用数据库)

光配到Nacos上还不够,容易出现问题,还要放到数据库中

image-20220921140806272

最终的数据源一致(都汇聚到mysql中)

Nacos 自带一个内嵌的数据库–derby ,若启动多个Nacos,数据会存在一致性问题,或配置丢失问题

所以 Nacos 采用集中式存储的方式来支持集群化部署,目前只支持 mysql

Nacos 支持三种部署方式

  • 单机模式 - 用于测试和单机试用
  • 集群模式- 用于生产环境,确保高可用
  • 多集群模式 - 用于多数据中心场景

单机模式下,把 derby 替换成 mysql,具体参考官网(部署手册 ——– 单机模式支持mysql):https://nacos.io/zh-cn/docs/deployment.html

集群模式:image-20220921145852227

image-20220921145729390

Linux版的下载安装:

github上下载Linux版本,到虚拟机中,拷贝到 /opt 下解压

1
tar -zxvf nacos-server-1.4.4.tar.gz

image-20220921162013141

解压后,会有一个nacos文件夹;拷贝到新创建的 mynacos 文件夹中

1
[root@hspEdu01 opt]# cp -r nacos /mynacos/

image-20220921162232625

步骤:

  1. Linux 上配置mysql
  2. application.properties 配置修改———把nacos默认的数据库改为指向mysql
  3. Linux 服务器上 nacos 的集群配置 cluster.conf———–梳理出同一集群的端口号,比方三台机器就有三个不同的端口号(IP地址要设置成Linux的地址)
  4. 编辑Nacos的启动脚本 start.sh,使它可以接受使用不同端口
  5. Nginx配置,作为负载均衡器
image-20220922103752093

二十、 SpringCloud Alibaba Sentinel 实现熔断和限流

官网:https://sentinelguard.io/zh-cn/docs/introduction.html

image-20220922153424783

下载:Releases · alibaba/Sentinel (github.com)

image-20220922154206331

cmd中运行jar包, 访问localhost:8080,默认用户名和密码 sentinel

image-20220922155757350

1. 演示工程

new Moudle: cloudAlibaba-sentinel-service8401

pom:使用 nacos+sentinel 的话,基本前3个依赖都加

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
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel-datasource-nacos 后续持久化使用-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719

#actuator 端点启用和暴露
management:
endpoints:
web:
exposure:
include: '*'

主启动:

@EnableDiscoveryClient ——– 让注册中心能够发现

@SpringBootApplication

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class FlowLimitController {

@GetMapping("/testA")
public String testA() {
return "-----------testA";
}

@GetMapping("/testB")
public String testB() {
return "-----------testB";
}
}

测试:启动 Nacos、Sentinel,启动微服务

​ 因为Sentinel是懒加载机制,需要调用接口后才能看到信息

image-20220923094656289

2. 流控规则

流量限制控制规则

image-20220923095115034
  • 资源名:唯一名称,默认请求路径
  • 针对来源:Sentinel 可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
  • 阈值类型/单机阈值:
    • QPS(每秒请求数量):当调用该 api 的QPS达到阈值时,进行限流
    • 线程数:当调用该 api 线程数达到阈值时,进行限流
  • 是否集群
  • 流控模式:
    • 直接:api达到限流条件时,直接限流
    • 关联:当关联的资源达到阈值时,就限流自己
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流) 【api级别的限流】
  • 流控效果:
    • 快速失败:直接失败,抛异常
    • Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值➗codeFactor,经过预热时长,才达到QPS阈值
    • 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效

2.1 流控模式

直接: 1s请求1次可以,多了直接报错image-20220923101532204

访问量1s超过1次:默认报错image-20220923101707345

思考:当访问失败时,我们需要能够自己处理问题,也就是允许用户自定义

1
2
QPS:每秒允许访问的请求量
线程数:高并发的请求过来(QPS很高),线程数为1,相当于我前台工作人员只有1个,只能接待一个服务,当前服务未结束,后面再来的就要报错

关联:当关联的资源达到阈值,就限流自己(连坐效应,比方支付模块到阈值,就限流下单)

当与A关联的B达到阈值时,限流A——–B惹事,A挂了

解释:这边压力大,那边就别进来了

image-20220923103552569

模拟:使用 postman 让B不停的请求;访问地址添加进多线程集合组

image-20220923105245997

Run,大批量线程高并发访问B,导致A失败

image-20220923104810531

20个线程结束后,调用A回复正常

3. 流控效果

直接 ==》 快速失败(默认的流控处理)

预热:冷启动的方式;当系统长期无人问津,突然拉高访问量,容易把系统压垮。通过冷启动,让通过的流量缓慢增加,在一段时间内逐渐达到阈值上限。

image-20220923110731919

默认 coldFactor 为3,即请求QPS从(coldFactor /3)经多少预热时间才逐渐升至设定的QPS阈值

例子:阈值为10,预热时间5s

系统初始化阈值为10/3,约为3,即阈值刚开始为3;经过5s后阈值慢慢升高到10

排队等待:严格控制请求通过的间隔时间,就是让请求以均匀速度通过;对应漏桶算法

image-20220923111530999

匀速排队,让请求以均匀速度通过,阈值类型必须是QPS,否则无效

设置含义:/testB每秒请求1次,超过就排队等待,等待超时时间为20000ms

测试:postman高并发多线程测试即可(一次一堆请求过来)

4. 熔断规则

image-20220923134330816

  • 慢调用比例

    指耗时大于阈值RT的请求称为慢调用,阈值RT由用户设置

    请求数大于最小请求数并且慢调用的比率大于比例阈值则发生熔断,熔断时长为用户自定义设置。

  • 异常比例(秒级)

    QPS >= 5 且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

  • 异常数(分钟级)

    异常数(分钟统计)超过阈值时, 触发降级,时间窗口结束后,关闭降级

5. Sentinel 热点key

image-20220923144309319

热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K数据

image-20220923153843310

image-20220923153854007

代码设置:设置热点规则后的兜底函数

1
2
3
4
5
6
7
8
9
@GetMapping("testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
return "-------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception) {
return "-------deal_testHotKey,sorry T_T"; // sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
}

方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理

本来应该是报我自定义的返回信息,这块可能是jar包冲突了所以直接成页面报错信息了 SOS

image-20220923155510881

注: 参数中只要有 p1 就会报问题;如果只携带 p2 是不会触发降级的

参数例外项

只要当 p1=5 时,阈值可变为200

image-20220923161040379
1
@SentinelResource 只管配置出错;若是程序出错,改报异常就是异常

6. @SentinelResource

从 HystrixCommand 到 @SentinelResource

8401 项目,pom 导入自定义的 ja r包

1
2
3
4
5
 <dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>

controller

两种方法都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class RateLimitController {
// 按资源名测试限流
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200,"按资源名称限流测试ok",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
}

// 按 url 测试限流
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200,"按url限流测试ok",new Payment(2020L,"serial002"));
}
}

启动 Nacos、Sentinel、8401

image-20220926093105136

blockHandler:设置了就返回自定义的报错信息;没有就返回系统默认的‘

image-20220926094236294

image-20220926093317513

限流是临时的,停掉微服务后规则会直接消失

把降级处理的信息改为统一类,降低 程序的耦合度

新建一个类用于统一返回降级信息:

image-20220926100325572

1
2
3
4
5
6
7
8
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444,"按客户自定义,global handlerException---------1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444,"按客户自定义,global handlerException---------2");
}
}

controller:测试的接口

1
2
3
4
5
6
7
8
9
10
// 测试降级的统一返回
@GetMapping("/rateLimit/customerBlockHandler")
// blockHandlerClass 指定哪个类是统一降级处理的类
// blockHandler 指定使用类中的哪个方法
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200,"按客户自定义",new Payment(2020L,"serial003"));
}

image-20220926100905503

7. Sentinel 服务熔断

Sentinel 整合 ribbon + openFeign + fallback

服务提供者:cloudAlibaba-provider-payment9003/9004 负载均衡

服务消费者:cloudAlibaba-consumer-nacos-order84

Ribbon系列

1> 创建提供者

9003和9004相同,改下端口号就行

pom

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
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringBoot 整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9003

spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址

# 把要监控的东西暴露出来
management:
endpoints:
web:
exposure:
include: '*'

主启动:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}

controller: 偷懒,用哈希表做数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;

// 模拟一个数据库
// Payment 是自定义的那个类
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L,new Payment(1L,"28a82uj2h8d92hnv"));
hashMap.put(2L,new Payment(2L,"bb2jdu9283nd9ch1"));
hashMap.put(3L,new Payment(3L,"cv87ekd0293ncf82"));
}

@GetMapping("/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: " + serverPort, payment);
return result;
}
}

测试:启动 nacos、sentinel、9003、9004

image-20220926144953921

2> 创建消费者

pom

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
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel-datasource-nacos 后续持久化使用-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.3</version>
</dependency>
<dependency> <!-- 引入自定义的api通用包,调实体类entities -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 84

spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
#Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719

# 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider

主启动

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}

配置类:负载均衡

1
2
3
4
5
6
7
8
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

controller

fallback 管运行异常(主业务逻辑的问题)

blockHandler管配置违规问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@RestController
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";

@Resource
private RestTemplate restTemplate;

@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") // 没有配置
public CommonResult<Payment> fallback(@PathVariable Long id) throws IllegalAccessException {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

// Java运行时异常,因为没有id为4的数据
if (id == 4) {
throw new IllegalAccessException("IllegalAccessException,非法参数异常。。。");
} else if (result.getData() == null) { // 输1-4以外直接空指针异常
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}
}

@SentinelResource的配置

1
2
@SentinelResource(value = "fallback", fallback = "handleFallback") // fallback只负责业务异常
@SentinelResource(value = "fallback", blockHandler = "blockHandler") // blockHandler只负责sentinel可视化控制台配置违规

两个结合使用(报错时各找各妈)

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
@Slf4j
@RestController
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";

@Resource
private RestTemplate restTemplate;

@RequestMapping("/consumer/fallback/{id}")
// 两个一起用
@SentinelResource(value = "fallback",fallback = "handleFallback", blockHandler = "blockHandler")

public CommonResult<Payment> fallback(@PathVariable Long id) throws IllegalAccessException {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalAccessException("IllegalAccessException,非法参数异常。。。");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}
// 本例是 fallback
public CommonResult handleFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handleFallback,exception内容 " + e.getMessage(),payment);
}
// 本例是 blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockException-sentinel限流,无此流水:blockException " + blockException.getMessage(),payment);
}
}

在Sentinel中设置熔断规则

image-20220926152535585

问题:如果异常同时触发 fallback 和 blockHandler;应该找谁??

测试:调用 id 为4的接口,本为Java异常问题,但同时触发熔断规则

image-20220926152832895

所以,若 fallback 和 blockHandler 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockException 处理逻辑

Feign系列

3> 使用Feign调用微服务

Feign组件一般是消费侧

image-20220926155734168

添加pom

1
2
3
4
5
<!--SpringCloud openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

yml: 激活 feign

1
2
3
4
# 激活Sentinel对feign的支持
feign:
hystrix:
enabled: true

主启动:添加一个注解

1
@EnableFeignClients

service:

1
2
3
4
5
6
7
8
9
// Feign 就是接口加注解
// fallback 去找它的实现方法
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
// 带着 Feign 注解的业务接口
public interface PaymentService {
//调用 9003 和 9004的方法
@GetMapping("/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
1
2
3
4
5
6
7
8
// 降级的统一实现方法
@Component
public class PaymentFallbackService implements PaymentService{
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(444444,"服务降级返回,------PaymentFallbackService",new Payment(id,"ErrorSerial"));
}
}

controller:

1
2
3
4
5
6
7
8
// ====================OpenFeign
@Resource
private PaymentService paymentService;

@GetMapping("/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}

测试:

image-20220926160212152

关闭9003、9004;服务降级,不会拖垮服务器

image-20220926160342379

4> 熔断框架比较

Sentinel Hystrix
隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离
熔断降级策略 基于响应时间、异常比率、异常数 基于异常比率
实时统计实现 滑动窗口(LeapArray) 滑动窗口(基于RxJava)
动态规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于QPS,支持基于调用关系的限流 有限的支持
流量整形 支持预热模式、匀速器模式、预热排队模式 不支持
系统自适应保护 支持 不支持
控制台 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 简单的监控查看

8. 规则持久化

存在问题:重启微服务后,Sentinel 规则会消失,生产环境需要将配置规则持久化

解决:规则配置注册进 Nacos 中

修改 cloudAlibaba-sentinel-service8401

pom:添加

1
2
3
4
5
<!--SpringCloud alibaba sentinel-datasource-nacos 规则久化使用-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

yml

image-20220926172022503

1
2
3
4
5
6
7
8
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow

Nacos新增配置

image-20220926171953487

image-20220926171909231
1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"cluserMode": false
}
]
  • resource:资源名称
  • limitApp:来源应用
  • grade:阈值类型,0表示线程数,1表示QPS
  • count:单机阈值
  • strategy:流控模式,0表示直接,1表示关联,2表示链路
  • controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
  • cluserMode:是否集群

测试:启动8401,访问接口 http://localhost:8401/rateLimit/byUrl

image-20220926172242332

快速点击:规则生效image-20220926172259629

重启 8401 后,再次调用接口,查看 Sentinel,规则重新出现

二十一、SpringCloud Alibaba Seata 处理分布式事务

​ 分布式之后,单体应用都被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作也需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性就无法保证。

我们需要解决的问题就是:如何保证全局数据的一致性

1. Seata 简介

​ Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事件

官网:https://seata.io/zh-cn/

一个典型的分布式事务过程:分布式事务处理过程的 一ID+三组件模型

  • XID:全局唯一的事务ID
  • 3组件概念
    1. TC——-事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚
    2. TM——-事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务
    3. RM——-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务的提交或回滚

处理过程:

image-20220927101736657

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

    (各个班的班主任TM向授课老师TC申请一个班级号)

  2. XID 在微服务调用链路的上下文中传播

    (班级号在学生们之间传播,让他们加入班级)

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖

    (学生们加入授课老师的课堂,授课老师可以统一看到所有学生)

  4. TM 向 TC 发起针对 XID 的全局提交或回滚协议

    (班主任发起签到,然后告诉老师可以上课了;老师可以屏幕共享到所有学生)

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求

    (下课休息,告知全部学生)