# SpringCloudStudy
**Repository Path**: packagejava/spring-cloud-study
## Basic Information
- **Project Name**: SpringCloudStudy
- **Description**: SpringCloud H,SpringCloud Alibaba 的学习
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2025-06-16
- **Last Updated**: 2025-06-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# SpringCloud
# 第一章 简介
## 1.1 微服务架构概述
一种**架构模式**,提倡将单一应用程序划分成一组小的服务,服务之间互相调用,互相配合,为用户提供最终价值
每个服务运行在独立的**进程中**,服务与服务间采用**轻量级的通信机制**互相协作(基于 http 协议的 RESTFul API)
每个服务都围绕其服务进行构建,并且能够**独立**的部署到生产环境,类生产环境等
与现在的**数字化生活**对比
- 数字化生活一个大的主体,细分的是具有多个维度的,而不同维度又有不同的厂家提供

- 但如果我们全部使用一个厂家下提供的维度支持,那么各个维度之间的连接就不会那么困难
将 **数字化生活** 替换成 **基于分布式的微服务架构** 时又需要**满足哪些维度?** 且 **支撑起这些维度的具体技术**又有谁能提供呢? -> SpringCloud
## 1.2 Spring Cloud 简介
> **分布式微服务架构**的一站式解决方案,是多种微服务架构落地技术的集合体,俗称 **微服务全家桶**
结构图

具体的**架构**(也就是维度)
- 服务注册与发现
- 服务调用
- 服务熔断
- 负载均衡
- 服务降级
- 服务消息队列
- 配置中心管理
- 服务网关
- 服务监控
- 全链路追踪
- 自动化构建部署
- 服务定时任务调用操作
具体的落地技术(等等)

## 1.3 Spring Cloud 技术栈

对于 SpringBoot 和 SpringCloud 之间的版本选择
1. 可以通过查看官网:https://spring.io/projects/spring-cloud

2. 通过请求查看 json 字符串: https://start.spring.io/actuator/info

# 第二章 关于 Cloud 各种组件的停更/升级/替换

SpringCloud 中文文档:https://www.springcloud.cc/spring-cloud-dalston.html
# 第三章 基本环境搭建
> 约定 > 配置 > 编码
## 3.1 IDEA Project 父工程
1. 新建一个 **Maven** 工程

2. 选择 Maven 版本

3. 保证字符编码为 UTF-8

4. 保证注解生效

5. 调整 Java 编译版本为 1.8

6. (可选) 过滤文件

## 3.2 父工程 POM
```xml
pom
UTF-8
1.8
1.8
4.12
1.2.17
1.16.18
8.0.17
1.1.16
3.4.2
org.apache.maven.plugins
maven-project-info-reports-plugin
3.0.0
org.springframework.boot
spring-boot-dependencies
2.3.11.RELEASE
pom
import
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR11
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.5.RELEASE
pom
import
mysql
mysql-connector-java
${mysql.version}
runtime
com.alibaba
druid
${druid.version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.spring.boot.version}
p6spy
p6spy
${mybatis-plus.p6spy.version}
junit
junit
${junit.version}
log4j
log4j
${log4j.version}
org.projectlombok
lombok
${lombok.version}
true
org.springframework.boot
spring-boot-maven-plugin
true
true
```
父工程创建完成之后可以通过 `mvn:clean` 和 `mvn:install` 将父工程发布到仓库以便子工程继承
## 3.3 Maven 细节复习
### dependencyManagement 和 dependencies 的区别
前者通产只出现在**父工程**中,且**只是声明依赖,并不实现引入**,如果子项目需要显式的声明使用的依赖
如果子项目中不声明依赖,那么将不会从父工程中继承下来,只有在子项目中声明了该依赖,且**没有指定具体版本**,才会从父工程中继承该项,并自动使用父工程中声明的 `version` 和 `scope`
如果子项目中指定了版本号,那么就会使用子项目中的版本号
**好处:** 同一管理多个版本号,方便维护和升级,且子项目要独立使用时也可以自己指定并不会影响别的工程
### maven 跳过单元测试

## 3.4 Rest 微服务工程构建
### 支付模块搭建
> 命名规范:项目名-模块名
1. 创建一个普通的 Maven 工程(什么都不选)
2. 选择父工程和父工程

3. 在子模块的 pom 中导入需要使用的依赖
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
com.alibaba
druid
com.baomidou
mybatis-plus-boot-starter
p6spy
p6spy
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
```
4. 在 `resources` 目录下创建 `application.properties` 文件
```properties
# 指定端口号
server.port=90
# 指定服务名
spring.application.name=cloud-payment
# 配置数据库
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/cloud?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
# 指定实体类所在的包
mybatis-plus.type-aliases-package=pers.dreamer07.springcloud.bean
```
5. 在 `src` 目录下创建一个主启动类
```java
@SpringBootApplication
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
```
6. 创建数据库表
```sql
CREATE TABLE `payment`(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`serial` varchar(200) DEFAULT '',
PRIMARY KEY (id)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4
```
7. 创建对应的实体类
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {
private Long id;
private String serial;
}
```
8. 创建一个 **CommonResult** 类,负责和前端通信
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message){
this(code, message, null);
}
}
```
9. 编写对应的 Mapper
```java
@Mapper
public interface PaymentMapper extends BaseMapper {
}
```
10. 编写业务类
```java
/**
* 定义操作支付信息的业务逻辑
* @author Prover07
*/
public interface PaymentService {
/**
* 将支付信息保存到数据库中
* @param payment
* @return
*/
public int createPayment(Payment payment);
/**
* 根据 id 获取对于的支付信息
* @param id
* @return
*/
public Payment getPaymentById(Long id);
}
```
```java
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
private PaymentMapper paymentMapper;
@Override
public int createPayment(Payment payment) {
return paymentMapper.insert(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentMapper.selectById(id);
}
}
```
11. 编写控制器类
```java
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping("/payment/create")
public CommonResult createPayment(Payment payment){
log.info("cloud payment create:{}", payment);
int isSuc = paymentService.createPayment(payment);
if (isSuc > 0){
return new CommonResult<>(200, "支付成功", payment);
}else {
return new CommonResult<>(400, "支付失败", null);
}
}
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id){
log.info("cloud payment get by id:{}", id);
Payment payment = paymentService.getPaymentById(id);
if (payment != null) {
return new CommonResult<>(200, "查找成功", payment);
}else{
return new CommonResult<>(400, "查找失败, 没有该编号的支付信息 - " + id, null);
}
}
}
```
12. 使用 postman 测试


### 热部署 Devtools
> **开发时使用,生产环境时关闭**
1. 添加以下依赖到工程中(上面的配置中其实已经配好了)
```xml
org.springframework.boot
spring-boot-devtools
runtime
true
```
2. 添加 `plugin` 到父工程中
```xml
org.springframework.boot
spring-boot-maven-plugin
true
true
```
3. 修改 IDEA 的配置

4. 进入 `pom.xml` 中,按住 `Ctrl + Shift + Alt + /`

5. 重启 IDEA
### 消费者订单模块
1. 创建一个 `cloud-consumer-order` 模块,步骤和支付模块相似
2. 添加需要的依赖
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建· `application.properties` 文件
```properties
# 面对用户使用的服务,尽量使用 80 接口
server.port=80
spring.application.name=cloud-consumer-order
```
4. 编写主启动类
```java
@SpringBootApplication
public class ConsumerOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerOrderApplication.class, args);
}
}
```
5. 创建一个配置类
```java
@Configuration
public class ApplicationConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
```
需要使用 `RestTemplate` 进行模块间的通信
6. 创建一个订单控制器类
```java
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
private final String PAYMENT_URL = "http://localhost:9090/";
@GetMapping("/consumer/payment/{id}")
public CommonResult getPaymentById(@PathVariable Long id){
return restTemplate.getForObject(PAYMENT_URL + "/payment/" + id, CommonResult.class);
}
@PostMapping("/consumer/payment/create")
public CommonResult createPayment(Payment payment){
return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
}
```
7. 使用 **Run Dashboard** 窗口方便查看多个微服务模块


8. 如果该窗口没有出现
- 搭建到对应工程的 `.idea/workspace.xml` 文件
- 搜索 **RunDashboard** 项添加以下代码
```xml
```
- 重启 IDEA 查看 View 项即可
9. 测试
注意这里有个坑,支付模块中的控制器请求参数还需要添加一个 `@RequestBody` 注解



## 3.5 工程重构
1. `cloud-consumer-order` 和 `cloud-payment` 两个模块中出现的代码的冗余 -> model 包
且在 POM 中引用了过多的重复依赖

2. 可以新建一个子模块 `cloud-api-common` 负责放一些公用的模块和第三方包
将两个子模块重复的依赖删除添加到 POM 中
注意:需要进行依赖传递的 `optional` 不能设置为 true
```xml
org.springframework.boot
spring-boot-devtools
runtime
org.projectlombok
lombok
com.baomidou
mybatis-plus-boot-starter
true
cn.hutool
hutool-all
5.6.5
log4j
log4j
```
3. 将 `cloud-api-common` 进行 clean 再进行 install 安装到仓库中以便使用
4. 再两个服务中导入对应的依赖即可
```xml
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
5. 测试
# 第四章 服务注册与发现
## 4.1 Eureka
### 基础知识

**服务治理**
- 在传统的 rpc 远程调用框架中,管理每个**服务与服务之间依赖关系**比较复杂
- 所以需要使用服务治理,管理多个服务之间的依赖关系,可以实现**服务调用,负载均衡,容错**等,实现**服务注册与发现**
**服务注册**
- Eureka 采用 CS 的设计架构,Eureka Server 作为服务器注册功能的服务器,也就是**服务注册中心**
- 系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接,维护人员可以通过 Eureka Server 监控系统中的各个微服务
- 当系统中的服务启动时,会将自己所在的服务器的信息发送给**服务注册中心**
**Eureka 两组件**
1. Eureka Server 提供服务注册服务
各个微服务节点通过配置启动后,会在 Eureka Server 中进行注册,Eureka Server 的**服务注册表**将会存储所有服务节点的信息,服务节点的信息可以在界面中直观看到
2. Eureka Client 通过注册中心进行访问
用于简化与 Eureka Server 的交互,客户端内置一个使用**轮询负载算法**的负载均衡器
在应用启动后,会向 Eureka Server 发送心跳(默认周期为 30 秒)
如果 Eureka Server 在多个心跳周期内没有接受到某个节点的心跳,就会将其从服务注册表中把这个服务节点移出(默认为 90 秒)
### 单机 Eureka 构建步骤
1. 创建一个 Maven 子工程 `cloud-eureka-server`
2. 引入需要使用的依赖
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 编写配置文件
```properties
spring.application.name=cloud-eureka-server
server.port=7001
# eureka服务端的实例名称
eureka.instance.hostname=localhost
# 不会像注册中心注册自己
eureka.client.register-with-eureka=false
# 表示自己是注册中心,负责维护服务注册,不需要进行服务检索
eureka.client.fetch-registry=false
# 配置 Eureka Server 服务端的访问地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
```
4. 编写主程序类
```java
@SpringBootApplication
// 开启 EurekaServer
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
```
5. 启动并访问 http://localhost:7001/ 查看

### 支付模块注册 -> Eureka
> 将支付模块注册到 Eureka 中,成为服务提供者 provider
1. 修改 POM,引入以下依赖
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
```
2. 修改 `application.properties` 配置文件
```properties
# 配置 Eureka Client
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址
eureka.client.service-url.defaultZone=http://localhost:7001/eureka
```
3. 在启动类上添加 `@EnableEurekaClient` 注解
```java
@SpringBootApplication
@EnableEurekaClient
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
```
4. 启动并访问 http://localhost:7100 (注意要先启动 Eureka Server)

### 消费者模块 -> Eureka
> 将消费者模块注册到 Eureka 中,成为服务消费者 consumer
1. 在 POM 引入以下模块
```xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
```
2. 在配置文件中添加以下几项
```properties
# 配置 Eureka Client
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址
eureka.client.service-url.defaultZone=http://localhost:7001/eureka
```
3. 在启动类上添加 `@EnableEurekaClient` 注解
4. 启动并访问 http://localhost:7001/ (注意要先启动 Eureka Server)

### 集群 Eureka 构建步骤
- Eureka 集群原理

微服务 RPC 远程服务调用最核心的就是 **高可用**,实现负载均衡 + 故障容错
Eureka 本质就是**互相注册,相互守望**,各个 Eureka Server 之间都会互相注册,对外暴露出一个整体
- Eureka 集群环境构建步骤
1. 额外创建一个 Maven 工程,依赖信息和 cloud-eureka-server 一致
2. 进入 `C:\Windows\System32\drivers\etc` 目录,修改 hosts 文件
```
127.0.0.1 www.eureka7001.com
127.0.0.1 www.eureka7002.com
```
3. 分别修改两个工程的 `application.properties` 文件,做到 **互相注册,相互守望**
```properties
#============== cloud-eureka-server
# eureka服务端的实例名称
## 单机版
#eureka.instance.hostname=localhost
## 集群版
eureka.instance.hostname=www.eureka7001.com
# 配置 Eureka Server 服务端的访问地址
## 单机版
#eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
## 集群版 - 需要是另一个 Eureka Server 的访问地址
eureka.client.service-url.defaultZone=http://www.eureka7002.com:7002/eureka/
# 不会像注册中心注册自己
eureka.client.register-with-eureka=false
# 表示自己是注册中心,负责维护服务注册,不需要进行服务检索
eureka.client.fetch-registry=false
#============== cloud-eureka-server2
spring.application.name=cloud-eureka-server2
server.port=7002
# eureka服务端的实例名称
eureka.instance.hostname=www.eureka7002.com
# 配置 Eureka Server 服务端的访问地址
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka/
# 不会像注册中心注册自己
eureka.client.register-with-eureka=false
# 表示自己是注册中心,负责维护服务注册,不需要进行服务检索
eureka.client.fetch-registry=false
```
4. 为第二个节点创建一个主程序类
```java
@SpringBootApplication
// 开启 EurekaServer
@EnableEurekaServer
public class EurekaServer2Application {
public static void main(String[] args) {
SpringApplication.run(EurekaServer2Application.class, args);
}
}
```
5. 启动后分别访问 http://www.eureka7001.com:7001 和 http://www.eureka7002.com:7002


### 注册到 Eureka 集群
1. 修改两个微服务模块的 `application.properties` 文件
```properties
## 配置 Eureka Server 服务端的访问地址
#eureka.client.service-url.defaultZone=http://localhost:7001/eureka
## 配置 Eureka Server 服务端的访问地址(集群版)
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/eureka
```
2. 启动 Eureka 集群后访问 http://www.eureka7001.com:7001 和 http://www.eureka7002.com:7002


3. 使用 PostMan 测试微服务

### 搭建支付模块集群
> 这里可以创建两个项目,也可以只有一个当重复启动
1. 如果是创建两个项目,注意修改端口号即可
这里使用 IDEA 启动同一个项目,修改端口号即可

2. 分别启动五个微服务模块,访问 Eureka 集群界面

3. 修改消费者模块中的业务逻辑
```java
// private final String PAYMENT_URL = "http://localhost:9090/";
// 配置 Eureka 和生产者集群后使用对应的服务名即可
private final String PAYMENT_URL = "http://CLOUD-PAYMENT/";
```
```java
@Configuration
public class ApplicationConfig {
@Bean
//使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
```
4. 启动消费者模块后使用 PostMan 测试

5. 分别查看两个支付模块的控制台打印
### actuator 微服务信息完善
1. 主机名称:服务名修改
1. 在 IDEA 的启动项中为两个服务添加 `-Deureka.instance.instance-id=cloud-payment9090` & `-Deureka.instance.instance-id=cloud-payment9091` 运行参数

2. 启动项目,访问 Eureka 的管理界面

2. 访问信息有 IP 信息提示
在 **cloud-payment** 项目的 `application.properties` 添加以下配置
```properties
## 显示 ip 信息
eureka.instance.prefer-ip-address=true
```

### 服务发现 Discovery
> 对于注册到 Eureka 里面的微服务,可以通过 **服务发现** 获得该服务的信息
1. 修改 **cloud-payment** 的控制器类
```java
public class PaymentController {
@Autowired
private DiscoveryClient discoveryClient;
....
/**
* 查看 Eureka 中的微服务模块信息
* @return
*/
@GetMapping("/discovery/service")
public Object getEurekaInfo(){
// discoveryClient.getServices() - 获取所有注册的微服务模块名称
for (String serviceName : discoveryClient.getServices()) {
log.info("cloud payment discovery service: {}", serviceName);
}
// discoveryClient.getInstances() - 获取指定服务名称的实例
for (ServiceInstance serviceInstance : discoveryClient.getInstances("cloud-payment")) {
log.info("cloud payment service instances: ip-{}, port-{}, uri-{}",
serviceInstance.getHost(), serviceInstance.getPort(), serviceInstance.getUri());
}
return discoveryClient;
}
}
```
2. 在启动类上添加一个 `@EnableDiscoveryClient` 注解
3. 启动项目,访问 http://localhost:9090/discovery/service

查看控制台打印

### Eureka 自我保护
> CAP 中的 AP 分支
**概述**
主要用于一组客户端和 Eureka Server 之间存在**网络分区场景**下的保护,一旦进入保护模式,Eureka Server 将会尝试保护其服务注册表中的信息,不会删除服务注册表中的数据(不会注销任何服务)
(如果在 Eureka Server 的管理界面出现以下文字,就代表进入了保护博士)

**出现原因**
默认情况下,如果 Eureka Server 在一定时间类内没有接受到某个微服务实例的心跳,Eureka Server 就会注销该服务信息(默认 90 秒)
但如果在短时间内 Eureka Server 丢失了过多的客户端时(发生了网络分区故障),那么 Eureka Server 就会进入自我保护模式
**关闭自我保护模式**
1. 在 **cloud-eureka-server** 和 **cloud-eureka-server2** 的 `application.properties` 添加以下配置
```properties
# 关闭自我保护机制
eureka.server.enable-self-preservation=false
# 配置最长未发送心跳时间(默认为90秒)
eureka.server.eviction-interval-timer-in-ms=2000
```
2. 在 `cloud-payment` 的 `application.properties` 添加以下配置
```properties
## Eureka 客户端向 Eureka Server 发送心跳的时间间隔(默认为30s)
eureka.instance.lease-renewal-interval-in-seconds=1
## Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
eureka.instance.lease-expiration-duration-in-seconds=10
```
3. 启动 Eureka 集群和一个消费模块

4. 关闭消费模块,再次查看 Eureka 信息

## 4.2 Zookeeper
### 在 Linux 上安装
参考博客:https://www.jianshu.com/p/ed6ec88b01c3
安装后启动 Zookeeper 并关闭防火墙:`systemctl stop firewalld.service`
### 服务提供者 -> Zookeeper
1. 为了和上面的程序分开,建议新建一个 Modul
2. 修改 POM
```xml
org.springframework.cloud
spring-cloud-starter-zookeeper-discovery
org.apache.zookeeper
zookeeper
org.apache.zookeeper
zookeeper
3.7.0
org.slf4j
slf4j-log4j12
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
3. 创建 `resoucres/application.properties`
```properties
server.port=9092
spring.application.name=cloud-zookeeper-payment
# 配置 Zookeeper
spring.cloud.zookeeper.connect-string=192.168.127.131:2181
```
4. 创建启动类
```java
@SpringBootApplication
//该注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class ZookeeperPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(ZookeeperPaymentApplication.class, args);
}
}
```
5. 编写业务类
```java
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String port;
@RequestMapping(value = "/payment/zk")
public String paymentzk() {
return "springcloud with zookeeper: "+ port +"\t"+ UUID.randomUUID().toString();
}
}
```
6. 启动项目,访问 http://localhost:9092/payment/zk
7. 通过命令行查看 zookeeper 中的信息

通过格式化查看 json 数据 - 模块注册进来时提供的信息
```json
{
"name": "cloud-zookeeper-payment",
"id": "7d30c60e-af97-4ae3-a07d-d81f42f83b8e",
"address": "localhost",
"port": 9092,
"sslPort": null,
"payload": {
"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
"id": "application-1",
"name": "cloud-zookeeper-payment",
"metadata": {
"instance_status": "UP"
}
},
"registrationTimeUTC": 1621918987164,
"serviceType": "DYNAMIC",
"uriSpec": {
"parts": [{
"value": "scheme",
"variable": true
}, {
"value": "://",
"variable": false
}, {
"value": "address",
"variable": true
}, {
"value": ":",
"variable": false
}, {
"value": "port",
"variable": true
}]
}
}
```
### 服务节点
Zookeeper 中的服务节点是 **临时节点** ,在一定的心跳周期内若没有检测到就会删除该服务节点
### 服务消费者 -> Zookeeper
1. 新建 Moudle **cloud-zookeeper-consumer-order**
2. 修改 POM
```xml
org.springframework.cloud
spring-cloud-starter-zookeeper-discovery
org.apache.zookeeper
zookeeper
org.apache.zookeeper
zookeeper
3.7.0
org.slf4j
slf4j-log4j12
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
3. 添加 `application.proprties` 配置文件
```properties
server.port=80
spring.application.name=cloud-zookeeper-consumer-order
spring.cloud.zookeeper.connect-string=192.168.127.131:2181
```
4. 主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class ZookeeperConsumerOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ZookeeperConsumerOrderApplication.class, args);
}
}
```
5. 业务类
```java
@Configuration
public class ApplicationConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
```
```java
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
// 使用对应的服务名即可
private static final String PAYMENT_URL = "http://cloud-zookeeper-payment/";
@GetMapping("/consumer/payment/zk")
public String paymentInfo(){
return restTemplate.getForObject(PAYMENT_URL + "payment/zk", String.class);
}
}
```
6. 启动并访问 http://localhost/consumer/payment/zk
7. 也可以通过 zk 的命令行查看注册的服务

## 4.3 Consul
### 简介
- 官网:https://www.consul.io/
- 下载:https://www.consul.io/downloads
选择对应的操作系统即可,这里先以 Window 为主,实战中建议使用 Linux
- Spring Cloud Consul 怎么玩:https://www.springcloud.cc/spring-cloud-consul.html
- 说明:
Consul是一套开源的分布式**服务发现**和**配置管理**系统,由 HashiCorp 公司用 **Go** 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的**每一个都可以根据需要单独使用**,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。
它具有很多优点。包括:基于raft协议,比较简洁;支持健康检查,同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群提供**图形界面跨平台**,支持Linux、Mac、Windows。
- 作用
- 服务发现 - 提供HTTP和DNS两种发现方式。
- 健康监测 - 支持多种方式,HTTP、TCP、Docker、Shell脚本定制化
- KV存储 - Key、Value的存储方式
- 多数据中心 - Consul支持多数据中心
- 可视化Web界面
### 安装并运行 Consul
1. 下载完压缩包之后直接解压得到一个文件夹

2. 进入 cmd 窗口,输入 `consul --version` 查看版本号
3. 输入 `consul agent -dev` 即可启动并进入开发模式
4. 可以通过访问 http://localhost:8500/ 查看 consul 的管理界面

### 服务提供者 -> Consul
1. 新建 Module `cloud-consul-payment`
2. 改 POM
```xml
org.springframework.cloud
spring-cloud-starter-consul-discovery
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
3. 新建 `application.properties`
```properties
server.port=9093
spring.application.name=cloud-consul-payment
#配置 Spring Cloud Consul
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
#服务名
spring.cloud.consul.discovery.service-name=${spring.application.name}
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class ConsulPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulPaymentApplication.class, args);
}
}
```
5. 编写业务类
```java
@RestController
public class PaymentController {
@Value("${server.port}")
private String port;
@GetMapping("/payment/consul")
public String paymentForConsul(){
return "springcloud with consul: "+ port +"\t"+ UUID.randomUUID().toString();
}
}
```
6. 启动并通过 http://localhost:8500 查看注册的服务

再访问 http://localhost:9093/payment/consul 进行测试

### 服务消费者 -> Consul
1. 新建 Module `cloud-consul-consumer-order`
2. 修改 POM
```xml
org.springframework.cloud
spring-cloud-starter-consul-discovery
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
3. 添加 `application.properties`
```properties
server.port=80
spring.application.name=cloud-consul-consumer-order
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=${spring.application.name}
```
4. 创建启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class ConsulConsumerOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulConsumerOrderApplication.class, args);
}
}
```
5. 编写业务类
```java
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
private static final String CONSUL_URL = "http://cloud-consul-payment";
@GetMapping("/consumer/payment/consul")
public String paymentInfoByConsul(){
return restTemplate.getForObject(CONSUL_URL + "/payment/consul", String.class);
}
}
```
6. 启动测试
访问 http://localhost:8500 查看服务状态

访问 http://localhost/consumer/payment/consul 测试服务

## 4.4 三种注册中心的异同点
| 组件名 | 语言CAP | 服务健康检查 | 对外暴露接口 | Spring Cloud集成 |
| --------- | ------- | ------------ | ------------ | ---------------- |
| Eureka | Java | AP | 可配支持 | HTTP |
| Consul | Go | CP | 支持 | HTTP/DNS |
| Zookeeper | Java | CP | 支持客户端 | 已集成 |
**CAP**:C - 强一致性 A - 可用性 P - 分区容错性;对于一个分布式系统来说,不可能同时满足 **强一致性** 和 **可用性**

因此,根据 CAP 可以将 NoSQL 进行分类,
- CA - 单点集群,满足—致性,可用性的系统,通常在可扩展性上不太强大。
- CP - 满足一致性,分区容错的系统,通常性能不是特别高。

- AP - 满足可用性,分区容错性的系统,通常可能对一致性要求低一些。

# 第五章 服务调用
## 5.2 Ribbon
### 简介
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套 **客户端负载均衡工具**
是 Netflix 发布的开源项目,主要功能是提供**客户端的软件负载均衡算法和服务调用**
可以在配置文件中列出所有集群的节点地址,Ribbon 会自动基于某种规则连接这些机器
**和 Nginx 的区别**
- Nginx 是服务器负载均衡(**集中式 LB**),客户端的所有请求都交给 Nginx,由 Nginx 实现请求转发
- Ribbon 是本地负载均衡(**进程内 LB**),在调用微服务接口时,会在注册中心上获取注册信息服务列表缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术
负载均衡 = Ribbon + RestTemplate
### 使用
**架构说明**
Ribbon 是一个软负载均衡的客户端组件,可以和其他所需请求的客户端结合使用

Ribbon 的工作步骤
1. 第一步先通过 Eureka Server 的服务列表,根据对应的负载均衡算法找到一个服务
2. 发送对应的请求给对应的服务
**POM 依赖文件**
```xml
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
```
如果使用了 `spring-cloud-starter-netflix-eureka-client` 那么就可以不需要额外引入(依赖传递)
### Ribbon 自带的负载均衡算法
都是通过核心接口 **IRule** 的实现类完成的

RoundRobinRule 轮询(**默认的**)
RandomRule 随机
RetryRule 先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试
WeightedResponseTimeRule 对 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择
BestAvailableRule 会**先**过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
ZoneAvoidanceRule 复合判断server所在区域的性能和server的可用性选择服务器
### 替换默认的负载均衡算法
1. 在 **cloud-consumer-order** 模块下添加 `pers.dreamer07.ribbon ` 包,注意不能放在 `@ComponentScan` 所扫描的当前包下以及子包下,否则该规则就会对所有 Ribbon 客户端有效
2. 添加一个配置类,用来替换 Ribbon 默认的负载均衡算法
```java
/**
* @program: springcloudstudy
* @description: 替换默认 Ribbon 负载均衡算法
* @author: EMTKnight
* @create: 2021-05-25
**/
@Configuration
public class RibbonRuleConfig {
@Bean
public IRule iRule(){
return new RandomRule();
}
}
```
3. 修改启动类
```java
@SpringBootApplication
@EnableEurekaClient
// 当使用特定的服务时使用指定的负载均衡算法
@RibbonClient(name = "CLOUD-PAYMENT", configuration = RibbonRuleConfig.class)
public class ConsumerOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerOrderApplication.class, args);
}
}
```
4. 访问 http://localhost/consumer/payment/get/1 查看两个服务提供者的控制台打印
### Ribbon 轮询负载算法的原理
**公式:** rest 接口请求次数 % 对应的服务集群节点总数量 = 实际调用的服务下标 (每次重启后 rest 接口请求次数从 1 开始)
如:重启服务后通过 **discoveryClient.getInstances("CLOUD-PAYMENT");** 得到对应的服务实例的集合
9090 和 9091 组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:9090
当总请求数位2时:2%2=О对应下标位置为0,则获得服务地址为127.0.0.1:9091
当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:9090
当总请求数位4时:4%2=О对应下标位置为0,则获得服务地址为127.0.0.1:9091
如此类推…
### Ribbon 轮询负载算法源码
```java
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
...
/*
lb - 负载均衡器
key - 这里没用上
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
// 获取所有健康的服务
List reachableServers = lb.getReachableServers();
// 获取所有服务
List allServers = lb.getAllServers();
// 获取对应的 size
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 得到要调用服务的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
// 得到对有的服务
server = allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
private int incrementAndGetModulo(int modulo) {
// 死循环 -> 自旋锁
for (;;) {
// 获取当前使用的服务器下标
int current = nextServerCyclicCounter.get();
// 获取下一个要使用的服务器下标
int next = (current + 1) % modulo;
// 通过原子操作 compareAndSet CAS(乐观锁)判断两个值之间是否出现错误
if (nextServerCyclicCounter.compareAndSet(current, next))
// 没有就直接返回
return next;
}
}
...
}
```
### 手写 Ribbon 负载均衡算法
1. 注释 **cloud-consumer-order** 中配置类 RestTemplate 的 `@LoadBalanced` 注解
2. 在 **cloud-payment** 模块中的控制类中添加方法用来测试接口
```java
@GetMapping("/payment/port")
public String getPort(){
return port;
}
```
3. 在 **cloud-consumer-order** 的 `pers.dreamer07.springcloud` 包下创建一个 `mloadbalancer` 包
创建 **MLoadBalancer** 接口用来定义负载均衡器的规范
```java
/**
* 定义负载均衡器的规范
*/
public interface MLoadBalancer {
/**
* 从对应的服务列表中根据指定的规范获取对应的服务
* @param serviceInstanceList 服务列表
* @return 获取服务
*/
public ServiceInstance getService(List serviceInstanceList);
}
```
创建对应的实现,这里实现的是 **轮询** 负载算法
```java
/**
* @program: springcloudstudy
* @description: 使用轮询负载算法
* @author: EMTKnight
* @create: 2021-05-26
**/
@Component
@Slf4j
public class RoundRobinLoadBalancer implements MLoadBalancer{
// AtomicInteger 操作 Integer 数据的原子类
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 获取对应的 rest 接口访问的次数
* @return
*/
public int getIndex(){
// 定义变量
int current;
int next;
do {
// 获取当前值
current = atomicInteger.get();
// 获取当前值+1,但如果当前 rest 接口访问超过了 10 万次就变为 0
next = current > 100000 ? 0 : current + 1;
// 原子判断,判断是否有出现数据异常,如果出现了就重试(自旋锁)
}while (!atomicInteger.compareAndSet(current, next));
log.info("当前 rest 接口的仿瓷次数: {}", next);
return next;
}
/**
* 这里实现轮询负载算法的主要逻辑
* @param serviceInstanceList 服务列表
* @return
*/
@Override
public ServiceInstance getService(List serviceInstanceList) {
// 求余
int index = getIndex() % serviceInstanceList.size();
return serviceInstanceList.get(index);
}
}
```
4. 修改 **OrderController** 控制类
```java
package pers.dreamer07.springcloud.controller;
import com.netflix.discovery.converters.Auto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import pers.dreamer07.springcloud.mloadbalancer.MLoadBalancer;
import pers.dreamer07.springcloud.model.CommonResult;
import pers.dreamer07.springcloud.model.Payment;
import java.util.List;
/**
* @program: springcloudstudy
* @description:
* @author: EMTKnight
* @create: 2021-05-24
**/
@RestController
@Slf4j
public class OrderController {
...
@Autowired
private MLoadBalancer mLoadBalancer;
@Autowired
private DiscoveryClient discoveryClient;
...
@GetMapping("/consumer/payment/port")
public String getPaymentPort(){
// 通过 discoveryClient 获取指定服务的所有实例
List instances = discoveryClient.getInstances("cloud-payment");
if (instances != null && instances.size() <= 0){
return "当前没有可用的服务";
}
// 获取对应的实例
ServiceInstance service = mLoadBalancer.getService(instances);
// 向服务发送对应的请求得到数据并返回
return restTemplate.getForObject(service.getUri() + "/payment/port", String.class);
}
}
```
5. 启动五个微服务后访问 http://localhost/consumer/payment/port 进行测试
## 5.3 OpenFeign 服务接口调用
### 简介
之前的开发模式中,我们使用了 **Ribbon + RestTemplate** 对 http 请求进行封装处理,形成了一套模板化的调用方法。但在实际开发中,一个微服务的接口会被多处调用,所有通常会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用
**Feign** 就在以上基础上做了进一步封装,由它类定义和实现 **依赖服务** 接口
Feign 中集成了 Ribbon,只需要**自定义服务依赖接口且以声明式的方法**,就可以优雅而简单的实现服务调用
**OpenFeign VS Feign**:前者是 SpringCloud 在后者的 **基础** 上支持了 SpringMVC 的注解
OpenFeign 的 `@Feignclient` 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
### 使用
1. 新建模块 **cloud-openfeign-consumer-order** 导入以下依赖
```xml
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
2. 添加 `application.properties ` 文件
```properties
server.port=80
spring.application.name=cloud-openfeign-consumer-order
# 配置 Eureka Client
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址(集群版)
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/eureka
```
3. 创建主启动类,额外添加一个 `@EnableFeignClients` 注解表示 **开始 FeignClient ** 客户端
```java
@SpringBootApplication
@EnableFeignClients
public class OpenFeignConsumerOrder {
public static void main(String[] args) {
SpringApplication.run(OpenFeignConsumerOrder.class, args);
}
}
```
4. 编写业务类接口,如果需要使用 `@FeignClient` 的功能,就不能编写业务逻辑
```java
/**
* @program: springcloudstudy
* @description: 定义服务依赖接口
* @author: EMTKnight
* @create: 2021-05-26
**/
@Service
// 指定使用的微服务名称
@FeignClient(value = "cloud-payment")
public interface OrderService {
/**
* 使用 SpringgMVC 的注解指定要调用的服务接口
* @param id
* @return
*/
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id);
}
```
5. 编写控制器类,注入业务类后直接使用即可
```java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/consumer/payment/get/{id}")
public CommonResult getPaymentInfo(@PathVariable Long id){
return orderService.getPaymentById(id);
}
}
```
6. 启动五个微服务模块后,访问 http://localhost/consumer/payment/get/1

注意查看控制台打印(服务提供者模块),可以发现 **Feign 实现了负载均衡**
### 超时控制
1. 修改服务提供者模块的控制器
```java
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
public String port;
....
@GetMapping("/payment/port/timeout")
public String getPortTimeout(){
// 故意暂停程序
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return port;
}
}
```
2. 修改 **cloud-openfeign-consumer-order** 模块,添加业务和控制器方法
```java
@Component
// 指定使用的微服务名称
@FeignClient(value = "cloud-payment")
public interface OrderService {
...
/**
* 调用需要一定时间内才能完成的业务逻辑代码
* @return
*/
@GetMapping("/payment/port/timeout")
public String getPortTimeout();
}
```
```java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
...
@GetMapping("/consumer/payment/port/timeout")
public String getPortTimeout(){
return orderService.getPortTimeout();
}
}
```
3. 启动五个微服务模块,访问 http://localhost/consumer/payment/port/timeout

OpenFeign 中默认业务逻辑操作超时时间为 1s
4. 由于 OpenFeign 中集成了 **Ribbon** 所以可以通过其进行配置
修改 **cloud-openfeign-consumer-order** 中的 `application.properties` 文件,添加以下内容
```properties
# 连接到微服务模块后具体业务执行的超时时间
ribbon.ReadTimeout=5000
# 连接到微服务模块的超时时间
ribbon.ConnectTimeout=5000
```
5. 重启再测试即可
### 日志打印
> 对 Feign 接口的调用情况进行监控和输出
**日志级别**
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
- FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
**在 SpringBoot 中配置日志 bean**
```java
import feign.Logger;
/**
* @program: springcloudstudy
* @description:
* @author: EMTKnight
* @create: 2021-05-26
**/
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignlogger(){
return Logger.Level.FULL;
}
}
```
**application.properties** 中配置需要开启日志的 Feign 客户端
```properties
# feign 以 debug 级别监控指定的客户端
logging.level.pers.dreamer07.springcloud.service.OrderService=debug
```
测试,查看控制台打印

# 第六章 服务降级
## 6.1 Hystrix
### 简介
> 分布式系统面临的问题:复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候不可避免的失败
****
**服务雪崩**
多个微服务之间调用时,微服务 A 调用微服务 B 和微服务 C,而微服务 B 和 C 又调用其他的微服务,这就是 **扇出**
如果扇出的链路上某个微服务的调用响应时间过长/不可用,微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的**雪崩效应**
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生**级联故障**,或者叫**雪崩**。
**Hystrix**
用于处理分布式系统的**延迟和容错的开源库**,在分布式系统里,许多依赖不可避免的回调用失败(超时,异常等),而 Hystrix 就可以保证在一个依赖出现问题时,**不会导致整体服务失败,避免级联故障,从而提高分布式系统的弹性**
`断路器` 本身就是一种开关装置,当某个服务单元故障之后,通过断路器的**故障监控**,向调用方返回一个符合预期的,可处理的**备选响应(FallBack)**,而不是长时间的等待/抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
**作用**
1. 服务降级
2. 服务熔断
3. 接近实时的监控
**官网:**https://github.com/Netflix/Hystrix/wiki/How-To-Use
### 重要概念
1. 服务降级
- fallback:不让客户端等待立刻返回一个友好提示
- 哪些情况会出现降级:
1. 程序运行异常
2. 超时
3. 服务熔断触发服务降级
4. 线程池/信号量打满也会导致服务降级
2. 服务熔断:
特殊的服务降级,当调用链路中的某个微服务不可用/响应时间过长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息
当检测到该节点微服务调用正常后,恢复调用链路
3. 服务限流:严禁突然的高并发操作,如果出现,就进行限流(一秒钟 N 个,后面的排队),有序进行
### 模拟服务超时/失败
#### 1) 接入服务提供者模块
1. 创建一个 Module `cloud-hystrix-payment`
2. 添加依赖
```xml
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 编写 `application.properties` 文件
```properties
server.port=9094
spring.application.name=cloud-hystrix-payment
# 配置 Eureka Client
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址(集群版)
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class HystrixPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentApplication.class, args);
}
}
```
5. 编写业务代码
```java
@Service
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfoSuc(Long id) {
return Thread.currentThread().getName() + " - 请求成功,流水号为: " + id;
}
@Override
public String paymentInfoError(Long id) {
// 模拟业务逻辑处理
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Thread.currentThread().getName() + " - 请求失败, 流水号为:" + id;
}
}
```
```java
@RestController
public class PaymentController {
@Autowired
private PaymentService paymentService;
@GetMapping("/payment/get/suc/${id}")
public String getPaymentInfoSuc(@PathVariable Long id){
return paymentService.paymentInfoSuc(id);
}
@GetMapping("/payment/get/error/${id}")
public String getPaymentInfoError(@PathVariable Long id){
return paymentService.paymentInfoError(id);
}
}
```
6. 启动,并访问 http://localhost:9094/payment/get/suc/1 进行测试
#### 2) JMeter 测试高并发
开启 JMeter,发送 20000 个请求给 9094 端口,都去访问 `getPaymentInfoError()` 服务
当处于高并发下的情况时访问 http://localhost:9094/payment/get/suc/1 也会有延迟(响应速度变慢)
**原因:** tomcat 默认的工作线程数满了,没有多余的线程来分解压力和处理
**结论:** 如果是由 80 端口的消费者进行测试,面对这么多请求,消费者只能干等
#### 3) 接入服务消费者
1. 新建 Module `cloud-feign-hystrix-consumer-order`
2. 引入需要的 POM 依赖
```xml
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建一个 `application,properties` 文件
```properties
server.port=80
spring.application.name=cloud-feign-hystrix-consumer-order
# 配置 Eureka Client
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址(集群版)
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class FeignHystrixConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignHystrixConsumerApplication.class, args);
}
}
```
5. 编写业务逻辑
```java
@FeignClient(value = "cloud-hystrix-payment")
@Component
public interface OrderService {
@GetMapping("/payment/get/suc/{id}")
public String getPaymentInfoSuc(@PathVariable("id") Long id);
@GetMapping("/payment/get/error/{id}")
public String getPaymentInfoError(@PathVariable("id") Long id);
}
```
```java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/consumer/payment/get/suc/{id}")
public String getPaymentInfoSuc(@PathVariable Long id){
return orderService.getPaymentInfoSuc(id);
}
@GetMapping("/consumer/payment/get/error/{id}")
public String getPaymentInfoError(@PathVariable Long id){
return orderService.getPaymentInfoError(id);
}
}
```
6. 启动,访问 http://localhost/consumer/payment/get/suc/1 进行测试
7. 先通过 Jmeter 发送 20000 个请求,在通过消费者 80 端口发送请求,可以发现响应熟读变慢了
**原因**: 8001 同一层次的其他接口服务被困死,因为 tomcat 线程池里的工作线程已被基站完毕
**但也是因为有上述故障和不加表现才有了 降级/容错/限流 等技术的产生**
#### 4) 降级容错解决的维度要求
超时导致服务器变慢 -> 超时不再等待
出错 -> 要有兜底的东西
解决:
- 对方服务(9094)超时了,调用者(80)不能一直卡死等待,必须有服务降级。
- 对方服务(9094)down机了,调用者(80)不能一直卡死等待,必须有服务降级。
- 对方服务(9094)OK,调用者(80)**自己出故障**或**有自我要求**(自己的等待时间小于服务提供者),自己处理降级。
### 服务降级
#### 1) 服务提供者
1. 在对应的业务类的方法上添加 `@HystrixCommand` 注解,通过相关的属性配置,实现 **服务降级**
注意兜底方法的返回值类型和形参列表必须和对应的业务方法相同
```java
@Service
public class PaymentServiceImpl implements PaymentService {
...
/**
* {@code HystrixCommand} - 服务降级配置
* - fallbackMethod: 指定该业务方法的 fallback(兜底方法)
* - commandProperties: 配置该业务方法的一些约束
* 这里是业务的处理时长,设置为 1 秒
* - 在该业务方法中如果出现的异常/超时就会调用 fallback 方法
* @param id
* @return
*/
@Override
@HystrixCommand(fallbackMethod = "paymentInfoErrorHandler", commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
})
public String paymentInfoError(Long id) {
// 模拟业务逻辑处理
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Thread.currentThread().getName() + " - 请求失败, 流水号为:" + id;
}
/**
* paymentInfoError 的兜底方法
* @return
*/
public String paymentInfoErrorHandler(Long id){
return Thread.currentThread().getName() + ": 系统繁忙,请稍后重试";
}
}
```
2. 在主启动类上添加 `@EnableCircuitBreaker` 注解
```java
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class HystrixPaymentApplication {
```
3. 重启,访问 http://localhost:9094/payment/get/error/1

#### 2) 服务消费者
> Hystrix 建议更多的使用在服务消费者中
1. 在对应的业务类的方法上添加 `@HystrixCommand` 注解,通过相关的属性配置,实现 **服务降级**
注意不能直接和 **Feign **一起使用
```java
@HystrixCommand(fallbackMethod = "getPaymentInfoErrorHandler", commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
@GetMapping("/consumer/payment/get/error/{id}")
public String getPaymentInfoError(@PathVariable Long id){
return orderService.getPaymentInfoError(id);
}
public String getPaymentInfoErrorHandler(@PathVariable("id") Long id){
return Thread.currentThread().getName() + ": 服务器繁忙,请稍后重试";
}
```
2. 在主启动类上添加 `@EnableCircuitBreaker` 注解
3. 启动,访问 http://localhost/consumer/payment/get/error/1
#### 3) 存在问题
1. 一个方法对应一个 fallback,代码膨胀
2. fallback 和 业务代码写在一起,代码混乱
#### 4) 全局 fallback
1. 在业务类中添加 一个用于**全局 fallback**的方法
```java
public String globalInfoErrorHandler(){
return Thread.currentThread().getName() + ": global - 服务器繁忙,请稍后重试";
}
```
2. 在业务类上添加 `@DefaultProperties(defaultFallback = "全局 fallback 方法")` 注解
```java
@DefaultProperties(defaultFallback = "globalInfoErrorHandler")
public class OrderController {
```
3. `@HystrixCommand` 注解中如果需要额外定义 fallback,就指定 ==fallbackMethod== 属性,如果不需要就不指定该属性
```java
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
@GetMapping("/consumer/payment/get/error/{id}")
public String getPaymentInfoError(@PathVariable Long id){
return orderService.getPaymentInfoError(id);
}
```
4. 启动项目并再次访问

#### 5) 将业务与 fallback 进行隔离
1. 如果需要针对业务逻辑与 fallback 的解耦,可以将目标锁定在微服务调用接口 **OrderService**
针对该类中的微服务接口进行统一的 fallback
2. 创建一个 **OrderService** 接口的实现类 -> **OrderServiceFallback**
```java
@Component
public class OrderServiceFallback implements OrderService {
@Override
public String getPaymentInfoSuc(Long id) {
return Thread.currentThread().getName() + "OrderServiceFallback --> getPaymentInfoSuc";
}
@Override
public String getPaymentInfoError(Long id) {
return Thread.currentThread().getName() + "OrderServiceFallback --> getPaymentInfoError";
}
}
```
3. 在 **OrderService** 中通过 `@FeignClient` 注解的 **==fallback==** 属性指定对应的 fallback 类
```java
@FeignClient(value = "cloud-hystrix-payment", fallback = OrderServiceFallback.class)
@Component
public interface OrderService {
```
4. 修改 `application.properties` 文件,添加以下配置
```properties
feign.hystrix.enabled=true
```
5. 只启动 Eureka 集群和服务消费者模块,模拟微服务模块宕机的情况

### 服务熔断
#### 1) 简介
断路器 == 保险丝
**熔断机制概述**
是应对雪崩效应的一种**微服务链路保护机制**。当**扇出链路**的某个微服务出错不可用/响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息
==当检测到该节点微服务调用响应正常后,恢复调用链路==
在 SpringCloud 中,熔断机制通过 Hystrix 实现,它会监控微服务键调用的状况,当失败的调用到一定阈值(默认是5秒内20次调用失败),就会启动熔断机制(也是通过 `@HystrixCommand` 注解)
[Martin Fowler 相关论文](https://martinfowler.com/bliki/CircuitBreaker.html)
#### 2) 使用
1. 修改 **cloud-hystrix-payment** 模块中的业务逻辑,对指定的微服务接口开启服务熔断
```java
@Service
public class PaymentServiceImpl implements PaymentService {
...
//=====服务熔断
@Override
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback",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) {
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
}
```
```java
@GetMapping("/payment/circuit/breaker/{id}")
public String circuitBreaker(@PathVariable Long id){
return paymentService.paymentCircuitBreaker(id);
}
```
2. 启动项目,根据设置的值进行相关的测试
3. 关于 `@HystrixCommand` 注解中 ==commandProperties== 属性的配置
| 参数名 | 值 | 注解 |
| --------------------------------------------------- | ------ | ------------------------------------------------------------ |
| execution.isolation.strategy | THREAD | 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离 |
| execution.isolation.semaphore.maxConcurrentRequests | 10 | 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数) |
| execution.isolation.thread.timeoutinMilliseconds | 10 | 配置命令执行的超时时间 |
| execution.timeout.enabled | true | 是否启用超时时间 |
| execution.isolation.thread.interruptOnTimeout | true | 执行超时的时候是否中断 |
| execution.isolation.thread.interruptOnCancel | true | 执行被取消的时候是否中断 |
| fallback.isolation.semaphore.maxConcurrentRequests | 10 | 允许回调方法执行的最大并发数 |
| fallback.enabled | true | 服务降级是否启用,是否执行回调函数 |
| circuitBreaker.enabled | true | 是否启用断路器 |
| circuitBreaker.requestVolumeThreshold | 20 | 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。 |
| circuitBreaker.errorThresholdPercentage | 50 | 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 `circuitBreaker.requestVolumeThreshold` 的情况下,如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。 |
| circuitBreaker.sleepWindowinMilliseconds | 5000 | 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。 |
| circuitBreaker.forceOpen | false | 断路器强制打开 |
| circuitBreaker.forceClosed | false | 断路器强制关闭 |
| metrics.rollingStats.timeinMilliseconds | 10000 | 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间 |
| metrics.rollingStats.numBuckets | 10 | 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常 |
| metrics.rollingPercentile.enabled | false | 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。 |
| metrics.rollingPercentile.timeInMilliseconds | 60000 | 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 |
| metrics.rollingPercentile.numBuckets | 60000 | 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。 |
| metrics.rollingPercentile.bucketSize | 100 | 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。 |
| metrics.healthSnapshot.intervalinMilliseconds | 500 | 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。 |
| requestCache.enabled | true | 是否开启请求缓存 |
| requestLog.enabled | true | HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中 |
==threadPoolProperties== 属性的配置
| 参数名 | 值 | 注释 |
| --------------------------- | ---- | ------------------------------------------------------------ |
| coreSize | 10 | 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量 |
| maxQueueSize | -1 | 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,否则将使用 LinkedBlockingQueue 实现的队列。 |
| queueSizeRejectionThreshold | 5 | 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。 |
#### 3) 总结
**熔断类型**
1. 关闭:不进行服务熔断
2. 半开:部分请求根据规则调用当前服务,如果**请求成功且符合规则**则认为当前服务恢复正常,关闭熔断
3. 打开:请求不会再进行服务调用,内部设置时钟一般为**MTTR(平均故障处理时间)**,当打开时长达到所设时钟则进入**半熔断状态。**
**官网断路器流程图**

工作步骤
1. Assuming the volume across a circuit meets a certain threshold : 假设一个电路的请求量满足一定的阈值
2. HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
And assuming that the error percentage, as defined above exceeds the error percentage defined in : 并假设上面定义的错误百分比超过下面定义的错误百分比
3. HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
Then the circuit-breaker transitions from CLOSED to OPEN. 然后断路器从闭合过渡到断开
While it is open, it short-circuits all requests made against that circuit-breaker. 当断路器开启时,它会短路所有对断路器提出的要求
After some amount of time ( HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next request is let through. If it fails, the command stays OPEN for the sleep window. If it succeeds, it transitions to CLOSED and the logic in 1) takes over again. ) ,过了一段时间,下一个请求便会通过。如果失败,则该命令对于睡眠窗口保持 OPEN 状态。如果它成功了,它就会转换到 CLOSED,然后1)中的逻辑再次接管
**断路器开启依据**
1. **快照时间窗:** 以指定的时间范围内对请求数据和失败率进行一次统计
2. **请求总数阀值** + **错误百分比阀值**:在 **快照时间窗** 内请求数量达到了**请求总数阀值**且失败率达到了**错误百分比阀值**就会开启服务熔断
**断路器开启/关闭的时间**
1. 请求总数阀值 + 失败次数阀值 达到
2. 请求总数阀值 + 错误百分比阀值 达到
3. 开启时任何请求都会被执行,而是执行对应的 `fallback` 服务降级
4. 断路器**打开之后**,hystrix 会启动一个**休眠时间窗**,在这个时间窗内,**降级逻辑是临时的成为主逻辑**
当休眠时间窗到期,断路器将进入**半开**状态,释放**一次请求到原来的主逻辑**上,如果此次请求**正常返回**,那么断路器将闭合,主逻辑恢复,如果这次请求**依然有问题**,断路器继续进入打开状态,休眠时间窗重新计时。
### 服务限流
将在 SpringCloud Alibaba 中的 Sentinal 说明
### 工作流程
**官方示意图**

**具体步骤**
1. 创建 **HystrixCommand**(依赖的服务返回单个操作结果) / **HystrixObserableCommand**(依赖的服务返回多个操作结果) 对象
2. 命令执行
- HystrixCommand 实现了两种方式 `execute()` 和 `queue()`
前者是同步执行,从依赖的服务返回一个单一的结果对象/在发生错误时抛出异常
后者是异步执行,直接返回一个 **Future** 对象,其中包含了执行结束时要返回的单一结果对象
- HystrixObserableCommand 实现了两种方式 `observer()` 和 `toObservable()`
前者返回 **Observable** 对象,它代表了操作的多个结果,它是一个 ==Hot Observable== (不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程)。
后者返回 **Observable** 对象,也代表了操作的多个结果,但它返回的是一个 ==Cold Observable==(没有“订间者”的时候并不会发布事件,而是进行等待,直到有“订阅者"之后才发布事件,所以对于Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
3. 判断当前命令的请求缓存功能是否被启用,如果该命令缓存名字,那么缓存的结果会立即以 Observable 对象的形式返回。
4. 判断**断路器是否打开**,如果打开,就执行第 8 步,如果关闭,就往下走
5. 判断线程池/请求队列信号量是否占满,如果命令依赖服务的专用**线程池/请求队列/信号量已被占满**,那么跳转到第 8 步
如果没有就进行执行
6. Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。
- HystrixCommand.run():返回一个单一的结果,或者抛出异常。
- HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
7. Hystrix 会将依赖服务的执行结果告诉熔断器,断路器会维护一组**计数器**来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行**"熔断/短路"**。
8. fallback() **服务降级**处理:如果在第 7 步中的执行出现了异常,也会来到这里
9. 如果命令执行成功,就将 处理结果/Observable对象 返回
**tips:**如果我们**没有为命令实现降级逻辑**或者在**降级处理逻辑中抛出了异常**,Hystrix依然会运回一个Obsevable对象,但是它不会不含任何结果数惯,而是通过 onError 方法通知命令立即中断请求,并通过 onError 方法将引起命令失败的异常发送给调用者。
### 服务监控 HystrixDashboard
#### 1) 搭建
1. 创建一个新 Module `cloud-hystrix-dashboard`
2. 修改 POM 文件
```xml
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
org.springframework.boot
spring-boot-starter-actuator
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `application.properties` 配置文件
```properties
server.port=9004
```
4. 创建主启动类
```java
/**
* @program: springcloudstudy
* @description:
* {@code @EnableHystrixDashboard} 开启 HystrixDashboard
* @author: EMTKnight
* @create: 2021-05-27
**/
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
```
5. 启动项目,访问 http://localhost:9004/hystrix

注意:需要被监控的微服务模块都需要添加以下依赖
```xml
org.springframework.boot
spring-boot-starter-actuator
```
6. 在需要监控的微服务模块的主启动类添加以下代码
```java
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*否则,Unable to connect to Command Metric Stream 404
*/
@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;
}
```
7. 启动 Eureka 集群和服务提供者
8. 在 Hystrix Dashboard 中添加以下参数

如果出现 **Unable to connect to Command Metric Stream.** 可以参考[这里](https://blog.csdn.net/Coufran/article/details/108107952)
9. 访问对应的服务熔断接口后查看 Hystrix Dashboard 监控界面

- 七色:对应的处理类型
- 一圈:
颜色的变化表示该熔断接口的健康程度,健康度从绿色<黄色<橙色<红色递减。
**同时流量越大实心圆越大**
- 一线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
- 整图说明


# 第七章 服务网关
## 7.2 GateWay
### 简介
**Spring Cloud 官方架构图:**

Gateway 在 Spring 生态系统之上构建的 API 网关服务,**基于 Spring5,SpringBoot 2 和 Project Reactor** 等技术
目标就是可以代替 1.x 版本中的 Zuul,而为了提升网关的性能,**SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的 Reacto r模式通信框架Netty**。
Spring Cloud Gateway 可以提供统一的路由方式且基于 **Filter链** 的方式提供了**网关基本的功能**
**作用:**
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- .....
**微服务架构中网关的位置:**

### 非阻塞异步模型
#### 1) 为何选 gateway
1. netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。
2. Zuul 1.0 进入了维护阶段,且 gateway 是由 springcloud 团队研发的
3. Gateway是**基于异步非阻塞模型**上进行开发的
#### 2) 特性
1. 基于Spring Framework 5,Project Reactor 和 Spring Boot 2.0 进行构建;
2. 动态路由:能够匹配任何请求属性;
3. 可以对路由指定 Predicate (断言)和 Filter(过滤器);
4. 集成 Hystrix 的断路器功能;
5. 集成 Spring Cloud 服务发现功能;
6. 易于编写的 Predicate (断言)和Filter (过滤器);
7. 请求限流功能;
8. 支持路径重写。
#### 3) 和 Zuul 的区别
1. Zuul 1.x 是一个基于阻塞式 I/O 的 API Gateway
2. Zuul 1.x 是基于 Servlat 2.5 使用阻塞架构,不支持任何长连接(WebSocket)
Zuul 的设计模式和 Nginx 较像,每次I/О操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第-次加载较慢的情况,使得Zuul的性能相对较差。
3. Spring Cloud Gateway 建立在 Spring Framework 5、Project Reactor和Spring Boot2之上,使用非阻塞API。
4. Spring Cloud Gateway 还支持 WebSocket,且与 Spring 紧密集成拥有更好的开发体验
#### 4) Zuul1.x 模型
采用 Tomcat 作为容器,使用传统的 Servlet IO 处理模型
**Servlet 的生命周期:**
1. container 启动时构造 servlet 对象并调用 `servert init()` 进行初始化
2. container 运行时接收请求,为每个请求分配一个线程,任何再调用 service
3. container 关闭时调用 `servlet destory()` 销毁 servlet

- 缺点:
Servlet是一个简单的网络 IO 模型,当请求进入 Servlet container 时,Servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(如抽风用Jmeter压),线程数量就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下 servlet 模型没有优势。
#### 5) Gateway 模型
传统的 Web 框架(Struts2, SpringMVC)等都是基于 Servlet API 与 Servlet 容器基础之上运行的
但 Servlet3.1 后有了 **异步非阻塞** 的支持,而 **WebFlux 是一个典型非阻塞异步框架**,核心是基于 Reactor 的 API 实现的
Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要**依赖Servlet APl**,它是**完全异步非阻塞的**,并且基于 **Reactor** 来实现 **响应式流规范**。
### 工作流程
#### 1) 基本概念
1. 路由:路由是构建网关的**基本模块**,由 ID,目标 URI,一系列**断言和过滤器**组成(比如断言为 true 时匹配该路由)
2. 断言:参考 Java8 中的 **Predicate** ,开发人员可以匹配 http 请求中所有内容(如请求头或请求参数),如果请求与断言相匹配则进行路由
3. 过滤:Spring 宽假中的 Gateway 实例,使用过滤器,可以在请求前后对其进行修改

- web 请求:通过一些匹配条件(predicate),定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化的控制
- filter: 就可以理解为一个无所不能的拦截器。有了这两个元素,再加上uri,就可以实现一个具体的路由了。
#### 2) 工作流程

客户端向Spring Cloud Gateway发出请求。然后在**Gateway Handler Mapping**中找到与**请求相匹配**的路由,将其发送到Gateway Web Handler。
Handler 再通过指定的**过滤 器链**来将**请求发送到我们实际的服务执行业务逻辑**,然后返回。过滤器之间用虚线分开是因为过滤器可能会再发送代理请求之前(Pre)或之后(post)执行业务逻辑。
**核心逻辑:**路由转发 + 执行过滤器链
### 构建基础模块
1. 创建一个新模块 **cloud-gateway**
2. 导入 POM 依赖
```xml
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `application.properties` 文件
```properties
server.port=9527
spring.application.name=cloud-gateway
# 配置路由
# 路由的 ID,没有固定规则但要求唯一建议配合服务名
spring.cloud.gateway.routes[0].id=payment_get_id
# 匹配后提供的服务路由地址
spring.cloud.gateway.routes[0].uri=http://localhost:9090
# 断言规则,断言,路径相匹配的进行路由
spring.cloud.gateway.routes[0].predicates[0]=Path=/payment/get/**
spring.cloud.gateway.routes[1].id=payment_discovery
spring.cloud.gateway.routes[1].uri=http://localhost:9090
spring.cloud.gateway.routes[1].predicates[0]=Path=/discovery/**
## 注册到 Eureka Server 中
eureka.client.register-with-eureka=true
## 是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
eureka.client.fetch-registry=true
## 配置 Eureka Server 服务端的访问地址(集群版)
eureka.client.service-url.defaultZone=http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
```
5. 启动 Eureka 集群和 Gateway 和服务提供者微服务模块
访问 http://localhost:9527/payment/get/1

访问 http://localhost:9527/discovery/service
### 通过编码的方式实现路由配置
1. 新建一个 **RouterConfig** 配置类,
```java
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder){
RouteLocatorBuilder.Builder routes = builder.routes();
// 配置多个路由
routes
// 配置路由
.route(
// 配置该路由的唯一标识
"FR_Linkage",
// 配置该路由的 断言(predicate) 和 路由服务接口地址(uri)
predicateSpec -> predicateSpec.path("/guonei").uri("http://news.baidu.com"))
.build();
return routes.build();
}
```
2. 启动项目,访问 http://localhost:9527/guonei

### 动态路由
基于微服务名称实现动态路由,从注册中心中获取对应的微服务列表,再使用内置的负载均衡实现访问
1. 启动 Eureka 和服务提供者集群
2. 修改 **cloud-gateway** 的 `application.properties` 配置
```properties
#spring.cloud.gateway.routes[0].uri=http://localhost:9090
spring.cloud.gateway.routes[0].uri=lb://CLOUD-PAYMENT
#spring.cloud.gateway.routes[1].uri=http://localhost:9090
spring.cloud.gateway.routes[1].uri=lb://CLOUD-PAYMENT
```
3. 访问 http://localhost:9527/payment/get/1
查看两个服务提供者模块的控制台是否为轮询输出
### Predicate 的使用
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories

GateWay 将路由匹配作为 Spring WebFlux HandlerMapping 基础架构的一部分。Spring Cloud GateWay 会内置许多 Route Predicate 工厂,而这些工厂都对应的 HTTP 请求的不同属性,多个工厂之间也可以自由组合
**使用**
1. the-after-route-predicate-factory:[After]
在指定的时间之后才可以访问,对应的时间格式可以通过 **ZonedDateTime now = ZonedDateTime.now();** 获取
```properties
spring.cloud.gateway.routes[0].predicates[0]=Path=/payment/get/**
spring.cloud.gateway.routes[0].predicates[1]=After=2021-05-27T15:19:37.771+08:00[Asia/Shanghai]
```

2. the-before-route-predicate-factory:[Before]
在指定的时间之前才可以访问,使用方法和 **the-after-route-predicate-factory** 一样
3. the-between-route-predicate-factory:[Between]
在指定的时间访问内才可以访问,使用方式和前两个一样
```properties
spring.cloud.gateway.routes[0].predicates[0]=Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
```
4. the-cookie-route-predicate-factory:[Cookie]
只有携带对应的 Cookie 才可以访问,前一个值为 cookie name,后一个为值的**正则表达式**
```properties
# 前一个参数为对应的 cookie 名,后一个为正则表达式
spring.cloud.gateway.routes[0].predicates[1]=Cookie=username,master
```
通过 **curl** 进行访问
```shell
curl http://localhost:9527/payment/get/1 --cookie "username=master"
```
5. the-header-route-predicate-factory:[Header]
只有携带对应的请求头才可以访问,使用方式和 Cookie 相似(前一个值为请求头 key 名,后一个为值的正则表达式)
```shell
curl http://localhost:9527/payment/get/1 --H "请求头key:请求头value"
```
6. the-host-route-predicate-factory:[Host]
只有符合规则的 Host 发起的才允许访问
```properties
spring.cloud.gateway.routes[0].predicates[0]=Host=**.somehost.org,**.anotherhost.org
```
7. the-method-route-predicate-factory:[Method]
只有通过指定的请求方式访问才允许访问
```properties
spring.cloud.gateway.routes[0].predicates[0]=Method=GET,POST
```
8. 等等(偷懒(\*\^▽^*)
**总结:** Predicate 就是为了实现一组匹配规则,让请求过来的符合规则的路径可以找到对应的路由进行处理
### Filter 的使用
**简介**
路由过滤器只能用来修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用
而 Spring Cloud 内置了多种路由过滤器,它们由 GatewayFilter 的工厂类来产生
**生命周期:** pre(之前),post(之后)
**种类:**
- GatewayFilter - 有31种(局部)
- GlobalFilter - 有10种(全局)
**自定义全局路由过滤器**
> 可以实现:全局日志记录,同一网关鉴权
```java
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @program: springcloudstudy
* @description: 全局日志记录
* @author: EMTKnight
* @create: 2021-05-27
**/
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
/**
* 过滤器的具体逻辑
* @param exchange
* @param chain
* @return
*/
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("new request uri: {}", exchange.getRequest().getURI());
String useranme = exchange.getRequest().getQueryParams().getFirst("useranme");
if (useranme == null || "".equals(useranme)){
log.warn("username is null");
// 如果数据不合法,设置对应的响应码并返回响应
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
// 如果数据合法,就进入下一个过滤器
return chain.filter(exchange);
}
/**
* 返回当前拦截器的优先级,数值越小越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
```


# 第九章 服务配置
## 9.1 Config 分布式配置中心
### 简介
**分布式系统面临的配置问题:**
微服务是需要将单体应用拆分成多个子服务的,每个服务的粒度相对较小,因此系统中会出现大量的服务,又由于每个服务都需要必要的配置信息才能运行,所以一套 **集中式,动态的配置管理** 设施是不可少的
**Spring Cloud Config:**

为微服务提供集中化的外部配置支持,配置服务器(Config Server)为各个不同的微服务的所有环境提供了一个中心化的外部配置
**使用:** SpringCloud Config 分为**客户端和服务端**两部分
**服务端**也称**分布式配置中心**,也是一个独立的微服务应用,用来连接 Config Server 并为客户端提供获取配置信息,加密/解密信息等访问接口
**客户端**是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容。并在启动时从配置中心获取和加载配置信息
配置中心默认采用 git 来存储配置信息,有助于环境配置进行版本管理,并且可以通过 git 客户端来方便的管理和访问配置内容
**作用:**
1. 集中管理配置文件
2. 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
4. 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
5. 将配置信息以 REST 接口的形式暴露 - post/crul访问刷新即可…
**与 Github 整合部署:** 由于 SpringCloud Config 默认使用 Git 来存储配置文件
### 搭建配置总控中心
1. 在 gitee 上创建一个新仓库 https://github.com/Dreamer-07/cloud-config
2. 在本地硬盘上新建 git 仓库并 **clone**
3. 切换两条分支 `dev/master` 分别创建三个环节下的文件 `test/prod/dev`
```yaml
config
info: "{分支名} branch, springcloud-config application-{开发环境}.yml version=1"
```
4. 新建 Module `cloud-config-center`
5. 修改 POM 添加以下依赖
```xml
org.springframework.cloud
spring-cloud-config-server
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
6. 添加 `application.yml`
```yaml
server:
port: 2233
spring:
application:
name: cloud-config-center
#连接中心化的外部配置
cloud:
config:
server:
git:
uri: https://gitee.com/dreamer-07/spring-cloud-conifg-server-study.git # gitee 仓库的地址
search-paths:
- cloud-config # git 搜索目录
label: master # 默认读取的分支
#配置 Eureka
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
7. 创建主启动类
```java
/**
* @program: springcloudstudy
* @description: 配置服务器, {@code @EnableConfigServer} 开启配置服务器
* @author: EMTKnight
* @create: 2021-05-27
**/
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ServerConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ServerConfigApplication.class, args);
}
}
```
8. 启动项目 http://localhost:2233/application-prod.yml

### 配置读取规则
参考文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/#_quick_start
如果不指定 {label} 就使用默认
1. `/{label}/{application}-{profile}.yml`(推荐)
2. `/{application}-{profile}.yml`
3. `/{application}/{profile}[/{label}]`
### 搭建客户端
1. 新建 Module `cloud-config-client`
2. 修改 POM 文件
```xml
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 在 `/resoucres` 目录下创建一个 **bootstrap.yml** 文件
**application.yml** 是用户级别的资源配置项,**bootstrap.yml** 是系统级别的资源配置项(**优先级更高**)
Spring Cloud 会创建一个 Bootrap Context 作为 Spring 应用 Application Context 的**父级上下文**
初始化时,Bootstrap Context 负责从**外部源加载配置属性并解析配置**。这两个上下文共享一个从外部获取的 Environment。
Bootstrap 属性有高优先级,默认情况下,它们**不会被本地配置**覆盖。Bootstrap context 和 Application Context 有着不同的约定,所以新增了一个 bootstrap.yml 文件,保证 Bootstrap Context 和 Application Context 配置的分离。
要将 Client 模块下的 application.yml 文件改为 bootstrap.yml ,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。
```yaml
server:
port: 9095
spring:
application:
name: cloud-config-client
cloud:
# 客户端配置
config:
# 分支
label: master
# 文件名
name: application
# 环境
profile: dev
# 配置中心地址
uri: http://localhost:2233
# 以上四个属性配置加起来就是 http://localhost:2233/master/application-dev.yml
# Eureka 客户端配置
eureka:
client:
service-url:
defaultZone: http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
```
5. 创建业务类
```java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: springcloudstudy
* @description:
* @author: EMTKnight
* @create: 2021-05-28
**/
@RestController
public class ConfigController {
@Value("${config.info}")
public String info;
@GetMapping("/get/config/info")
public String getConfigInfo(){
return info;
}
}
```
6. 启动 Eureka 集群和配置中心在启动客户端,访问 http://localhost:9095/get/config/info

### 动态刷新
**分布式配置的动态刷新问题:**
1. 在 gitee 上修改对应的 `master/application.dev` 配置文件

2. 通过配置服务器访问 http://localhost:2233/master/application-dev.yml

3. 通过客户端访问 http://localhost:9095/get/config/info

可以发现配置客户端读取的配置还是旧的,此时只能通过重启微服务/**动态刷新**才可以读取最新的配置
**动态刷新:**
1. 修改客户端 POM,确保引入以下配置
```xml
org.springframework.boot
spring-boot-starter-actuator
```
2. 修改 `bootstrap.yml`
```yaml
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*
```
3. 在业务类上添加 `@RefreshScope` 注解
```java
@RestController
@RefreshScope
public class ConfigController {
```
4. 重启再进行测试
1. **运维人员**在 gitee 上修改远程配置后,需要通过 ==POST== 请求发送到 http://localhost:9095/actuator/refresh 刷新客户端服务

2. 再次测试即可

**存在的问题**
1. 手动刷新,如果有太多的微服务客户端,运维人员工作量加大
2. 需要特定化更新(大部分更新,但小一部分微服务不更新)
3. 能否做到 **类似于广播,一次通知,处处生效?** -> Bus 消息服务总线
# 第十章 服务总线
## 10.1 Spring Cloud Bus
### 简介
Spring Cloud Bus 配合 Spring Cloud Config 实现配置的动态刷新
**工作示意图:**


Spring Cloud Bus 是用来将 **分布式系统的节点** 与 **轻量级消息系统** 链接起来的框架,它整合了 Java 的事件处理机制和消息中间件的功能。
Spring Cloud Bus 目前仅支持 RabbitMQ 和 Kafka(这里选择 RabbitMQ)
**作用:**
管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改,事件推送等,也可以当做**微服务之间的通信通道**
**总线:**
在微服务架构的系统中,通常会使用**轻量级的消息代理**来构建一个**共用的消息主题**,并让系统中所有微服务实例都连接上来。由于该主题中产生的**消息会被所有实例监听和消费**,所以称它为**消息总线**。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
**基本原理:**
ConfigClient 实例都监听 MQ 中同一个 topic (默认是Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置。
### 模块构建
> 确保具有 RabbitMQ 环境优先
**新建一个模块:**
1. 新建 Module `cloud-config-client2`
2. 修改 POM
```xml
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `bootstrap.yml`
```yaml
server:
port: 9096
spring:
application:
name: cloud-config-client
cloud:
# 客户端配置
config:
# 分支
label: master
# 文件名
name: application
# 环境
profile: dev
# 配置中心地址
uri: http://localhost:2233
# 以上四个属性配置加起来就是 http://localhost:2233/master/application-dev.yml
# Eureka 客户端配置
eureka:
client:
service-url:
defaultZone: http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class ConfigClient2Application {
public static void main(String[] args) {
SpringApplication.run(ConfigClient2Application.class, args);
}
}
```
5. 编写业务类
```java
@RestController
@RefreshScope
public class ConfigController {
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String configInfo()
{
return "serverPort: "+serverPort+"\t\n\n configInfo: "+configInfo;
}
}
```
6. 启动访问进行测试
### 设计思想
1. 利用消息总线触发一个客户端的 `/bus/refresh` 从而刷新所有客户端配置

2. 利用消息总线触发配置服务器(Config Server)的 `/bus/refresh` ,从而刷新所有客户端的配置

对比起第一种方式,第二种方式更合适:
1. 打破了微服务的**职责单一性**,因为微服务本身是**业务模块**,它本不应该承担配置刷新的职责。
2. 破坏了微服务各节点的对等性(同一个服务的各个节点应该负责的东西是一致的)
3. 具有局限性(微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改)
### 动态刷新全局广播
1. 在配置中心和对应的客户端中引入 Spring Cloud 对 RabbitMQ 的支持
```xml
org.springframework.cloud
spring-cloud-starter-bus-amap
```
2. 修改配置中心的 `application.yml`
```yaml
spring:
...
# 添加 Spring 整合 rabbitmq 的相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
...
# rabbitMQ 的相关配置(暴露 bus 刷新配置的端点)
management:
endpoints:
web:
exposure:
include: "bus-refresh"
```
3. 修改客户端的 `boostrap.yml` 配置文件
```yaml
spring:
...
# 添加 Spring 整合 rabbitmq 的相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
...
# rabbitMQ 的相关配置(暴露 bus 刷新配置的端点)
management:
endpoints:
web:
exposure:
include: '*'
```
4. 启动 Eureka 集群和三个微服务进行测试
5. 修改 gitee 上对应的文件

运维通过 curl 发送刷新指令给配置中心
```shell
curl -X POST http://localhost:2233/actuator/bus-refresh
```
**一次修改,广播通知,处处生效**
6. (不需要重启)访问两个客户端的接口


### 动态刷新定点通知
1. 通过 curl 发送刷新指令时 url 后面指定即可
```shell
curl -X POST http://localhost:2233/actuator/bus-refresh/服务名:端口号[]
```
2. 修改 gitee 上的文件,只通知 9095 不通知 9096
```shell
curl -X POST http://localhost:2233/actuator/bus-refresh/cloud-config-client:9095
```


### 服务总结

1. 配置中心启动后,订阅 topic(**springCloudBus**) 同时读取远程服务器上的配置文件

2. 客户端启动后,订阅相同的 topic 且与配置中心指定的配置文件进行合并
3. 运维人员修改配置文件,推送到 gitee 后发送刷新指令到配置中心
4. 配置中心通过 rabbitmq 发布一个 **refresh event(刷新事件)**
5. 客户端监听刷新事件,重新与配置中心指定的配置文件进行合并
# 第十一章 消息驱动
## 11.1 Stream
### 简介
官方文档:https://docs.spring.io/spring-cloud-stream/docs/3.1.3/reference/html/
中文文档:https://www.springcloud.cc/spring-cloud-dalston.html#_spring_cloud_stream
对于常见的消息中间件,如果都学习就会导致学习&开发成本过大,可以通过 cloud stream 屏蔽底层消息中间件的差异,降低切换成本,统一消息的**编程模型**。
官方定义:**一个构建消息驱动微服务的框架**
应用程序通过 inputs 和 outputs 和 Spring Cloud Stream 中 **binder** 对象交互
Spring Cloud Steam 的 **binder 对象**负责与消息中间件交互,Spring Integration 来连接**消息代理中间件以实现消息事件驱动**
同时 Spring Cloud Stream 为一些供应商的消息中间接产品提供了 **个性化的自动配置实现**,引用了 **发布-订阅,消费组,分区**三个概念核心
但目前只支持 ==RabbitMQ, Kafka==
### 设计思想
**标准 MQ:**

- 生产者/消费组之间通过 **消息** 作为媒介传递信息内容
- 消息必须走特定的通道 - 消息通道 Message Channel
- 通过消息通道 MessageChannel 的子接口 SubscribableChannel 进行消费
**为何使用 Cloud Stream:**
例如 RabbitMQ 和 Kafka 这两个消息中间件的架构不同(RabbitMQ 中 exchange 和 Kafka 中的 Topic 是一个东西)
如果在项目中同时使用(JavaEE 和 大数据),这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候 **Spring Cloud Stream 给我们提供了—种解耦合的方式。**
**Stream 如何实现:**
通过定义**绑定器(binder)**作为中间层,可以实现应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的==Channel==通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
**Binder 相关概念:**
- INPUT 对应于消费者
- OUTPUT 对应于生产者

> Stream 中的消息通信方式遵循了 **发布-订阅模式**,通过 Topic 主题进行广播( RabbitMQ中的 Exchange )
### 编写流程

- 通过 **Binder** 连接中间件,屏蔽差异
- **Channel** 是消息通讯系统中实现存储和转发的媒介,也是队列 Queue 的一种抽象,用过其进行对队列的配置
- Source 和 Sink 简单的可理解为参照对象是 Spring Cloud Stream 自身,从 Stream **发布消息就是输出,接受消息就是输入**。
**编码 api 和常用注解**

****
### 生产者模块
1. 新建一个 Module `cloud-stream-rabbitmq-provider`
2. 修改 POM
```XML
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建一个 `application.yml` 文件
```yaml
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:
client:
service-url:
defaultZone: http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
instance:
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class StreamRabbitmqProviderApplication {
public static void main(String[] args) {
SpringApplication.run(StreamRabbitmqProviderApplication.class, args);
}
}
```
5. 编写消息生产者业务逻辑
```java
/**
* @program: springcloudstudy
* @description: 定义消息生产者接口
* @author: EMTKnight
* @create: 2021-05-31
**/
public interface IMessageProvider {
public String send();
}
```
```java
@EnableBinding(Source.class) // 定义消息的通信管道
@Slf4j
public class MessageProviderImpl implements IMessageProvider {
// 注入消息发送管道
@Autowired
private MessageChannel output;
@Override
public String send() {
String uuid = UUID.randomUUID().toString();
// 向管道中发送消息
output.send(MessageBuilder.withPayload(uuid).build());
log.info("send channel: {}", uuid);
return uuid;
}
}
```
6. 编写控制器类
```java
@RestController
public class MessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/send/message")
public String sendMessage(){
return messageProvider.send();
}
}
```
7. 启动 Eureka 集群和 RabbitMQ 以及生产者模块
访问 http://localhost:8801/send/message 后查看 MQ 管理界面

### 消费者模块
1. 新家 Module `cloud-stream-rabbitmq-consumer`
2. 修改 POM,引入以下依赖
```xml
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建一个 `application.yml` 文件
```yaml
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: # 在 Spring Cloud Stream 中,输入就是接收消息
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://www.eureka7001.com:7001/eureka,http://www.eureka7002.com:7002/
instance:
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableEurekaClient
public class StreamRabbitmqConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(StreamRabbitmqConsumerApplication.class, args);
}
}
```
5. 创建一个监听器,负责处理中间件信息
```java
@Component
@EnableBinding(Sink.class)
@Slf4j
public class MessageListener {
@Value("server.port")
private String port;
@StreamListener(Sink.INPUT)
public void input(Message message){
String payload = message.getPayload();
log.info("stream consumer input: {}, port: {}", payload, port);
}
}
```
6. 启动四个微服务模块,访问 http://localhost:8801/send/message 后查看消费者模块控制台打印

注意:到此为止,未显式用过任何一个 RabbitMQ 的 API
### 重复消费与持久化
**重复消费:**
1. 重新创建一个和 8802 相同的模块并启动
2. 访问 http://localhost:8801/send/message 查看消费者模块的控制台打印
可以发现应该作为一个集群中的两个模块,同时接受到了生产者提供的消息,导致 **重复消费**
3. Spring Cloud Stream 默认会为不同的消费者分到不同的组中(不同组存在重复消费)
但如果可以让同一个模块的消费分到同一个组,就会产生竞争关系,能够保证**一个消息只被消费一次**

**分组:**
1. 修改两个消费者模块的 yml 文件,添加以下配置
```yaml
spring:
...
cloud:
stream:
...
bindings: # 服务的整合处理
input: # 在 Spring Cloud Stream 中,输入就是接收消息
...
group: byqtxdy # 设置组名
```
2. 重启,通过生产者再次发送消息, 观察两个消费者模块的控制台打印
、
**同一个组的多个微服务实例,一个消息每次只会有一个拿到**
**持久化:**
配置 ==group== 就会开启持久,在消费者宕机期间,生产者发送的消息,都会再消费者重启之后的重启进行消费
不会导致**信息丢失**
# 第十二章 链路追踪
## 12.1 Sleuth
### 简介
在微服务框架中,一个由客户端发起的请求在后端系统中会**经过多个不同的的服务节点**调用来协同产生最后的请求结果,每一个前段请求都会形成一条**复杂的分布式服务调用链路**,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。

而 Spring Cloud Sleuth 就可以通过一套完整的解决方案
分布式系统中提供追踪解决方案并兼容支持了 ZIPKIN(Sleuth 负责收集链路信息,Zipkin 负责展示)
### 搭建 Zipkin
1. 下载对应 zipkin-server 的相关 jar 包
访问 https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec 即可
2. 通过 cmd 执行 jar 包

3. 访问 http://localhost:9411/zipkin/ 查看对应的图形化界面

4. 术语:
- Trace:一条请求链路,有对应的唯一标识,由多个 span 构成
- Span:调用链路来源,也就是一次服务请求信息,各个 span 之间用一个 parent id 关联起来
### 链路监控展示
1. 基于原生的 cloud-payment 和 cloud-consumer-order 模块,导入以下依赖
```xml
org.springframework.cloud
spring-cloud-starter-zipkin
```
2. 修改 `application.properties` 文件,添加以下配置
```properties
# 配置 Sleuth 链路追踪
## 配置链路信息发送的目标网址
spring.zipkin.base-url=http://localhost:9411
## 采样率值介于 0 到 1 之间,1 则表示全部采集
spring.sleuth.sampler.probability=1
```
3. 启动 Eureka 集群和两个服务
4. 访问 http://localhost/consumer/payment/get/1 后查看 http://localhost:9411/



# 第十三章 SpringCloud Alibaba
**文档:**
- 中文文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
- 英文文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
**组件:**
- **Sentinel**:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- **Nacos**:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
- Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
- **Seata**:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
**作用:**
- 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
**POM 依赖:**
导入到对应的父工程中
```xml
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.5.RELEASE
pom
import
```
## 13.1 Nacos 服务注册和配置中心
### 简介
> Nacos = Naming + Configuration + Service
一个更易于构建**云原生**应用的动态服务发现、配置管理和服务管理平台(**Nacos = Eureka + Config + Bus**)
下载地址:https://github.com/alibaba/nacos/releases
文档:https://nacos.io/zh-cn/
**各个注册中心的比较:**
| 服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
| ------------------ | --------------------- | ---------- | --------------- |
| Eureka | AP | 支持 | 低(2.x版本闭源) |
| Zookeeper | CP | 不支持 | 中 |
| consul | CP | 支持 | 高 |
| Nacos | AP(高可用+分区容错性) | 支持 | 高 |
### 安装与运行
> 确保本地环境 Java8 + Maven
1. 从[官网](https://github.com/alibaba/nacos/releases)上选择对应的版本后下载并解压

2. 进入到 bin 目录通过 cmd 执行 `startup.cmd -m standalone` 命令启动 nacos

3. 访问 http://localhost:8848/nacos

### 服务注册中心
#### 1) 构建服务提供者
1. 新建一个 Module `alibaba-nacos-payment`
2. 导入对应的依赖
父工程
```xml
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.5.RELEASE
pom
import
```
子工程
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 编写对应的 `application.properties` 文件
```properties
server.port=8081
spring.application.name=alibaba-nacos-payment
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
management.endpoints.web.exposure.include=*
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaNacosPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(AlibabaNacosPaymentApplication.class, args);
}
}
```
5. 创建业务类
```java
@RestController
public class NacosInfoController {
@Value("${server.port}")
private String port;
@GetMapping("/get/payment/port")
public String getPaymentPort(){
return "Payment port is " + port;
}
}
```
6. 启动项目,查看 http://localhost:8848/nacos 登录(账号密码默认都是 `nacos`)查看注册的服务

为测试后续 Nacos 的负载均衡,建立一个和当前 payment 一模一样的 Module
#### 2) 构建服务消费者
1. 新建 Module `alibaba-nacos-consumer`
2. 修改 POM 文件,引入需要的依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `application.yml` 文件
```properties
server.port=80
spring.application.name=alibaba-nacos-consumer
#添加微服务地址的配置,将配置与代码分离
service-url.nacos-payment: http://alibaba-nacos-payment
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConsumerApplication.class, args);
}
}
```
5. 配置负载均衡+RestTemplate
```java
@Configuration
public class NacosConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
```
6. 编写业务类
```java
@RestController
@Slf4j
public class ConsumerController {
@Value("${service-url.nacos-payment}")
private String nacPaymentServiceUrl;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/get/payment/port")
public String getPaymentPort(){
return restTemplate.getForObject(nacPaymentServiceUrl + "/get/payment/port", String.class);
}
}
```
7. 启动并访问进行测试,可以发现 Nacos 中已经实现了 **轮询负载算法**
#### 3) 服务注册中心对比
**和其他注册中心特性对比:**

**Nacos 服务发现实例模型:**

**Nacos 中 AP 和 CP 的切换:**
> C是所有节点在同一时间看到的数据是一致的;而A的定义是所有的请求都会收到响应。
—般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。
如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。
切换命令:`curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP`
### 服务配置中心
#### 1) 基础配置
> Nacos 就是一个配置服务器
1. 新建一个 Module `alibaba-nacos-config-client`
2. 修改 POM 文件,引入以下依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `application.properties` 和 `bootstrap.properties` 原理和 Spring Cloud Config 一样
```properties
#========bootstrap.properties
server.port=9097
spring.application.name=alibaba-nacos-config-client
# Nacos 服务注册中心地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
# Nacos 服务配置中心地址
spring.cloud.nacos.config.server-addr=localhost:8848
# 指定配置文件类型
spring.cloud.nacos.config.file-extension=yaml
#=========application.properties
spring.profiles.active=dev
```
4. 在 Nacos 中新建配置文件,名字要符合规范:`${微服务名}-${环境}.{文件类型}`

5. 创建主启动类
```java
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientApplication.class, args);
}
}
```
6. 创建业务类
```java
@RestController
@RefreshScope // 开启 Nacos 的动态刷新功能
public class ConfigInfoController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/get/config/info")
public String getConfigInfo(){
return configInfo;
}
}
```
7. 启动并访问 http://localhost:9097/get/config/info 进行测试

8. 在 Nacos 上进行修改

再次访问 - 不需要额外发送命令

**小结:**
- Nacos 自带动态刷新,且不需要使用额外的命令
- 文件名(dataId) = ${微服务名}-${环境}.${文件类型}
默认是根据 `spring.application.name` 提供的微服务名,也可以通过指定 `spring.cloud.nacos.config.prefix` 实现
#### 2) 分类配置
**Namespace + Group + Data ID:**
1. Namespace 命名空间,类似于 Java 中的 package,用于 **区分部署环境**
Group + Data ID,用于从逻辑上区分两个目标对象
2. 三者情况

3. Namesapce 默认是 `public`,Group 默认是 `DEFAULT_GROUP`,默认 Cluster 是 `DEFAULT`
- Nacos 默认的 Namespace 是 `public` ,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
- Group 默认是 `DEFAULT_GROUP`,Group 可以把不同的微服务划分到同一个分组里面去
- Service 就是微服务:一个 Service 可以包含多个 Cluster (集群),Nacos 默认 Cluster 是 DEFAULT,Cluster 是对指定微服务的一个**虚拟划分**。
比方说为了容灾,将 Service 微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的 Service 微服务起一个集群名称(HZ) ,给广州机房的 Service 微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
- Instance - 微服务的实例。
**三种加载配置方案**
1. 通过 Data ID 切换部署环境
1. 在 Nacos 上新建一个 `alibaba-nacos-config-client-test.yaml` 配置文件

2. 修改微服务模块的部署环境 -> `application.properties` 中的配置项
```properties
#spring.profiles.active=dev
spring.profiles.active=test
```
3. 重启,再次访问测试

2. 通过 Group 分组方案切换部署环境
1. 在 Nacos 上新建两个配置文件 `alibaba-nacos-config-info.yaml` 但分组名分别为 TEST_GROUP 和 DEV_GROUP

2. 添加微服务模块中的 `bootstreap.properties` 配置
```properties
# 指定配置文件所在的组
spring.cloud.nacos.config.group=DEV_GROUP
# 指定配置文件使用的前缀
spring.cloud.nacos.config.name=alibaba-nacos-config
```
3. 重启项目,进行访问

3. 通过 Namespace 切换部署环境
1. 在 Nacos 新建命名空间 dev & test

2. 在 dev 命名空间下创建两个不同分组的配置文件

注意命名空间的唯一标识
3. 在微服务模块的 `bootstrap.properties` 添加以下配置
```properties
# 对应的命名空间的唯一标识
spring.cloud.nacos.config.namespace=2d3f7efe-87a8-4881-a12d-f85fbf07264e
```
4. 重启微服务模块,访问并测试

### 集群与持久化
#### 1) 架构说明
官网:https://nacos.io/zh-cn/docs/deployment.html
Nacos 配置服务器是基于内存的,对于配置文件中的数据,默认都是通过一个嵌入式数据库实现数据的存储
但在生产环境中,一台 Nacos 往往是不够的,需要配置多台搭建一个 **集群**,而如果继续使用嵌入式数据库机会导致**数据一致性问题**,而 Nacos 为了解决这个问题,采用了**集中式存储的方式来支持集群化部署(目前只支持MySQL的存储)**
**Nacos 的三种部署模式:**
- 单机模式-用于测试和单机试用。
- 集群模式-用于生产环境,确保高可用。
- 多集群模式-用于多数据中心场景。

#### 2) 切换成 Mysql 数据源
> nacos 中使用的嵌入式数据库是 **derby**([参考链接](https://github.com/alibaba/nacos/blob/develop/config/pom.xml))
1. 找到 nacos 安装目录下的 `/conf/nacos-mysql.sql` 并在 mysql 中执行

2. 找到 `/conf/application.properties` 文件, 添加以下配置
```properties
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://localhost:3306/cloud?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
db.user=数据库连接使用的用户名
db.password=数据库连接使用的密码
```
3. 重新启动 Nacos,创建新配置文件,查看 `config_info` 表

#### 3) Linux 上 Nacos + Mysql 生产环境配置
1. 分别创建三台 Linux 服务器,安装对应的 Nacos Server 和 JDK 1.8
> 如果是 Centos 7 建议手动重新安装一份 OpenJDK
2. 在 `conf/` 下分别创建 `cluster.conf` 文件
```conf
192.168.127.134:8848
192.168.127.135:8848
192.168.127.136:8848
```
3. 修改 `conf/` 下的 `application.properties` 文件,切换到 Mysql 数据源
```properties
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://10.1.53.30:3306/cloud?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
db.user=数据库账户
db.password=数据库密码
```
4. 分别启动三台 Nacos Server 并进行访问测试
5. 在另一台上配置 Nginx,实现负载均衡
```nginx
upstream nacos-cluster{
server 192.168.127.134:8848;
server 192.168.127.135:8848;
server 192.168.127.136:8848;
}
server {
listen 1111;
server_name 192.168.127.134;
location / {
proxy_pass http://nacos-cluster;
}
}
```
6. 访问 Nginx 配置的反向代理地址进行测试

7. SpringBoot 项目集成 Nacos Server 集群
将注册中心地址修改为对应的 Nginx 代理即可
```properties
# Nacos 服务注册中心地址
spring.cloud.nacos.discovery.server-addr=http://192.168.127.134:1111
# Nacos 服务配置中心地址
spring.cloud.nacos.config.server-addr=http://192.168.127.134:1111
```

## 13.2 Sentinel 服务熔断和限流
### 简介
官方文档:https://sentinelguard.io/zh-cn/docs/quick-start.html
**主要特性:**

- **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- **完善的 SPI 扩展点**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
**两个核心部分:**
1. 核心库:(Java 客户端) 不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
2. 控制台:基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
**主要解决:** 服务雪崩,服务降级,服务熔断,服务限流
**下载地址:** https://github.com/alibaba/Sentinel/releases
> 下载后通过命令行执行即可,访问 http://localhost:8080/ 账号密码均为 sentinel
>
> 
### 初始化监控模块
1. 新建 Module `cloud-alibaba-sentinel-services`
2. 修改 POM
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建 `application.yml` 文件
```yaml
server:
port: 8401
spring:
application:
name: cloud-alibaba-sentinel-services
cloud:
nacos:
# Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
# Sentinel 服务监控平台
dashboard: localhost:8080
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelServicesApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelServicesApplication.class, args);
}
}
```
5. 创建业务类
```java
@RestController
public class FlowController {
@GetMapping("/test1")
public String getTest1Info(){
return "頑張らなくちゃはいけません";
}
@GetMapping("/test2")
public String getTest2Info(){
return "これからずっと頑張っていく";
}
}
```
6. 启动 Nacos 和 Sentinel 再启动项目
7. 由于 Sentinel 使用了懒加载,当监控服务被访问时才会显示在控制台上,所以要先访问接口
查看 http://localhost:8080

### 流控规则
#### 1) 基本介绍

- 资源名:对应的接口,也是唯一标识
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。
- 线程数:当调用该API的线程数达到**阈值**的时候,进行限流。
- 是否集群:不需要集群。
- 流控模式
- 直接:API 达到限流条件时,直接限流。
- 关联:当关联的资源达到阈值时,就限流自己。
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。
- 流控效果
- 快速失败:直接失败,抛异常。
- Warm up:根据 Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为**QPS**,否则无效。
#### 2) 阈值类型 + 直接(流控模式)
1. QPS:当调用该 API 的 QPS 达到阈值的时候,进行限流。
**配置**
**测试:** 快速多次访问对应的接口,查看限流后的效果

2. 线程数:当调用该API的线程数达到**阈值**的时候,进行限流。
**配置**
为了方便观察,修改源程序
```java
@GetMapping("/test1")
public String getTest1Info(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "頑張らなくちゃはいけません";
}
```
**打开两个窗口,轮流访问对应的接口**

#### 3) 关联(流控模式)
> 当关联的资源达到阈值时,就限流自己。
>
> 例如订单模块关联了支付模块,当支付模块达到了指定的**阈值**后,就将订单模块进行限流
**设置**
**通过 Postman** 进行高并发访问 `/test2` 再访问 `/test1`:
设置 Postman

发送请求时,访问 `/test1`

**注意:**这里不会对 test2 进行限流,而是会对 test1 进行限流
#### 4) 链路(流控模式)
1. 新增一个业务类
```java
@Service
public class FlowService {
// 标识这是一个资源,资源名为 sentinelChain
@SentinelResource("sentinelChain")
public String sentinelChain() {
return "Sentinel Mode - Chain";
}
}
```
2. 在控制器中添加两个接口,都是访问 `sentinelChain` 的
```java
@GetMapping("/chain1")
public String testChain1(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return flowService.sentinelChain();
}
@GetMapping("/chain2")
public String testChain2(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return flowService.sentinelChain();
}
```
3. 添加配置信息,1.7.0 版本开始(对应Spring Cloud Alibaba的2.1.1.RELEASE),官方在**`CommonFilter`** 引入了**`WEB_CONTEXT_UNIFY`** 参数,用于控制是否收敛context;将其配置为 false 即可根据不同的URL 进行链路限流;
```yaml
spring:
...
cloud:
sentinel:
...
web-context-unify: false
```
4. 配置流量规则
5. 轮流访问 `/chain1` 和 `/chain2` 可以发现 chain1 不会报错,而 chain2 会
6. 总结:
链路流控模式指的是,当从**某个接口**过来的资源达到**限流条件**时,开启**限流**;
它的功能有点类似于 **来源配置项**,区别在于:**针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度 更==细==;**
#### 5) 预热(流控效果)
**简介:**
Wram up,即预热/冷启动方式,当系统长期处于低水位的情况下,突然的流量增加,将系统拉到高水位可能瞬间把系统压垮
而通过 **冷启动**,可以让通过的流量慢慢增加,在一定的时间内增加**阈值上限**,给冷系统一个预热的时间,避免冷系统被压垮
(冷启动系统的 QPS 曲线)
**Sentinel** 中默认的 coldFator(冷加载因子) 默认为 3,即 QPS 的阈值上限会从**设置的最大阈值上限/coldFator** 开始在指定的时间内上升到 **设置的最大阈值上限**
**配置:**
系统初始化的阈值为 3 (10/3),在 5 秒内就会慢慢升到 10
也可以通过快速点击访问 http://localhost:8041/test1 发现刚开始或许会失败,但后续就会稳定下来
**应用场景:** 秒杀系统开启的瞬间
#### 6) 排队等待(流控效果)
> 均匀排队,让请求可以以均匀的速度通过,阈值类型必须设置成 **QPS**,否则无效
>
> 参考:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6-%E5%8C%80%E9%80%9F%E6%8E%92%E9%98%9F%E6%A8%A1%E5%BC%8F
1. 修改源程序
```java
@GetMapping("/test2")
public String getTest2Info(){
log.info("test2 is starting.....");
return "これからずっと頑張っていく";
}
```
2. 设置
3. 通过 Postman 进行高并发测试

4. 查看控制台打印

**应用场景:** 间隔性突发的流量(例如消息队列。当某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。)
### 降级
#### 1) 简介
官方文档:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是**保障高可用**的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如: 支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的**稳定性是不能保证**的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对**不稳定的弱依赖服务调用进行熔断降级**,**暂时切断不稳定调用**,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在==客户端(调用端)==进行配置。
**熔断策略:**
- 慢调用比例:需要设置允许的慢调用 RT(最大响应时间),当请求的响应时间大于 RT 时就为一个**慢调用**。
在单位统计时长(statIntervalMs)内**请求数目大于设置最小请求数目**,并且**慢调用的比例大于阈值**,就会自动熔断服务
在指定的熔断时间过后,就会进入**探测恢复状态**(HALF-OPEN),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于则会再次熔断
- 异常比例:在单位统计时长 (statIntervalMs) 内**请求数目大于设置的最小请求数**且**异常的比例大于阈值**
就会自动熔断,在指定的熔断时间过后,就会进入**探测恢复状态**(HALF-OPEN),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于则会再次熔断
异常比率的阈值范围是 `[0.0, 1.0]` 代表 0 ~ 100%
- 异常数:在单位统计时长内异常数超过了阈值就会自动熔断
在指定的熔断时间过后,就会进入**探测恢复状态**(HALF-OPEN),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于则会再次熔断
#### 2) 降级策列 - 慢处理比例
1. 创建新的接口方法
```java
@GetMapping("/test3")
public String getTest3Info(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "芜湖起飞";
}
```
2. 在 Sentinel Server 中配置**慢处理比例**降级规则
3. 通过 JMter 进行压力测试,在访问对应的接口
#### 3) 降级策略 - 异常比例
1. 创建新的接口方法
```java
@GetMapping("/error/ratio")
public String testErrorRatio(){
log.info("test ERROR_RATIO(异常比例)");
int i = 10 / 0;
return flowService.sentinelChain();
}
```
2. 配置**异常比例**的降级规则
3. 通过 JMter 测试并访问对应的接口(如果没有熔断就是出现 Error Page)
#### 4) 降级策略 - 异常数
1. 异常接口可以不变,修改对应的降级策略即可
2. 直接访问对应的接口即可(这里会在第 10 次后触发服务熔断)
### 热点 key 限流
#### 1) 简介
官方文档:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html
热点:在一个时间段内经常访问的数据,很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。例如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的**热点参数**,并根据配置的限流阈值与模式,对包**含热点参数的资源调用进行限流**。热点参数限流可以看做是一种**特殊的流量控制**,==仅对==包含热点参数的资源调用生效。
> Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
#### 2) 实战
1. 创建新的接口
```java
@RestController
public class HotKeyController {
/**
* {@code @SentinelResource}: 配置有关 Sentinel 的属性
* - value: 指定资源名,也是唯一标识
* - blockHandler: 针对 sentinel 控制台配置异常
* @param p1
* @param p2
* @return
*/
@SentinelResource(value = "testHotKey", blockHandler = "blockGetHotKeyInfo")
@GetMapping("/test/hotkey")
public String getHotKeyInfo(
@RequestParam(required = false) String p1,
@RequestParam(required = false) String p2
){
return "------> HotKeyInfo";
}
public String blockGetHotKeyInfo(String p1, String p2){
return "------> blockGetHotKeyInfo";
}
}
```
2. 配置热点限流规则
3. 访问接口测试,如果不带 `p1` 只带 `p2` 是不会出现热点限流的
**注意:** 这里如果不配置 fallback,就会出现 Error Page
4. 配置**参数例外项:** 当请求参数为某个特定的值时可以修改为指定的阈值
5. 再次访问接口进行测试,可以发现当 `p1 = 5` 时,不会出现热点限流
### 系统规则
官方文档:https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81
**简介:**
Sentinel 系统自适应限流从**整体维度**对==应用入口流量==进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
**入口流量:**进入应用的流量(`EntryType.IN`),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持的模式:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
### @SentinelResource 注解的使用
#### 1) 配置资源名 + Sentinel 配置错误后续处理
1. 启动 Nacos 和 Sentinel
2. 创建新的接口,用来测试 `@SentinelResource` 注解
```java
@RestController
public class RateLimitController {
/**
* {@code @SentinelResource} 的使用
* - value: 资源名
* - blockHandler: 针对 Sentinel 控制台配置出错的兜底方法
* @return
*/
@SentinelResource(value = "byResources", blockHandler = "getResourcesInfoHandler")
@GetMapping("/byResources")
public CommonResult getResourcesInfo(){
return new CommonResult<>(200, "按资源名称作为 Sentinel 配置", new Payment(2021L, "阿巴阿巴"));
}
public CommonResult getResourcesInfoHandler(BlockException blockException){
return new CommonResult<>(400, blockException.getMessage() + " - 服务不可用");
}
}
```
3. 启动项目,查看 Sentinel 控制台,配置服务限流
再次进行测试即可
#### 2) 配置 URL + Sentinel 默认处理
1. 创建新接口
```java
/**
* {@code @SentinelResource} 的使用: 将接口的地址作为 Sentinel 控制台配置
* @return
*/
@SentinelResource(value = "byUrl")
@GetMapping("/byUrl")
public CommonResult getUrlInfo(){
return new CommonResult<>(200, "根据 URL 作为 Sentinel 配置", new Payment(2022L, "阿巴巴"));
}
```
2. 启动项目,访问对应的接口
3. 在 Sentinel 控制台配置服务限流
再次进行测试即可 -> 如果没有配置 fallback 就会使用 Sentinel 默认的兜底方法
> **上面兜底方案面临的问题:**
>
> 1. 系统默认兜底方法不能体现出业务要求。
> 2. 自定义的处理方法又和业务代码耦合在一块,不直观。
> 3. 每个业务方法都有一个 fallback 就会导致代码膨胀
> 4. 全局统—的处理方法没有体现。
#### 3) 自定义 fallback
1. 创建一个 `handler/RateLimitHandler.java` 类,定义**全局的统一处理方法**
实现 **业务代码和处理代码的解耦**
```java
@Component
public class RateLimitHandler {
/**
* 编写自定义的符合业务的处理方法 --> handler01
* @param exception
* @return
*/
public static CommonResult handler01(BlockException exception){
return new CommonResult<>(444, "全局统一的处理方法: handler01()");
}
public static CommonResult handler02(BlockException exception){
return new CommonResult<>(444, "全局统一的处理方法: handler02()");
}
}
```
2. 修改原业务类,避免一个业务方法对应一个 `fallback()`
```java
/**
* 测试自定义的全局统一 fallback
* blockHandlerClass: 配置全局统一 fallback 类
* blockHandler: 配置指定的 fallback 方法
* @return
*/
@SentinelResource(value = "globalHandler",
blockHandlerClass = RateLimitHandler.class, blockHandler = "handler01")
@GetMapping("/test/globalHandler")
public CommonResult getGlobalHandler(){
return new CommonResult<>(200, "全局 fallback", new Payment(2023L, "阿巴巴巴"));
}
```
3. 启动项目,访问对应的接口后再 Sentinel 控制台进行配置
4. 重新测试接口

#### 4) 注解属性
- `value`:资源名称,必需项(不能为空)
- `entryType`:entry 类型,可选项(默认为 `EntryType.OUT`)
- `blockHandler` / `blockHandlerClass`: `blockHandler` 对应处理 `BlockException` 的函数名称,可选项。blockHandler 函数访问范围需要是 `public`,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 `BlockException`。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `blockHandlerClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。
- fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 `Throwable` 类型的参数用于接收对应的异常。
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `fallbackClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。
- defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnor 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个 `Throwable` 类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `fallbackClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。
- `exceptionsToIgnore`(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
> 若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 `BlockException` 时只会进入 `blockHandler` 处理逻辑。若未配置 `blockHandler`、`fallback` 和 `defaultFallback`,则被限流降级时会将 `BlockException` **直接抛出**
#### 5) 三个核心 API
> Sentinel 工作主流程:https://sentinelguard.io/zh-cn/docs/basic-implementation.html
1. SphU 用来定义资源
2. ContextUtil 用来表示上下文环境
3. Tracer 定义统计
### 服务熔断
#### 1) 构建环境
> 整合 Ribbon 实现负载均衡
**创建两个服务提供者模块**
1. 新建一个 Module `cloud-alibaba-sentinel-provider-payment`
2. 修改 POM
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 添加 `application.yaml`
```yaml
server:
port: 9094
spring:
application:
name: cloud-alibaba-sentinel-provider-payment
cloud:
nacos:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelProviderApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelProviderApplication.class, args);
}
}
```
5. 编写业务类
```java
@RestController
public class PaymentController {
@Value("${server.port}")
private String port;
// 模拟数据源
private HashMap dataMap = new HashMap(){{
put(1L, new Payment(1L, UUID.randomUUID().toString()));
put(2L, new Payment(2L, UUID.randomUUID().toString()));
put(3L, new Payment(3L, UUID.randomUUID().toString()));
}};
@GetMapping("/payment/{id}")
public CommonResult getPaymentInfo(@PathVariable Long id){
return new CommonResult<>(200, port + "-获取支付信息成功", dataMap.get(id));
}
}
```
6. 这里为了体现负载均衡,需要额外建一个一样的服务提供者模块,但端口号不同

**创建服务消费者模块**
1. 新建 Module `cloud-alibaba-sentinel-consumer-order`
2. 修改 POM
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.boot
spring-boot-starter-web
pers.dreamer07.springcloud
cloud-api-common
${project.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
```
3. 创建 `application.yaml`
```yaml
server:
port: 84
spring:
application:
name: cloud-alibaba-sentinel-consumer-order
cloud:
nacos:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8729
# 配置服务列表
services-uri:
cloud-alibaba-sentinel-provider-payment: http://cloud-alibaba-sentinel-provider-payment
```
4. 创建主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelConsumerApplication.class, args);
}
}
```
5. 创建配置类
```java
@Configuration
public class ApplicationConfig {
@Bean
// 开启负载均衡
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
```
6. 创建业务类
```java
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Value("${services-uri.cloud-alibaba-sentinel-provider-payment}")
private String providerServiceUrl;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")//没有配置
public CommonResult fallback(@PathVariable Long id)
{
CommonResult result = restTemplate.getForObject(providerServiceUrl + "/payment/"+id, CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
```
7. 启动项目, 访问 http://localhost:84/consumer/fallback/id 此时如果 id 不合法就会直接抛出 Error 页面,对用户不友好
#### 2) 配置 fallback
> fallback 只会针对业务异常
1. 添加接口的 `@SentinelResource` 注解的 ==fallback== 属性
并创建对应的 fallback() 方法
```java
@SentinelResource(value = "fallback", fallback = "fallbackHandler") // 配置 fallback
public CommonResult fallback(@PathVariable Long id)
{
...
}
public CommonResult fallbackHandler(Long id, Throwable t){
return new CommonResult<>(400, "出错啦! " + t.getMessage(), null);
}
```
2. 再次访问 http://localhost:84/consumer/fallback/id 接口

#### 3) 配置 blockfallback
> 该属性主要针对 Sentinel 控制台配置规则出错
1. 添加接口的 `@SentinelResource` 注解的 ==blockHandler== 属性
并创建对应的 fallback() 方法
```java
@SentinelResource(value = "fallback", fallback = "fallbackHandler", blockHandler = "fallbackBlockHandler")
public CommonResult fallback(@PathVariable Long id){...}
public CommonResult fallbackBlockHandler(Long id, BlockException e){
return new CommonResult<>(400, "服务器繁忙! " + e.getMessage(), null);
}
```
2. 配置 Sentinel 控制台的降级规则
3. 多次快速访问 http://localhost:84/consumer/fallback/id 接口

**总结:** 如果 `fallback` 和 `blockfallback` 都配置了,当出现 ==BlockException== 异常(控制台配置异常)时就会走 `blockfallback`
其他 Java 异常依然会走 `fallback` 异常
#### 4) 整合 OpenFeign
> 使用 OpenFeign 代替 Ribbon 实现服务调用
1. 修改消费者模块,导入以下依赖
```xml
org.springframework.cloud
spring-cloud-starter-openfeign
```
2. 开启 Sentinel 对 Feign 的支持
如果使用该配置导致报错(Requested bean is currently in creation: Is there an unresolvable circular reference?)
可以考虑将 SpringCloud 的版本调整到 **Hoxton.SR9**
```yaml
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
```
3. 在主启动类加上 `@EnableFeignClients` 注解
4. 创建对应的业务类和 fallback 处理方法类
```java
@FeignClient(value = "cloud-alibaba-sentinel-provider-payment", fallback = OrderServiceFeignFallback.class)
public interface OrderServiceFeign {
@GetMapping("/payment/{id}")
public CommonResult getPaymentInfo(@PathVariable("id") Long id);
}
```
```java
@Component
public class OrderServiceFeignFallback implements OrderServiceFeign {
@Override
public CommonResult getPaymentInfo(Long id) {
return new CommonResult<>(4444, "数据出错啦! -- OrderServiceFeignFallback",new Payment(id, null));
}
}
```
5. 在控制层中调用即可
```java
@RestController
public class OrderController {
...
@Resource
private OrderServiceFeign orderServiceFeign;
@GetMapping("/consumer/openFeign/{id}")
public CommonResult testOpenFeign(@PathVariable Long id){
return orderServiceFeign.getPaymentInfo(id);
}
...
}
```
6. 关闭两个服务消费者模块,启动项目,访问 http://localhost:84//consumer/openFeign/1

#### 5) 服务熔断框架对比
### 持久化
> 一旦我们重启应用,sentinel规则将消失,生产环境中需要将配置规则进行持久化
这里选择使用 Nacos 作为数据源进行持久化
1. 在 `cloud-alibaba-sentinel-services` 模块中引入以下依赖
```xml
com.alibaba.csp
sentinel-datasource-nacos
```
2. 在配置文件中添加配置
```yaml
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848 #nacos 访问地址
dataId: cloud-alibaba-sentinel-services # nacos 中对应存储规则配置文件的 dataID
groupId: DEFAULT_GROUP # nacos 中对应存储规则配置文件的 groupID
data-type: json # nacos 中对应存储规则配置文件的类型
rule-type: flow # 定义存储的规则的类型(限流/降级/其他),可以查看 org.springframework.cloud.alibaba.sentinel.datasource.RuleType
```
3. 在 Nacos 中创建对应的配置文件(确保 groupId 和 dataId 相同)

```json
[
{
"resource": "/byUrl", // 资源名称
"IimitApp": "default", // 针对来源
"grade": 1, // 阈值类型,0表示线程数, 1表示QPS;
"count": 1, // 单机阈值;
"strategy": 0, // 流控模式,0表示直接,1表示关联,2表示链路;
"controlBehavior": 0, // 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
"clusterMode": false // 是否集群。
}
]
```
4. 启动对应的服务模块,访问对应的接口**后**查看 Sentinel 控制台的流控规则配置

## 13.3 Seata 分布式事务
> nacos + seata1.4
### 简介
一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。(是什么)
官网:http://seata.io/zh-cn/
**分布式事务:**(能干嘛)
在微服务架构的系统中,有可能会出现跨数据源(可能一个微服务对应一个数据库)的操作,此时只能保证在当前微服务内部的数据一致性,而无法保证整个系统的数据一致性
**一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题**。
### 术语
> 通过 一ID(Transaction ID XID 全局唯一的事务ID) + 三组件 模型实现分布式事务
**三组件:**
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
**具体处理过程:**

1. TM 向 TC 申请开启一个**全局事务**,全局事务创建成功并生成一个全局唯一的 XID;
2. XID 在微服务调用链路的上下文中传播;
3. RM 向 TC 注册**分支事务**,将其**纳入XID对应全局事务的管辖**;
4. TM 向 TC 发起**针对 XID 的全局提交或回滚决议**;
5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
### 安装与使用
1. 在 https://github.com/seata/seata/releases 中选择合适的版本后下载解压即可
2. 修改事务的数据源:在 `conf` 文件夹中备份配置文件 `file.conf` ,再进行修改(事务日志存储模式为db +数据库连接信息)
```yaml
store {
# 使用 db 表示使用数据库作为数据源
mode = "db"
...
db {
...
## 配置数据源
db-type = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC"
user = "root"
password = "123456"
...
}
}
```
3. 数据库建表:在 `conf` 文件夹中找到 README.md 中的 Server 连接,进入到 github 中根据说明复制对应的数据源的 sql 语句
```sql
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
```
4. 在 Nacos 中新建一个名为 seata 的命名空间
5. 将 seata 注册到 nacos 中:(先备份)在 `conf` 文件夹中找到 registry.conf 文件
```yaml
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "b142f989-037e-40a5-9b53-0c822f78fb61" # seata 命名空间的 id
cluster = "default"
username = "nacos"
password = "nacos"
}
...
}
```
6. 在 [seata-config-center](https://github.com/seata/seata/tree/1.4.2/script/config-center) 中下载 `config.txt`(放在 Seata 根目录) 和 `nacos/nacos-config.sh`(放在 /conf 下)
`config.txt` 中存放着 seata 的配置,可以通过执行 `nacos-config.sh` 将其推送到 nacos 中,客户端就无需配置 **file.conf** 和 **registry.conf**
7. 修改 `config.txt`

8. 通过 **Git Bash Here** 执行 `nacos-config.sh` 以下命令
```shell
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t {Seata 命名空间 id} -u nacos -w nacos
```
Nacos 配置列表

9. 启动 seata 即可 (bin/seata-server.bat),查看 Nacos 中注册的服务列表

**怎么玩:** 本地 @Transactional + 全局 @GlobalTransactional
基于 SEATE 的分布式交易解决方案:

### 分布式事务实战
#### 1) 业务环境搭建
**业务说明:**
当用户下单时,会在**订单服务**中创建一个订单, 然后通过远程调用**库存服务**来扣减下单商品的库存,再通过远程调用**账户服务**来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
**数据库准备**
1. 创建三个数据库,对应三个微服务模块(账户,库存,订单)
```sql
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
```
2. 在各自的数据库下创建各自的业务表
```sql
CREATE TABLE t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
);
SELECT * FROM t_order;
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
);
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0','100');
SELECT * FROM t_storage;
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额', I
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
);
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
```
3. 在各自的数据库中创建对应的回滚日志表,用来记录分支事务的执行情况
```sql
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
```
4. 最终效果

#### 2) 业务代码编写
**alibaba-seate-order-services**
****
1. 新建 Module `alibaba-seate-order-services`
2. 修改 POM,导入需要的依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-spring-boot-starter
io.seata
seata-spring-boot-starter
1.4.2
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
com.alibaba
druid
com.baomidou
mybatis-plus-boot-starter
p6spy
p6spy
pers.dreamer07.springcloud
cloud-api-common
${project.version}
```
3. 创建 `application.yml` (注意这里整合了 p6spy 实现 MP 日志打印)
```yaml
server:
port: 2002
spring:
application:
name: alibaba-seate-order-services
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 确保和 seata-server 在同一个命名空间和分组中
group: SEATA_GTOUP
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
seata:
enabled: true
application-id: ${spring.application.name}
enable-auto-data-source-proxy: true # 开启 seata 数据源代理
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
service:
vgroup-mapping:
my_test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
```
确保 `seata.{事务分组名}` 和 `seata.service.vgroup-mapping.${事务分组名}=default` 的事务分组名一样
该配置可以在 `config.txt` 中修改

4. 创建对应的实体类
```java
@TableName("t_order")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status;
}
```
5. 创建对应的 Mapper 接口即可
```java
@Mapper
public interface OrderMapper extends BaseMapper {
}
```
6. 创建对应的微服务调用 Feign
```java
@FeignClient(name = "alibaba-seata-account-services")
public interface AccountFeignClient {
@PutMapping("/account/deduct/{accountId}/{money}")
CommonResult updateAccountMoney (@PathVariable("accountId") Long accountId, @PathVariable("money") Integer money);
}
```
```java
@FeignClient(name = "alibaba-seata-storage-services")
public interface StorageFeignClient {
@PutMapping("/storage/stock/{productId}/{count}")
CommonResult updateStorageStock(@PathVariable("productId") Long productId, @PathVariable("count") Integer count);
}
```
7. 创建对应的订单业务逻辑类
```java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StorageFeignClient storageFeignClient;
@Override
public CommonResult addOrder(Order order) {
log.info("order service - add order start");
order.setStatus(0);
orderMapper.insert(order);
log.info("storage service - update storage stock start");
storageFeignClient.updateStorageStock(order.getProductId(), order.getCount());
log.info("storage service - update storage stock success");
log.info("account service - update account money start");
accountFeignClient.updateAccountMoney(order.getUserId(), order.getMoney().intValue());
log.info("account service - update account money success");
updateOrderStatus(order.getId());
log.info("order service - add order success");
return new CommonResult<>(200, "添加订单成功", order);
}
@Override
public CommonResult updateOrderStatus(Long id) {
Order order = orderMapper.selectById(id);
if (order == null){
return new CommonResult<>(400, "订单信息不存在", null);
}
order.setStatus(1);
orderMapper.updateById(order);
return new CommonResult<>(200, "订单支付成功", order);
}
}
```
8. 创建对应的控制器类
```java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order/creata")
public CommonResult addOrder(Order order){
return orderService.addOrder(order);
}
}
```
9. 创建对应的主启动类
```java
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan(basePackages = "pers.dreamer07.springcloud.mapper")
public class SeataOrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderServiceApplication.class, args);
}
}
```
**alibaba-seata-storage-services**
1. 创建 Module `alibaba-seata-storage-services`
2. 修改 POM,引入需要的依赖(和 alibaba-seate-order-services 一致)
3. 创建对应的 `application.yml` 文件
```yaml
server:
port: 2001
spring:
application:
name: alibaba-seata-account-services
cloud:
nacos:
discovery:
server-addr: localhost:8848
group: SEATA_GTOUP
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/seata_account?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
seata:
enabled: true
application-id: ${spring.application.name}
enable-auto-data-source-proxy: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
service:
vgroup-mapping:
my_test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
```
4. 创建对应的实体类和 Mapper 接口
5. 创建对应的商品业务逻辑类
```java
@Service
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Override
public CommonResult updateStorageStock(Long productId, Integer count) {
Storage storage = storageMapper.selectById(productId);
if (storage == null){
throw new RuntimeException("商品数据异常");
}
Integer residue = storage.getResidue();
if (residue < count){
throw new RuntimeException("商品个数不足");
}
storage.setUsed(storage.getUsed() + count);
storage.setResidue(residue - count);
storageMapper.updateById(storage);
return new CommonResult<>(200, "修改商品信息成功", storage);
}
}
```
6. 编写对应的控制器类
```java
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@PutMapping("/storage/stock/{productId}/{count}")
CommonResult updateStorageStock(@PathVariable("productId") Long productId, @PathVariable("count") Integer count){
return storageService.updateStorageStock(productId, count);
}
}
```
7. 创建对应的主启动类
```java
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "pers.dreamer07.springcloud.mapper")
public class SeataStorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStorageServiceApplication.class, args);
}
}
```
**alibaba-seata-account-services:** 和上述两个大同小异,大多是业务逻辑和配置文件的不同
```yaml
server:
port: 2001
spring:
application:
name: alibaba-seata-account-services
cloud:
nacos:
discovery:
server-addr: localhost:8848
group: SEATA_GTOUP
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/seata_account?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
seata:
enabled: true
application-id: ${spring.application.name}
enable-auto-data-source-proxy: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
namespace: b142f989-037e-40a5-9b53-0c822f78fb61
service:
vgroup-mapping:
my_test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
```
#### 3) 测试
**正常情况:**
1. 通过 Postman 发送请求

2. 查看数据库数据

**模拟超时:**
1. 修改用户模块的逻辑,模拟超时异常(OpenFeign 默认超时时间为 1s)
```java
@Override
public CommonResult deductMoney(Long accountId, Integer money) {
log.info("alibaba-seata-account-services: deduct money start - id: {}, money: {}", accountId, money);
Account account = accountMapper.selectById(accountId);
if (account == null){
throw new RuntimeException("账户不存在");
}
if (account.getResidue() < money){
throw new RuntimeException("余额不足");
}
account.setResidue(account.getResidue() - money);
account.setUsed(account.getUsed() + money);
// 模拟超时异常
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountMapper.updateById(account);
log.info("alibaba-seata-account-services: deduct money success");
return new CommonResult<>(200, "扣除账户余额成功", account);
}
```
2. 再次测试

3. 查看数据库数据

可以发现出现了数据异常
**添加 @GlobalTransactional** 注解解决分布式事务问题
1. 修改订单模块的业务方法,添加 `@GlobalTransactional` 注解
```java
@GlobalTransactional(name = "test-create-order", rollbackFor = Exception.class)
public CommonResult addOrder(Order order) {
```
==name== 为当前业务对应的全局事务名称,==rollbackFor== 为当出现了指定异常类的信息后就进行回滚
更多属性可以查看源码

2. 再次测试请求超时

> 这里可以优化成在用户模块中使用 `@GlobalTransactional` 避免订单不能被保存
### 事务模式
#### 1) AT 模式
**前提:**
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
**整体机制:**两阶段提交协议的演变
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:全局提交事务时使用异步化(可以非常快速的完成),回滚时会通过一阶段的回滚日志进行**反向补偿**
**分布式事务的执行流程:**
> 
1. TM 开启分布式事务(TM 向 TC 注册全局事务记录)
2. 根据业务需求,编排数据库,服务等事务内资源( RM 向 TC 汇报资源准备状态) ;
3. TM 结束分布式事务,事务一阶段结束(TM 向 TC 提交/回滚全局事务)
4. TC 汇总事务信息,决定分布式事务的提交/回滚
5. TC 通知所有 RM 提交/回滚事务,事务二阶段结束
**原理分析:**
> 可以通过 debug 程序查看数据库中的 undo_log 表
- 一阶段加载:在此阶段,Seata 会拦截 **业务 SQL**
1. 解析 SQL 语义,找到业务 SQL 需要更新的业务数据,在业务数据被更新前,将其保存成 **before image**
2. 执行业务 SQL
3. 将更新后的数据保存为 **after image** 生成行锁(事务结束之前其他事务无法操作该行)
以上的所有操作都会在一个数据库事务内完成,保存**一阶段操作的原子性**

- 二阶段提交:顺利提交后,Seata 只需将一阶段保存的快照数据和行锁删除,完成数据清理即可

- 二阶段回滚:Seata 需要回滚一阶段执行的 **业务 SQL** 还原业务数据
首先进行校验是否脏写(对比当前业务数据和 **after image**) 如果相同代表没有脏写,可以通过 **before image + 逆向 SQL** 还原数据,如果出现脏写就转人工处理
之后也会删除相应的 UNDO LOG 记录

一阶段加载过程
