SpringCloud
SpringCloud
Nuyoah版本适配
可通过查阅官方文件,可得springCloud适配的SpringBoot和JAVA的版本,建议通过GitHub对应的官方文档来查阅
SpringCloud作用
是将一系列微服务操作整合到一起
- 服务网关:Vue前台页面不应该直接知道后台服务器的真实地址,所以需要通过服务网关来实现
- 负载均衡:后台服务器不可能只有一个,需要对多个服务器进行调用,通过负载均衡来实现
- 分布式事务:后台数据库也不可能只有一个,为了能够实现各个数据库之间的数据一致,通过分布式事务来实现
- 服务熔断:当后台有服务器失效的时候,需要通过服务熔断来进行服务降级监控
SpringCloud就是将上述功能进行整合
如果有一些老项目还是需要使用一些不流行的框架
Base工程模块构建
-
构建父工程Maven
设置编码:设置 - 编译器 - 文件编码 - 全部UTF-8
设置注解生效:设置 - 构建执行部署 - 编译器 - 注解处理器 - 勾上启用注解
设置JDK版本:设置 - 构建执行部署 - 编译器 - JAVA编译器 - 设置目标字节码版本为17
设置忽略文件类型:设置 - 编辑器 - 文件类型 - 忽略文件和文件夹
-
设置父工程pom文件
依赖引入报错是因为dependencyManagement只是管理包依赖,并没有真正的下载,可以先将dependencyManagement标签去掉然后刷新maven项目,等待包下载完毕之后,再添加上dependencyManagement标签就不会爆红了
dependencyManagement标签的作用:让子项目不用在显示的声明依赖项的版本号,都沿用父项目中定义的,避免引入出错,升级方便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.2</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<swagger3.version>2.2.0</swagger3.version>
<mapper.version>4.2.3</mapper.version>
<fastjson2.version>2.0.40</fastjson2.version>
<persistence-api.version>1.0.2</persistence-api.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springboot 3.2.0-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud 2023.0.0-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud alibaba 2022.0.0.0-RC2-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--通用Mapper4之tk.mybatis-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>${persistence-api.version}</version>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project> -
创建子工程,子工程pom文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>mybatis_generator2024</artifactId>
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--mysql8.0-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project> -
配置数据库,外加mapper service entity生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24spring:
datasource:
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: admin
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
# 匿名域
type-aliases-package: com.auguigu.domain.entity
# mapper文件映射路径
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
# # 日志文件
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: true
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
logic-not-delete-value: 0
logic-delete-field: deleted
logic-delete-value: 1
id-type: auto -
创建微服务
-
建module
-
改POM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-provider-payment8001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> -
写YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21server:
port: 8001
# ==========applicationName + druid-mysql8 driver===================
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: admin
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true -
主启动
1
2
3
4
5
6
7
8
9
10
11
12
13package com.atguigu.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class);
}
} -
业务类
mapper
1
2
3
4
5
6
7package com.atguigu.cloud.mapper;
import com.atguigu.cloud.domain.entities.Pay;
import tk.mybatis.mapper.common.Mapper;
public interface PayMapper extends Mapper<Pay> {
}service
1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.atguigu.cloud.service;
import com.atguigu.cloud.domain.entities.Pay;
import java.util.List;
public interface PayService {
public int add(Pay pay);
public int delete(Integer id);
public int update(Pay pay);
public Pay getById(Integer id);
public List<Pay> getAll();
}serviceimpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package com.atguigu.cloud.service.impl;
import com.atguigu.cloud.domain.entities.Pay;
import com.atguigu.cloud.mapper.PayMapper;
import com.atguigu.cloud.service.PayService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
public class PayServiceImpl implements PayService {
private PayMapper payMapper;
public int add(Pay pay) {
return payMapper.insertSelective(pay);
}
public int delete(Integer id) {
return payMapper.deleteByPrimaryKey(id);
}
public int update(Pay pay) {
return payMapper.updateByPrimaryKeySelective(pay);
}
public Pay getById(Integer id) {
return payMapper.selectByPrimaryKey(id);
}
public List<Pay> getAll() {
return payMapper.selectAll();
}
}controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36package com.atguigu.cloud.controller;
import com.atguigu.cloud.domain.DTO.PayDTO;
import com.atguigu.cloud.domain.entities.Pay;
import com.atguigu.cloud.service.PayService;
import com.atguigu.cloud.utils.BeanCopyUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
public class PayController {
private PayService payService;
public String addPay( { Pay pay)
int i = payService.add(pay);
return "success: " + i;
}
public String deletePay( { Integer id)
int i = payService.delete(id);
return "success: " + i;
}
public String updatePay( { PayDTO payDTO)
Pay pay = BeanCopyUtils.CopyBean(payDTO, Pay.class);
int i = payService.update(pay);
return "success: " + i;
}
}Utils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41package com.atguigu.cloud.utils;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.stream.Collectors;
public class BeanCopyUtils {
/**
* 拷贝单个对象
* @param o
* @param clazz
* @return
* @param <V>
*/
public static <V> V CopyBean(Object o, Class<V> clazz) {
V result = null;
try {
result = clazz.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(o, result);
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
/**
* 复制多个对象
* @param list
* @param clazz
* @return
* @param <O>
* @param <V>
*/
public static <O, V> List<V> CopyBeanList(List<O> list, Class<V> clazz) {
return list.stream()
.map(o -> CopyBean(o, clazz))
.collect(Collectors.toList());
}
}实体类 :PayDTO 和 Pay
-
测试
swagger3测试
引入依赖
1 | <dependency> |
@Tag注解:标识在Controller类上,表明这个Controller的作用是什么
1 |
|
@Operation:标识在Controller类中的接口上,标识该接口的租用
1 |
|
@Schema:标识在实体类和对应的属性上表明该实体类的作用
1 | /** |
创建Swagger3配置类
config.Swagger3Config
1 | package com.atguigu.cloud.config; |
时间格式问题
方法一:注解法
在相应的类的属性上使用@JsonFormat注解:
两个写一个即可,建议写上面的
1 |
|
方法二:配置文件
application.yml
1 | spring: |
统一返回值问题
后端返回是数据不建议直接返回String List 等各种各样的类型
所以需要统一返回值
返回值格式:
-
code状态值:有后端统一定义各种返回值结果
分类 区间 分类描述 1** 100-199 信息,服务器收到请求,需要请求者继续执行操作 2** 200-299 成功,操作被成功接受并处理 3** 300-399 重定向,需要进一步的操作已完成请求 4** 400-499 客户端错误,请求包含语法错误或无法完成请求 5** 500-599 服务器错误,服务器在请求过程中发生了错误 -
message描述:本次接口调用的结果描述
-
data数据:本次返回的数据
-
(扩展)接口调用时间之类:timestamp接口调用时间
设置方式
-
新建枚举类AppHttpCodeEnum
构造枚举三步
举值(例如下面的SUCCESS()) -
构造(举值中有几个参数就构造几个变量, 创建构造方法将这几个变量初始化) -
遍历()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60package com.atguigu.cloud.enums;
import java.util.Arrays;
public enum AppHttpCodeEnum {
// 成功
SUCCESS(200,"操作成功"),
/**操作失败**/
RC999(999,"操作XXX失败"),
/**操作成功**/
RC200(200,"success"),
/**服务降级**/
RC201(201,"服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202(202,"热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203(203,"系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204(204,"授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403(403,"无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401(401,"匿名用户访问无权限资源时的异常"),
RC404(404,"404页面找不到的异常"),
/**服务异常**/
RC500(500,"系统异常,请稍后重试"),
RC375(375,"数学运算异常,请稍后重试"),
INVALID_TOKEN(2001,"访问令牌不合法"),
ACCESS_DENIED(2003,"没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
BUSINESS_ERROR(1004,"业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式"),
SYSTEM_ERROR(500, "出现错误");
int code;
String msg;
AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.msg = errorMessage;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public static AppHttpCodeEnum getReturnCodeEnum(int code) {
return Arrays.stream(AppHttpCodeEnum.values())
.filter(e -> e.getCode() == code)
.findFirst()
.orElse(null);
}
} -
新建统一定义返回对象ResponseResult
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123package com.atguigu.cloud.domain;
import com.atguigu.cloud.enums.AppHttpCodeEnum;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.io.Serializable;
public class ResponseResult<T> implements Serializable {
private Integer code;
private String msg;
private T data;
private long timestamp;
public ResponseResult() {
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
this.timestamp = System.currentTimeMillis();
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
public static ResponseResult okResult(Object data) {
ResponseResult result = setReturnCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}
public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setReturnCodeEnum(enums,enums.getMsg());
}
public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setReturnCodeEnum(enums,msg);
}
public static ResponseResult setReturnCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}
private static ResponseResult setReturnCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}
public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
统一异常处理
无需再写try…catch…finally
不用关注错误代码,只需要关注正确结果即可
创建系统异常对象
exception.SystemException
1 | package com.atguigu.cloud.exception; |
创建异常处理类
handler.exception.GlobalExceptionHandler
1 | package com.atguigu.cloud.handler.exception; |
RestTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法
是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
一个模块服务调用另一个模块服务
两种方式
restTemplate.getForObject(String url, Class< T > responseType, Object… uriVariables)
restTemplate.getForEntity(String url, Class< T > responseType, Object… uriVariables)
这三个参数分别代表
Rest请求地址,Http相应被转换成的对象类型, 请求参数
1 |
|
创建restTemplate配置类
1 |
|
工程重构
将多个模块中公共的代码提取出来
新建cloud-api-commons模块将公共部分提取出来
改POM文件 引入依赖
1 | <dependencies> |
将公共部分提取到该文件中
删除源文件中的内容
在公共项目中install一下
在原项目的pom文件中引入公共项目
1 | <dependency> |
服务注册与发现 Consul
为什么要引入微服务
上面Controller问题
1 | public static final String PaymentSrv_URL = "http://localhost:8001"; |
微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题
(1)如果订单微服务和支付微服务的IP地址或者端口号发生了变化,则支付微服务将变得不可用,需要同步修改订单微服务中调用支付微服务的IP地址和端口号。
(2)如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。
(3)如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护会变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现,从此刻开始我们正式进入SpringCloud实战
Eureka停用的原因
- Eureka停更进维
- Eureka对初学者不好
- 注册中心独立和微服务功能解耦,Eureka服务和程序员开发的微服务混在一起
- 阿里巴巴Nacos的崛起
什么是consul
Consul 是一套开源的分布服务发现和配置管理系统
SpringCloudConsul官网:https://spring.io/projects/spring-cloud-consul
作用:
-
服务发现:提供HTTP和DNS两种发现方式
-
健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制
-
KV存储:KeyValue存储方式
-
多数据中心:Consul支持多数据中心
-
可视化Web界面
consul的使用:https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/
下载
-
consul下载路径:https://developer.hashicorp.com/consul/install?product_intent=consul
win系统控制台输入wmic cpu get AddressWidth 显示64就用AMD64
-
在解压路径下的文件夹下使用consul --version 如果输出信息则表明下载正确
-
在开发模式下使用
- consul agent -dev
- 通过http://localhost:8500访问Consul首页
使用
服务注册与发现
服务提供者8001
-
修改pom文件
1
2
3
4
5<!-- springCloudConsul依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency> -
修改yml文件:
1
2
3
4
5
6
7spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name} -
修改写死的微服务网址
1
2// public static final String PaymentSrv_URL = "http://localhost:8001";
public static final String PaymentSrv_URL = "http://cloud-payment-service"; -
在RestTemplate配置类上添加@LoadBalanced注解,表明让testTemplate支持负载均衡
1
2
3
4
5
6
7
8
public class RestTemplateConfig {
public RestTemplate restTemplate() {
return new RestTemplate();
}
}如果不添加负载均衡会出现:I/O error on GET request for “http://cloud-payment-service/pay/get/2”: cloud-payment-service
因为SpringCloudConsul默认支持负载均衡,因为我们微服务提供者有两个,在消费者发送请求给生产者的两个微服务的过程中,服务器并不知道我们到底给哪一个生产者去处理,所以报这个异常。
所以在RestTemplate上需要添加@LoadBalanced注解,让服务器知道采用轮询的负载均衡方式进行请求即可。
三个注册中心的异同点:
CPA
C:Consistency (强一致性)
A:Availability(可用性)
P:Partition tolerance(分区容错性)
最多只能同时较好的满足两个
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,因此,根据 CAP 原理将 NOSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:
CA- 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强木。
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | JAVA | CP | 支持 | 客户端 | 已集成 |
分布式配置
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个
服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分
都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数
据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。
当下我们每一个微服务自己带着一个application.yml,上百个配置文件的管理…/(ToT)/~~
1. 配置对应项目的pom文件
添加SpringCloudConfig依赖项
1 | <!-- SpringCloud consul config --> |
2.配置yml文件
通过配置bootstrap.yml文件提取所有项目中application.yml中的公共项
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context 的父上下文。初始化的时候,Bootstrap
Context 负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的 Environment’。
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。"Bootstrap context 和Application Context’有着不同的约定
所以新增了一个bootstrap.yml 文件,保证 Bootstrap Context 和Application Context 配置的分离
application.yml立件改为bootstrap.yml,这是很关键的或者两者共存
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
application.yml
1 | server: |
bootstrap.yml
1 | spring: |
3.consul中KeyValue配置
参考规则:文件夹路径默认是",“分割,上面配置文件已改成”-"
1 | config/cloud-payment-service/data |
配置路径严格按照上面的来建立
测试结果
1 |
|
动态刷新
-
主启动类添加**@RefreshScope**
-
设置刷新时间(一般不建议设置,默认时间55s)
1
2
3
4
5
6
7
8
9
10
11
12
13
14spring:
application:
name: cloud-payment-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # 默认文件分割服为“,” 我们将其设置为“-”
format: YAML
watch:
wait-time: 30 # 设置自动刷新时间
KeyValue持久化
在consul.exe的文件夹下新建consul_start.bat文件写入
1 | @echo.服务启动...... |
在consul.exe的文件夹下新建mydata文件夹
负载均衡 LoadBalance
什么是LoadBalance
官网:LoadBalance)
LB负载均衡(Load Balance)是什么:
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等
Spring-cloud-starter-loadbalancer组件是什么
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient (WeClient是SpringWeb Flux中提供的功能,可以实现响应式异步请求)
loadbalancer本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
Loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现
RPC远程服务调用技术。
LoadBalance的使用
LoadBalancer 在工作时分成两步:
第一步,先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,默认轮询调用谁都可以正常执行。类似生活中求医挂号,某个科室今日出诊的全部医生,客户端你自己选一个。
第二步,按照指定的负载均衡策略从server取到的服务注册列表中由客户自己选择一个地址,所以LoadBalancer是一个客户端的负载均衡器
创建一个服务多个实例
创建一个和8001配置服务一模一样的服务8002
得到一个微服务中有两个实例:这样consumer微服务实例想要访问payment微服务实例,需要将payment微服务的所有实例轮巡检查
修改订单80模块
添加依赖:POM文件
1 | <!-- loadbalancer --> |
添加访问8001 8002的方法
1 | public static final String PaymentSrv_URL = "http://cloud-payment-service"; |
cloud-payment-service微服务下面有两个实例,所以需要在restTemplate配置类上加上@LoadBalanced,让restTemplate能够支持轮询检查
1 |
|
总结
Consul获取所有服务的方法,使用DiscoveryClient动态获取所有上线的服务列表
1 |
|
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
1 | List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service"); |
如: List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:
当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
如此类推…
切换负载均衡算法
默认有两种算法:轮询,随机
切换方法 在RestTemplateConfig配置类中切换
1 |
|
服务接口调用 OpenFeign
什么是OpenFeign
openfeign是一个声明式的Web服务客户端
只需创建一个Rest接口并在该接口上添加注解@FeignClient即可
OpenFeign基本上就是当前微服务之间调用的事实标准
OpenFeign能干什么
OpenFeign是对RestTemplate 和 LoadBalance的封装
前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在
OpenFeign的实现下,我们只需创建一个接口并使用注解的方式配置它(在一个微服务接口上面标注一个@FeionClient注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提
供者给出调用接口清单,消费者直接通过OpenFeign调用即可。
OpenFeian同时还集成SpringCloud LoadBalancer
可以在使用OpenFeiqn时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能。而与SprinaCloud
LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
OpenFeign使用方法
1. 接口加注解
接口:微服务接口
注解:@FeignClient注解
服务消费者,通过公共API模块调用服务,而不是直接调用服务
订单模块要去调用支付模块,订单和支付两个微服务,需要通过Api接口解耦,一般不要在订单模块写非订单相关的业务,自己的业务自己做+其它模块走FeianApi接口调用
2. 建立模块
改POM
1 | <dependencies> |
写yaml
1 | server: |
修改主启动类
1 |
|
3.修改通用模块
服务请求模块通过API接口来调用对应的模块,可能有多个服务都要通过API来进行模块的调用,所以要将OpenFeign接口放在通用模块中
在公共模块引入OpenFeign模块
1 | <!-- OpenFeign接口模块 --> |
新建服务接口PayFeignApi,头上配置@FeignClient注解
在公共模块下新建apis.PayFeignApi接口,接口中提供的方法就是服务cloud-payment-service对外暴露的方法
1 | // 参数为服务名称 |
4.修改Controller层接口
不再需要RestTemplate进行多模块调用了,直接调用公共的API来进行调用
1 |
|
超时控制
在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问
题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配
置超时时间就有必要了。
OpenFeigen默认超时时间是60s,超过60s之后会报错
yml配置超时时间
connectTimeout:连接超时时间
readTimeout:请求处理超时时间
application.yml
全局配置
1 | spring: |
局部配置
1 | spring: |
重试机制
重试机制:如果访问一次没有获取相应的结果,则会重新访问,设置重新访问的次数
OpenFeign默认机制是关闭的:只会调用一次,如果不成功就结束
开启Retryer功能:新增配置类FeignConfig并修改Retryer配置
1 |
|
默认HttpClient修改
OpenFeign中http client
如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求
由于默认HtpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。
使用Apache HttpClient5 替换 OpenFeign默认的HttpClient,解放性能
POM修改
1 | <!-- Apache HttpClient5 --> |
YML修改
1 | spring: |
请求/相应压缩
对请求和响应进行GZIP压缩:
Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过下面的两个参数设置,就能开启请求与相应的压缩功能:
1 | spring: |
细粒度化设置:
对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限
只有超过这个大小的请求才会进行压缩:
1 | spring: |
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发乐缩数据类型
spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小
日志打印
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别
从而了解 Feign 中 Http 请求的细节
说白了就是对Feign接口的调用情况进行监控和输出
日志级别
NONE: 默认的,不显示任何日志;
BASIC: 仅记录请求方法、URL、响应状态码及执行时间;
HEADERS: 除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL: 除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
配置Bean
1 |
|
配置YAML
通过配置YAML文件,来配置需要开启配置的服务名
公式:logging.level + 含有@FeignClient注解的完整的代包名的接口名 + debug
1 | logging: |
服务的熔断和降级
问题
分布式服务面临的问题:复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
解决方式
有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
需要实现:
- 服务熔断:设置一个开关,开能用,关失效,并返回友好的处理方式
- 服务降级:服务繁忙,请稍后再试,并返回友好的处理方式
- 服务限流:限定1s中几个
- 服务限时
- 服务预热
- 接近实时的监控
- 兜底的处理动作
Circuit Breaker
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
当过一段时间后CircuitBreaker会切换到HALF_OPEN状态,尝试发送几个探路请求,看链路是否通顺,如果通顺则切换到CLOSED状态,如果不行则返回OPEN状态
**CircuitBreaker和Resilience4J的关系:**CircuitBreaker只是以一套规范和接口,落地实现者是Resilience4J
Resilience4J
Resilience4j 是一个专为函数式编程而设计的轻量级容错库。 Resilience4j 提供了高阶函数(装饰器)来增强任何功能接口, 带有断路器、速率限制器、重试或隔板的 lambda 表达式或方法引用。 您可以在任何函数接口、lambda 表达式或方法引用上堆叠多个装饰器。 优点是您可以选择所需的装饰器,而没有别的。
Resilience4j 2 需要 Java 17。
Resilience4j 提供了几个核心模块:
- resilience4j-circuitbreaker:熔断
- resilience4j-ratelimiter:限流
- resilience4j-bulkhead: 隔离
- resilience4j-retry:自动重试(同步和异步)
- resilience4j-timelimiter:超时处理
- resilience4j-cache:结果缓存
还有用于度量的附加模块、Feign、Kotlin、Spring、Ratpack、Vertx、RxJava2 等。
三个基本状态
CLOSED OPEN HALF_OPEN
转换方式:
-
断路器有三个普通状态: 关闭 (CLOSED) 、开启(OPEN) 、半开(HALF_OPEN),还有两个特殊状态: 禁用(DISABLED)、强制开启(FORCED OPEN)
-
当熔断器关闭时,所有的请求都会通过熔断器
- 如果失败率超过设定的闻值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝,
- 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率
- 如果失败率超过闻值,则变为打开状态,如果失败率低于阔值,则变为关闭状态。
-
断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或基于时间的滑动窗口
- 基于访问数量的滑动窗口统计了最近N次调用返回结果。居于时间的滑动窗口统计最近N秒的调用回结果。
-
除此以外,熔断器还会有两种特殊状态: DISABLED (始终允许访问)和FORCED OPEN (始终拒绝访问)
- 这两个状态不会生成熔断器事件 (除状态装换外),并且不会记录事件的成功或者失败。
- 退出这两个状态的唯一方法是触发状态转换或者重置熔断器。
创建和配置断路器
Config 属性 | 描述 |
---|---|
failure-rate-threshold | 以百分比配置失败率峰值 |
sliding-window-type | 断路器的滑动窗口期类型 可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。 |
sliding-window-size | 若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器; 若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。 |
slowCallRateThreshold | 以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。 |
slowCallDurationThreshold | 配置调用时间的峰值,高于该峰值的视为慢调用。 |
permitted-number-of-calls-in-half-open-state | 运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。 |
minimum-number-of-calls | 在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。 |
wait-duration-in-open-state | 从OPEN到HALF_OPEN状态需要等待的时间 |
COUNT_BASED版本
修改服务提供者8001
1 |
|
修改公共API接口
将8001新增服务接口暴露出来
1 |
|
改POM 80端口服务调用者
修改公共API的POM文件,引入以下文件
1 | <!--resilience4j-circuitbreaker--> |
写YAML 80端口服务调用者
在yaml文件中配置滑动窗口的类型,这里设置为COUNT_BASED
并设置请求窗口大小
最小样本数
是否自动过渡到半关闭状态
开到关的时间
半开状态最大请求数
实例熔断配置
1 | spring: |
新增80端口Controller
@CircuitBreaker
参数:
name: 指定在调用那个服务的时候需要进行保障
fallbackMethod:指定在调用失败的时候处理方法
1 |
|
TIME_BASED版本
基于时间的滑动窗口是通过有N个桶的环形数组实现
如果滑动窗口的大小为10秒,这个环形数组总是有10个桶,每个桶统计了在这一秒发生的所有调用的结果(部分统计结果),数组中的第一个桶存储了当前这一秒内的所有调用的结果,其他的桶存储了之前每秒调用的结果。
滑动窗口不会单独存储所有的调用结果,而是对每个桶内的统计结果和总的统计值进行增量的更新,当新的调用结果被记录时,总的统计值会进行增量更新。
检索快照(总的统计值)的时间复杂度为0(1),因为快照已经预先统计好了,并且和滑动窗口大小无关.
关于此方法实现的空间需求(内存消耗)约等于O)。由于每次调用结果(元组)不会被单独存储,只是对N个桶进行单独统计和一次总分的统计。
每个桶在进行部分统计时存在三个整型,为了计算,失败调用数,慢调用数,总调用数。还有一个ong类型变量,存储所有调用的响应时间。
YAML文件
1 | # Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子 |
总结
当满足一定的峰值和失败率达到一定条件后,断路器将会进入OPEN状态(保险丝跳闸),服务熔断
当OPEN的时候,所有请求都不会调用主业务逻辑方法,而是直接走fallbackmetnod兜底背锅方法,服务降级
段时间之后,这个时候断路器会从OPEN进入到HALF OPEN半开状态,会放几个请求过去探探链路是否通?
- 如成功,断路器会关闭CLOSE(类似保险丝闭合,恢复可用);
- 如失败,继续开启。重复上述
BulkHead
官网Bulkhead
含义:bulkhead(船的)舱壁/(飞机的)隔板
隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
是什么:限并发
能干嘛:依赖隔离&负载保护:用来限制对于下游服务的最大并发数量的限制
Resilience4j 提供了两种舱壁模式的实现,可用于限制并发执行的数量:
- 使用信号量
SemaphoreBulkhead
- 使用有界队列和固定线程池。
FixedThreadPoolBulkhead
SemaphoreBulkhead
概述:
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
测试:
在8001中添加微服务接口
1 | //=========Resilience4j bulkhead 的例子 |
在公共模块APIS中添加对外暴露的接口
1 | /** |
修改服务请求者的POM文件
1 | <!--resilience4j-bulkhead--> |
修改服务请求者的YAML文件
1 | #resilience4j bulkhead 的例子 |
调用方式Controller
1 | /** |
FixedThreadPoolBulkhead
概述:
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
测试:
POM文件:
1 | <!--resilience4j-bulkhead--> |
YAML 文件
配置名称 | 默认值 | 含义 |
---|---|---|
maxThreadPoolSize | Runtime.getRuntime().avaliableProcessors() | 配置最大线程池大小 |
coreThreadPoolSize | Runtime.getRuntime().avaliableProcessors()-1 | 配置核心线程池大小 |
queueCapacity | 100 | 配置队列的容量 |
keepAliveDuration | 20ms | 当前线程数大于核心的时候,这是多余线程 在终止前等待任务的最长时间 |
1 | ####resilience4j bulkhead -THREADPOOL的例子 |
调用Controller
将Type设置为:THREADPOOL
1 | /** |
RateLimiter
限流官网:TimeLimiter
限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或待、降级等处理
限流算法
-
漏斗算法:Leaky Bucket
一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。
如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。
缺点:
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
-
令牌桶算法(Token Bucket)SpringBoot默认算法
-
滚动时间窗算法
允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。
由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次,but…
缺点:
间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮,例如在间隔端最后一次性全部涌入,会导致系统压垮
-
滑动时间窗口(sliding time window)
顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:
- 窗口:需要定义窗口的大小
- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,
不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次
测试
修改服务提供者8001
1 | //=========Resilience4j ratelimit 的例子 |
在公共模块APIS中添加对外暴露的接口
1 | /** |
修改服务请求者的POM文件
添加ratelimiter依赖
1 | <!--resilience4j-ratelimiter--> |
修改服务请求者的YAML文件
1 | ####resilience4j ratelimiter 限流的例子 |
增加服务请求者的Controller方法
使用@RateLimiter
1 |
|
服务链路追踪
分布式链路追踪
分布式所面临的的问题:
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
需要解决的问题:
- 如何实时观测系统的整体调用链路情况。
- 如何快速发现并定位到问题。
-
如何尽可能精确的判断故障对系统的影响范围与影响程度。
-
如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
-
如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
-
如何尽可能精确的分析系统的存储瓶颈与容量规划。
解决方法:
分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
Spring Cloud Sleuth(micrometer)提供了一套完整的分布式链路追踪 (Distributed Tracing)解决方案且兼容支持了zipkin展现
micrometer负责收集链路信息,zipkin负责展示信息
分布式链路追踪原理
一条链路追踪会在每个服务调用的时候加上Trace ID 和 Span ID
链路通过TraceId唯一标识,
Span标识发起的请求信息,各span通过parent id 关联起来 (Span:表示调用链路来源,通俗的理解span就是一次请求信息)
CS: Client Sent: 客户端发送这个请求的时间
CR: Client Reeceived: 客户端接受到数据的时间
SS: Server Sent: 服务端发送响应的时间
SR: Server Received: 服务端接受到这个请求的时间
SR-CS:服务端接收到该请求的时间 - 客户端发送请求的时间 = 网络传输时间
SS - SR:服务端发送相应的时间 - 服务端接收到请求的时间 = 业务处理时间
CR - CS:客户端接收到数据的时间 - 客户端发送请求的时间 = 远程调用耗时
CR - SS:客户端收到数据的时间 - 服务器端发送请求的时间 = 网络传输时间
一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来,通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。
Zipkin
Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。
使用:
1 | java -jar zipkin-server-3.1.1-exec.jar |
默认网址: http://127.0.0.1:9411/
Micrometer整合Zipkin实现链路监控
Micrometer:链路采样
Zipkin:图形展示
父工程POM
引入依赖的原因
由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:
序号 | 名称 | 描述 |
---|---|---|
1 | micrometer-tracing-bom | 导入链路追踪版本中心,体系化说明 |
2 | micrometer-tracing | 指标追踪 |
3 | micrometer-tracing-bridge-brave | 一个Micrometer模块,用于与分布式跟踪工具 Brave 集成,以收集应用程序的分布式跟踪数据。 Brave是一个开源的分布式跟踪工具,它可以帮助用户在分布式系统中跟踪请求的流转, 它使用一种称为"跟踪上下文"的机制,将请求的跟踪信息存储在请求的头部, 然后将请求传递给下一个服务。在整个请求链中,Brave会将每个服务处理请求的时间和其他 信息存储到跟踪数据中,以便用户可以了解整个请求的路径和性能。 |
4 | micrometer-observation | 一个基于度量库 Micrometer的观测模块,用于收集应用程序的度量数据。 |
5 | feign-micrometer | 一个Feign HTTP客户端的Micrometer模块,用于收集客户端请求的度量数据。 |
6 | zipkin-reporter-brave | 一个用于将 Brave 跟踪数据报告到Zipkin 跟踪系统的库。 |
补充包:spring-boot-starter-actuator SpringBoot框架的一个模块用于监视和管理应用程序
1 | <micrometer-tracing.version>1.2.0</micrometer-tracing.version> |
服务提供者8001
POM文件:
1 | <!--micrometer-tracing指标追踪 1--> |
YAML文件:
1 | # ========================zipkin=================== |
新建Controller测试
1 |
|
公共APIS添加暴露接口
1 | /** |
调用者80
POM文件:
1 | <!--micrometer-tracing指标追踪 1--> |
YAML文件:
1 | # zipkin图形展现地址和采样率设置 |
测试业务类Controller
1 | /** |
服务网关 GateWay
概述
是什么
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
所有的请求都要先经过Gateway,处理之后再发送给后面的微服务框架
微服务网关所处位置
作用
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
总结
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。
Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
三大核心
- 路由 router:网关的基本构建块。 它由 ID、目标URI、一系列断言和过滤器组成。如果断言为 true,则匹配路由。
- 断言 Predicate:这是一个 Java 8中 java.util.function.Predicate。 可以匹配 HTTP 请求中的任何内容,例如标头或参数。
- 过滤器 Filter:这些是使用特定工厂构建的GatewayFilter实例。 使用过滤器,可以在发送请求被路由之前或之后修改请求和响应。
对应的三个功能
-
web前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
-
predicate就是我们的匹配条件;
-
filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用
配置
建Module
cloud-gateway9527
改POM
1 | <dependencies> |
写YAML
将该服务入驻Consul
1 | server: |
主启动
1 | package com.atguigu.cloud; |
路由映射
8001服务准备PayGatewayController
1 |
|
修改Gateway的YAML文件
1 | server: |
测试
没有添加网关前:http://localhost:8001/pay/gateway/get/1
添加网关后:http://localhost:9527/pay/gateway/get/1
通过9527端口来替换8001端口
Gateway网关将后端真实地址8001替换成了9527
通过Feign配合GateWay来访问
修改公共APIS中的服务名称,改为GateWay的服务名称,通过调用GateWay中的服务,进而来调用8001服务提供者的服务
1 | // @FeignClient("cloud-payment-service") |
高级特性
Route以微服务名-动态获取服务URL
上述方法存在问题:在9524GateWay的配置文件中后端服务端口地址写死了,需要改成服务名,动态获取URL
问题写法===http://localhost:8001
1 | gateway: |
优化之后===lb://cloud-payment-service
1 | gateway: |
Predicate断言
Route决定前台页面能不能够找到
Predicate决定前台页面能不能访问
是什么
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多Route Predicate工厂可以进行组合
Spring Cloud Gateway 创建 Route 对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性多种谓词工厂可以组合,并通过逻辑and。
配置方法:
Shortcut Configuration:缩写
Fully Expanded Argument:全写,KeyValue
常用APIS
-
AfterRoutePredicateFactory,在一个时间之后才能访问
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- After=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai] -
BeforeRoutePredicateFactory,在一个时间之前能访问
1
2
3
4
5
6
7
8
9
10spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- After=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai]
- Before=2024-04-05T22:22:15.037109200+08:00[Asia/Shanghai] -
BetweenRoutePredicateFactory,在一个时间之前能访问
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Between=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai],Before=2024-04-05T22:22:15.037109200+08:00[Asia/Shanghai] -
CookieRoutePredicateFactory
两个参数:
第一个:Cookie name
第二个:正则表达式
路由规则会通过获取对应的 Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Cookie=Cookiename, Re表达式 -
HeaderRoutePredicateFactory
两个参数
第一个头部参数名
第二个正则表达式
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Header=X-Request-Id, Re表达式 #请求头要有X-Request-Id属性并且值为正则表达式 -
HostRoutePredicateFactory
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符它通过参数中的主机地址作为匹配规则
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Host=**.somehost.org, **.anotherhost.org -
PathRoutePredicateFactory
路径相匹配的在进行访问
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/** -
QueryRoutePredicateFactory
请求参数必须符合条件才能访问
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Query=参数名, 参数值的正则表达式 -
RemoteAddrRoutePredicateFactory
访问的IP地址必须是特定的才行
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- RemoteAddr= 192.168.1.1/24 # 网络前缀必须是192.168.1才能访问 -
MethodRoutePredicatet
特定方法才能访问
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1
uri: lb://cloud-payment-service
predicates:
- Method= GTE, POST -
自定义规则
自带规则无法满足需求
要么继承AbstractRoutePredicateFactory抽象类
要么实现RoutePredicateFactory接口
-
新建类名XXX需要以RoutePredicate结尾并继承AbstractRoutePredicateFactory类
1
2public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
} -
重写Apply方法
1
2
3
4
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactor.Config config) {
return null;
} -
新建apply方法所需要的静态内部类MyRoutePredicateFactory.Config,这个Config类就是我们的路由断言规则
1
2
3
4
5
6
7
8
9public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
public static class Config {
private String userType;
}
} -
空参构造方法,内部调用super
1
2
3
4
5
6
public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
public MyRoutePredicateFactor() {
super(MyRoutePredicateFactor.Config.class);
}
} -
重写Apply方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactor.Config config) {
return new Predicate<ServerWebExchange>() {
public boolean test(ServerWebExchange serverWebExchange) {
String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
if(userType == null){ // 如果没有带参数直接返回false
return false;
}
// 参数存在,就和Config中的内容进行比较
if(userType.equalsIgnoreCase(config.getUserType())){
return true;
}
return false;
}
};
} -
测试:YAML文件
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh2 #pay_routh2
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
- My=diamond上述方式会出错,因为自己设计的断言不支持短格式
完整格式:
1
2
3
4
5
6
7
8
9spring:
cloud:
gateway:
routes:
- id: pay_routh2 #pay_routh2
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
- name:My在我们自定义Predicate类中添加shortcutFieldOrder,就支持短格式了
1
2
3public List<String> shortcutFieldOrder() {
return Collections.singletonList("userType");
}
-
Filter 过滤器
“pre”和“post”分别会在请求被执行前调用和被执行后调用用来修改请求和响应信息
作用:请求鉴权,异常处理
有三种类型
-
全局默认过滤器
gateway出厂默认已有的,直接用即可,主要作用于所有的路由
不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可
-
单一内置过滤器
也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组
有多重过滤器,可在官网查验
-
自定义过滤器
内置 请求头相关组(RequestHeader)
-
AddRequestHeader GatewayFilter Factory
在进行访问https://example.org该网址的时候,将请求头中的内容添加一个X-Request-red=blue参数
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue -
RemoveRequestHeader GatewayFilter Factory
移除请求头中的一部分
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-red, blue -
SetRequestHeader GatewayFilter Factory
更新请求头中的一部分
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- SetRequestHeader=X-Request-red, blue
内置 请求参数相关组 (RequestParameter)
-
AddRequestParameter GatewayFilter Factory
添加请求参数
如果请求中含有相同名称的参数,则按照请求头中的参数为依据
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestParameter=X-Request-red, blue -
RemoveRequestParameter GatewayFilter Factory
删除请求参数
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- RemoveRequestParameter=X-Request-red, blue
内置 响应头相关组(ResponseHeader)
-
AddResponseHeader GatewayFilter Factory
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Request-red, blue -
RemoveResponseHeader GatewayFilter Factory
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- RemoveResponseHeader=X-Request-red, blue -
SetResponseHeader GatewayFilter Factory
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- SetResponseHeader=X-Request-red, blue
前置和路径相关组
-
PrefixPath GatewayFilter Factory
前缀路径添加
1
2
3
4
5
6
7
8
9
10
11
12spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: lb://cloud-payment-service
predicates:
# - Path="/pay/gateway/filter/**" 正确路径
# 即使访问路径不正确,也能通过,在filters中拼接成为正确的访问路径
- Path= "/gateway/filter/**"
filters:
- PrefixPath=/pay -
SetPath GatewayFilter Factory
将错误地址转换成正确地址,如果想要错误地址中的数据,需要使用占位符
例如下面,我们可以通过 http://localhost:9527/xyz/abc/filter访问到 http://localhost:9527/pay/gateway/filter
filter被占位符所占用,放到真实的地址中
1
2
3
4
5
6
7
8
9
10
11
12spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: lb://cloud-payment-service
predicates:
# - Path="/pay/gateway/filter/**" 正确路径
# 设置错误的访问路径
- Path= "xyz/abc/{segment}"
filters:
- SetPath=/pay/gateway/{segment} -
RedirectTo GatewayFilter Factory
1
2
3
4
5
6
7
8
9
10spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: lb://cloud-payment-service
predicates:
- Path="/pay/gateway/filter/**"
filters:
- RedirectTo=302, https://www.baidu.com
内置 其他
全局配置,一次配置所有路由生效,不建议
1 | spring: |
自定义全局过滤器
新建类MyGlobalFilter并实现GlobalFilter,Ordered两个接口
实现两个方法filter,getOrder
1 | package com.atguigu.cloud.mygateway; |
自定义单一过滤器
查看自带的单一过滤器
都是继承AbstractNameValueGatewayFilterFactory抽象类,所以自定义单一过滤器也需要继承AbstractNameValueGatewayFilterFactory抽象类
新建类名以GatewayFilterFactory结尾
1 |
|
配置YAML文件
1 | spring: |
SpringCloudAlibaba入门
版本依赖:版本发布说明 | Spring Cloud Alibaba (aliyun.com)
SpringCloudAlibaba-Nacos 服务注册
作用
替代Consul做服务注册中心
替代Consul做服务配置中心和满足动态刷新广播通知
安装
使用:在下载好的Bin文件夹中敲CMD,然后运行startup.cmd -m standalone即可开始
Nacos Discovery服务注册中心
服务提供者
-
新建Module—cloudalibaba-provider-payment9001
-
POM文件:引入Discovery文件
1
2
3
4
5<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>全部POM文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
YAML文件:
1
2
3
4
5
6
7
8
9
10server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址 -
创建主启动类
1
2
3
4
5
6
public class Main9001 {
public static void main(String[] args) {
SpringApplication.run(Main9001.class);
}
} -
创建Controller对象
1
2
3
4
5
6
7
8
9
10
public class PayAlibabaController {
private String serverPort;
public String getPayInfo( Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
} -
启动9001,然后访问http://localhost:8848/nacos点击服务管理即可找到我们注册的nacos-payment-provider
服务消费者
-
创建模块cloudalibaba-consumer-nacos-order83
-
改POM文件
新引入文件依赖
1
2
3
4
5
6
7
8
9
10<!--nacos-discovery Nacos服务发现 将消费者项目注册进Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--loadbalancer 负载均衡 可能一个服务提供者有多个实例,需要使用负载均衡来实现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>总体POM:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML文件
1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
service-url:
nacos-user-service: http://nacos-payment-provider -
业务类
新建config.RestTemplateConfig业务配置
1
2
3
4
5
6
7
8
9
public class RestTemplateConfig {
//赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate()
{
return new RestTemplate();
}
}新建业务类Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderNacosController {
private RestTemplate restTemplate;
private String serverURL;
public String paymentInfo( Integer id)
{
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
}
Nacos支持负载均衡
Nacos Config服务配置中心
之前案例Consul8500服务配置动态变更功能可以被Nacos取代
通过Nacos和spring-cloud-starter-alibaba-nacos-config实现中心化全局配置的动态变更
配置步骤
-
建Module----cloudalibaba-config-nacos-client3377
-
改POM
添加的依赖
1
2
3
4
5
6
7
8
9
10<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>完整的POM文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<dependencies>
<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML文件
Nacos同Consul一样,在项目初始化时,要保证先从配置中心进行配置拉取
拉取配置之后,才能保证项目的正常启动,为了满足动态刷新和全局广播通知
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap.yaml
1
2
3
4
5
6
7
8
9
10
11# nacos配置
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置application.yaml
1
2
3
4
5
6
7
8server:
port: 3377
spring:
profiles:
active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境 -
修改主启动类
1
2
3
4
5
6
public class Main3377 {
public static void main(String[] args) {
SpringApplication.run(Main3377.class, args);
}
} -
配置业务类
@RefreshScope,只要服务器中更改了,本地自动修改
1
2
3
4
5
6
7
8
9
10
11
12
//在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class NacosConfigClientController
{
private String configInfo;
public String getConfigInfo() {
return configInfo;
}
}
在Nacos中添加配置信息
在 Nacos Spring Cloud 中,dataId 的完整格式如下:
1 | ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} |
@RefreshScope注解实现配置的自动更新
添加配置文件:DataID需要是配置文件中配置的内容
当我们修改了配置文件中的内容,那么添加了@RefreshScope注解的配置类,也会自动获取新文件
Nacos配置文件自动保存30天,30天内的文件配置修改都可以回滚
NameSpace-Group-DataId
为什么要设置NameSpace命名空间
问题1:
实际开发中,通常一个系统会准备
dev开发环境
test测试环境
prod生产环境。
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:
一个大型分布式微服务系统会有很多微服务子项目,
每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…
那怎么对这些微服务配置进行分组和命名空间管理呢?
DATAID方案
默认空间public+默认分组DEFAULT GROUP+新建DatalD
通过spring.profile.active属性就能进行多环境下配置文件的读取
bootstrap.yml
1 | # nacos配置 第一种:默认空间+默认分组+新建DataID |
application.yml
1 | server: |
通过不同的测试环境来配置不同的配置变量
GROUP方案
在配置文件中添加一个新的分组
group: PROD_GROUP
bootstrap.yml
1 | # nacos配置 |
application.yml
1 | server: |
Namespace方案
新建命名空间
在新的命名空间中新建配置
修改YAML文件,在config文件中添加一条
namespace:Prod_Namespace
1 | # nacos配置 |
SpringCloudAlibaba Sentinel 熔断限流
功能
官网 home | Sentinel (sentinelguard.io)
作用:从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性
面试题:
-
服务雪崩
因为微服务之间的相互调用,可能因为一个微服务出错,导致整个微服务系统瘫痪,称为服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
-
服务降级
当服务出错的时候有个保底的响应方式
服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。
例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。
-
服务熔断
当服务访问量过大的时候,会自动断开连接,防止服务器崩溃
在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务熔断一般情况下会有三种状态:闭合、开启和半熔断;
闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
-
服务限流
限制服务访问量
服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
-
服务隔离
当服务出错的时候,可使用服务隔离来将出错的服务,进行隔离,从而不影响别的进程
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。
互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。
-
服务超时
当服务相应之间的时间间隔过大的时候会断开服务之间的链接
整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。
形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。
安装
下载:Releases · alibaba/Sentinel (github.com)
运行:java -jar sentinel-dashboard-1.8.7.jar
查看界面:http://localhost:8080/#/dashboard 账号密码默认是sentinel
入门案例
-
启动Nacos
-
启动Sentinel
-
建Module====cloudalibaba-sentinel-service8401
-
改POM
新增依赖
1
2
3
4
5<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>整体POM文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54<dependencies>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 -
主启动类
1
2
3
4
5
6
public class Main8401 {
public static void main(String[] args) {
SpringApplication.run(Main8401.class);
}
} -
业务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FlowLimitController {
public String testA()
{
return "------testA";
}
public String testB()
{
return "------testB";
}
} -
Sentinel采用懒加载,如果接口不被访问,则Sentinel不会监控
流量控制
Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。
资源名 | 资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。 |
---|---|
针对来源 | 具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。 |
阈值类型 | QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。 |
单机阈值 | 与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时, 进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。 |
是否集群 | 选中则表示集群环境,不选中则表示非集群环境。 |
流控模式-直接
设置了阈值类型为QPS,且阈值为2
所以在1s内访问超过两次之后会限流报错
也可以自定义兜底方案,只需要在Controller上设置fallback兜底方案
流控模式-关联
当关联的资源达到阈值时,就限流自己
当与A关联的资源B达到阀值后,就限流A自己
配置
流控模式-链路
来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施,比如C请求来访问就限流,D请求来访问就是OK
针对同一个Service服务资源,两个接口同时调用,使用链路控制的就会限流,不使用链路控制的就不会
测试:
-
新建FlowLimitService,设置值为common,再使用链路限流的的时候的资源名
1
2
3
4
5
6
7
public class FlowLimitService {
public void common() {
System.out.println("------FlowLimitService come in");
}
} -
修改Controller
testc 和 testd都访问flowLimitService的common方法,如果阈值到了就对C限流,对D不限流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FlowLimitController {
private FlowLimitService flowLimitService;
public String testA()
{
return "------testA";
}
public String testB()
{
return "------testB";
}
public String testC()
{
flowLimitService.common();
return "------testC";
}
public String testD()
{
flowLimitService.common();
return "------testD";
}
} -
修改YAML文件web-context-unify: false # controller 层的方法对 service层调用不认为是同一个根链路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
流控效果-直接
直接失败抛出异常
流控效果-预热
我们设置一个阈值,在刚开始的时候并不能直接到达阈值巅峰,而是刚开始的时候阈值比较低,等过一段时间之后在过渡到阈值巅峰
初始阈值为:公式: 阈值除以冷却因子coldFactor(默认值为3),经过预热时长后才会达到阈值
例如上面的,阈值为10,初始阈值为3,经过5s之后阈值才为10
流控效果-排队等待
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下
来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的
请求。
配置:
超时时间:1000ms
单机阈值:1
就是在1000ms中只能有一个请求,多出来的请求需要往后排队
熔断
熔断降级 · alibaba/Sentinel Wiki · GitHub
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
慢调用比例
选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
当访问请求大于RT的时候,该请求被标记为慢调用,我们在请求数大于最小请求数的时候熔断才会生效,在统计时长内达到最小请求数量,且阈值大于设定的阈值的时候,会被熔断
异常比例
当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
上面的图片表明在1s内请求数大于5,且异常请求大于10% 那么在接下来的5s内请求会被熔断
异常数
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
统计异常数量,如果超过两个的话则会进行熔断
@SentinelResource注解
SentinelResource是个流量防卫防护组件注解,用于指定防护资源,,对配置的资源进行流量控制、熔断降级等功能。
注解说明:
1 |
|
按照rest地址限流+默认限流返回
按照路径限流
该注解没有引入SentinelResource注解,多次访问会出现Blocked by Sentinel (flow limiting)
测试:没有引入SentinelResource注解
1 |
|
按照SentinelResource资源名称限流+自定义限流返回
主要是SentinelResource接口中的value的值,通过设置value值来判断那个元素应该被限流
使用@SentinelResource注解来标识资源名
使用blockHandler来自定义处理器
1 |
|
按照SentinelResource资源名称限流+自定义限流返回+服务降级处理
通过fallback来进行服务降级处理
1 |
|
配置规则:
如果多次点击触发限流:会使用blockHandler来处理
如果程序出错会到时:fallback来处理异常
blockHandler,主要针对sentinel配置后出现的违规情况处理
fallback,程序异常了JVM抛出的异常服务降级
两者可以同时共存
热点规则
热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作
代码
1 |
|
代码配置图:
限流模式只支持QPS模式,固定写死了。(这才叫热点)
@SentinelResource注解的方法参数索引,0代表第一个参数,1代表第二个参数,以此类推
单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。
下面面的抓图就是第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandler_testHotKey支持方法。
当我们设置好p1的限流时访问:http://localhost:8401/testHotKey?p1=1,当超过1s1次的时候就会被限流,只要我们访问的时候有p1这个参数,就会被限流,当我们访问的时候没有p1这个参数的时候就不会被限流
例外
设置p1的值为特定的值的时候,我们将其访问设置为不限流
一定要点击添加
授权规则
我们需要根据来源名单判断是否能放行,设置黑白名单,黑名单不能访问,白名单能访问
新建EmpowerController
1 |
|
新建处理器MyRequestOriginParser
handler.MyRequestOriginParser处理器,实现RequestOriginParser接口,实现parseOrigin方法
设置请求参数名:serverName
1 |
|
配置授权
配置流控应用test,test2设置为黑名单
我们在RequestOriginParser处理器中配置了参数名为serverName
所以我们在请求的时候,如果参数中含有serverName=test 或者 serverName=test1的情况,则直接禁止访问
规则持久化
一旦我们重启微服务应用,sentinel规则将消失,生产环境需要将配置规则进行持久化
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
步骤
修改cloudalibaba-sentinel-service8401
-
修改POM文件
添加依赖项
1
2
3
4
5<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>完整的POM文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54<dependencies>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
改YAML文件
1
2
3
4
5
6
7
8
9
10
11
12spring:
cloud:
sentinel:
datasource:
ds1: # 自定义key,也可以做限流类型
nacos:
server-addr: localhost:8848 # nacos地址
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType 流控规则rule-type参数
1
2
3
4
5
6
7public enum RuleType {
FLOW("flow", FlowRule.class), // 流量控制规则
DEGRADE("degrade", DegradeRule.class), // 熔断降级规则
PARAM_FLOW("param-flow", ParamFlowRule.class), // 热点规则
SYSTEM("system", SystemRule.class), // 系统保护规则
AUTHORITY("authority", AuthorityRule.class), // 访问权限控制规则
} -
添加Nacos业务规则配置
创建配置:
1
2
3
4
5
6
7
8
9
10
11[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]resource:资源名称;
limitApp:来源应用;
grade:阈值类型,0表示线程数,1表示QPS;
count:单机阈值;
strategy:流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
clusterMode:是否集群。
OpenFeign整合Sentinel
需求:访问者要有fallback服务降级的情况,不要持续访问9001加大微服务负担,但是通过feign接口调用的又方法各自不同, 如果每个不同方法都加一个fallback配对方法,会导致代码膨胀不好管理,工程埋雷
解决方式:通过fallback属性进行统一配置,feign接口里面定义的全部方法都走统一的服务降级,一个搞定即可
9001微服务自身还带着sentinel内部配置的流控规则,如果满足也会被触发
-
OpenFeign接口的统一fallback服务降级处理
-
Sentinel访问触发了自定义的限流配置,在注解@SentinelResource里面配置的blockHandler方法。
步骤1 修改服务提供方9001
-
引入依赖
1
2
3
4
5
6
7
8
9
10<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency> -
修改YAML,引入Sentinel配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 -
修改Controller,方便测试,只有blockHandler方法,没有fallback方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public ResponseResult getPayByOrderNo( String orderNo)
{
//模拟从数据库查询出数据并赋值给DTO
PayDTO payDTO = new PayDTO();
payDTO.setId(1024);
payDTO.setOrderNo(orderNo);
payDTO.setAmount(9.9);
payDTO.setPayNo("pay:"+ IdUtil.fastUUID());
payDTO.setUserId(1);
return ResponseResult.okResult("查询返回值:"+payDTO);
}
public ResponseResult handlerBlockHandler( String orderNo, BlockException exception)
{
return ResponseResult.errorResult(AppHttpCodeEnum.BAD_REQUEST,"getPayByOrderNo服务不可用," +
"触发sentinel流控配置规则"+"\t"+"o(╥﹏╥)o");
}
步骤2 修改cloud-api-commons
通过Feign将我们设置的两个接口暴露出去
先在cloud-api-commons添加依赖
1 | <!--openfeign--> |
新增PayFeignSentinelApi接口,兜底方法为PayFeignSentinelApiFallBack类中的方法
1 |
|
新建PayFeignSentinelApiFallBack类,实现PayFeignSentinelApi接口,并重写对应方法,对应方法出错之后调用该兜底方法
1 |
|
步骤3 修改服务消费者
修改cloudalibaba-consumer-nacos-order83通过PayFeignSentinelApi去调用9001
-
修改POM文件
新添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>整体POM文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52<dependencies>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
修改YAML,添加支持
1
2
3
4# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true -
修改Controller
1
2
3
4
5
6
7
8
9
10
11
12
public class OrderNacosController
{
private PayFeignSentinelApi payFeignSentinelApi;
public ResponseResult getPayByOrderNo( String orderNo)
{
return payFeignSentinelApi.getPayByOrderNo(orderNo);
}
} -
修改整体的SpringBoot Cloud 版本,为了整合Alibaba
1
2
3
4
5
6<!-- 正常版本 -->
<!-- <spring.boot.version>3.2.0</spring.boot.version> -->
<!-- <spring.cloud.version>2023.0.0</spring.cloud.version> -->
<!-- SpringBoot Cloud Alibaba适配版本 -->
<spring.boot.version>3.0.9</spring.boot.version>
<spring.cloud.version>2022.0.2</spring.cloud.version> -
修改主启动类,添加@EnableFeignClients注解
1
2
3
4
5
6
7
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class);
}
}
GateWay整合Sentinel
实例DEMO
在服务提供者9001,之前再加一层网关
cloudalibaba-sentinel-gateway9528 保护 cloudalibaba-provider-payment9001
建Module
cloudalibaba-sentinel-gateway9528
改POM文件
1 | <dependencies> |
写YAML
1 | server: |
主启动
1 |
|
配置类
config.GatewayConfiguration
1 |
|
SpringCloudAlibaba Seata分布式
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
but
关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
分布式事务解决的问题:
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。
此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
**需求:**希望提供一中分布式事务框架,解决微服务架构下的分布式事务问题
概述
Simple Extensible Autonomous Transaction Architecture:简单可扩展自治事务框架
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
下载地址:Release v2.0.0 · apache/incubator-seata · GitHub
@Transactional控制本地事务
@GlobalTransactional 控制全局事务
TC TM RM分别表示什么意思
工作流程
纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知
Seata对分布式事务协调和控制就是1+3
一个XID是全局事务的唯一标识,他可以在服务的调用链路中传递,绑定到服务的事务的上下文
三个TC TM RMSeata术语表 | Apache Seata
TC (Transaction Coordinator) - 事务协调者
- 维护全局和分支事务的状态,驱动全局事务提交或回滚
- 有且仅有一个
- 就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚
TM(Transaction Manager) - 事务管理器
- 定义全局事务的范围: 开始全局事务、提交或回滚全局事务
- 有且仅有一个
- 标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议
RM(Resource Manager) - 资源管理器
- 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态
- 可以有多个
- 就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向 TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚
执行流程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
安装
-
下载地址:seata下载地址
-
新人文档:新人文档 | Apache Seata
-
建立数据库:
-
CREATE DATABASE seata; USE seata; <!--code186-->
-
-
更改配置
Seata下载文件夹下的conf下的application.yml中的内容,记得先备份
按照application.example.yml的样式配置
将模式改成db模式
修改完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${log.home:${user.home}/logs/seata}
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
namespace:
cluster: default
username: nacos
password: nacos
store:
# support: file 、 db 、 redis 、 raft
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
user: mysql
password: mysql
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/** -
启动Nacos,startup.cmd -m standalone 和Seata seata-server.bat
-
seata初始密码:seata
实战案例-数据库准备
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
创建数据库
1 | CREATE DATABASE seata_order; |
创建对应的回滚表
应为需要三个数据库的事务,要么同时成功,要么全部回滚,这时候每个数据库都需要一个回滚表
1 | CREATE TABLE IF NOT EXISTS `undo_log` |
分别在三个数据库中都建立该回滚表
建立业务表
seata_order数据库:
1 | CREATE TABLE t_order( |
seata_account数据库
1 | CREATE TABLE t_account( |
seata_storage数据库
1 | CREATE TABLE t_storage( |
实战案例-微服务编码
业务需求:下订单 减库存 扣余额 改订单状态
通过MybatisPlus生成entity和mapper
FeignApi接口
新增cloud-api-commons新增库存和账户两个Feign两个接口
1 |
|
Order微服务
-
建Module seata-order-service2001
-
改POM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML
新内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # 将seata注册进seata
namespace: "" # seata三件套
group: SEATA_GROUP # seata三件套
application: seata-server # seata三件套
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
# seata的日志级别
logging:
level:
io:
seata: info1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: admin
# ========================mybatis-plus===================
mybatis-plus:
# 匿名域
type-aliases-package: com.atguigu.cloud.entity
# mapper文件映射路径
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
# # 日志文件
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: true
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info -
主启动类:
1
2
3
4
5
6
7
8
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class, args);
}
} -
Service层方法
OrderService
1
2
3
4public interface OrderService extends IService<Order> {
public void create(Order order);
}OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private OrderMapper orderMapper;
// 订单微服务通过OpenFeign去调用库存微服务
private StorageFeignApi storageFeignApi;
// 订单微服务通过OpenFeign去调用账户微服务
private AccountFeignApi accountFeignApi;
public void create(Order order) {
// xid全局事务id检查, 重要!!!
String xid = RootContext.getXID();
// 1、新建订单
log.info("---------------开始新建订单: " + "\t" + "xid: " + xid);
// 订单初始状态为0
order.setStatus(0);
// 保存订单,此时订单实体已经有数据库主键了
save(order);
log.info("---------------新建订单成功: " + "\t" + "orderInfo: " + order);
// 库存操作
log.info("---------------开始调用Storage库存,扣减Count");
storageFeignApi.decrease(order.getProductId(), order.getCount());
log.info("---------------开始调用Storage库存,扣减Count完成");
// 账户余额操作
log.info("---------------开始调用Account余额,扣减余额");
accountFeignApi.decrease(order.getUserId(), order.getMoney());
log.info("---------------结束调用Account余额,扣减余额");
// 修改订单状态,将0改为1
log.info("---------------开始修改订单状态");
order.setStatus(1);
updateById(order);
log.info("---------------结束修改订单状态");
log.info("---------------结束新建订单: " + "\t" + "xid: " + xid);
}
} -
Controller层
1
2
3
4
5
6
7
8
9
10
11
12
public class OrderController {
private OrderService orderService;
public ResponseResult create(Order order)
{
orderService.create(order);
return ResponseResult.okResult(order);
}
}
Storage微服务
-
建Module seata-storage-service2022
-
改POM文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: admin
# ========================mybatis-plus===================
mybatis-plus:
# 匿名域
type-aliases-package: com.atguigu.cloud.entity
# mapper文件映射路径
mapper-locations: classpath:mapper/*.xml
configuration:
# # 日志文件
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: true
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info -
主启动
1
2
3
4
5
6
7
8
public class SeataStorageMain2022 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMain2022.class, args);
}
} -
服务类
StorageService
1
2
3public interface StorageService extends IService<Storage> {
void decrease(; Long productId, Integer count)
}StorageServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {
/**
* 扣减库存
*/
public void decrease({ Long productId, Integer count)
LambdaQueryWrapper<Storage> queryWrapper = new LambdaQueryWrapper<Storage>();
queryWrapper.eq(Storage::getProductId, productId);
Storage storage = getOne(queryWrapper);
storage.setTotal(storage.getTotal()-count);
updateById(storage);
}
} -
Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StorageController
{
private StorageService storageService;
/**
* 扣减库存
*/
public ResponseResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return ResponseResult.okResult("扣减库存成功!");
}
}
Account微服务
-
建Module seata-account-service2003
-
改POM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
写YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: admin
# ========================mybatis-plus===================
mybatis-plus:
# 匿名域
type-aliases-package: com.atguigu.cloud.entity
# mapper文件映射路径
mapper-locations: classpath:mapper/*.xml
configuration:
# # 日志文件
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: true
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info -
主启动
1
2
3
4
5
6
7
8
public class SeataAccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMain2003.class, args);
}
} -
Service服务提供
AccountService
1
2
3
4
5
6
7
8public interface AccountService extends IService<Account> {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 本次消费金额
*/
void decrease(; Long userId, Long money)
}AccountServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
public void decrease(Long userId, Long money) {
LambdaQueryWrapper<Account> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Account::getUserId, userId);
Account account = getOne(queryWrapper);
account.setResidue(account.getResidue() - money);
updateById(account);
}
} -
Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountController {
private AccountService accountService;
/**
* 扣减账户余额
*/
public ResponseResult decrease({ Long userId, Long money)
accountService.decrease(userId,money);
return ResponseResult.okResult("扣减账户余额成功!");
}
}
测试
没有添加@GlobalTransactional
在没有添加@GlobalTransactional的时候
当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1
但是三个数据库都已经产生效果
但是出错之后没有回退情况,还是会保持原先已经产生的效果
所以在没有添加@GlobalTransactional的时候如果一程序出错,不会发生回退情况
添加@GlobalTransactional
添加全局事务注解:设置名称,和异常处理方法
1 |
1 |
|
此时订单模块是TM,也是其中一个RM,谁使用@GlobalTransactional注解,谁就是TM
Seata通过全局锁XID将三个事务连接在一起
三个事务通过XID绑定在一起
添加了@GlobalTransactional之后,在数据库发生改变的时候,会将原先的操作存放在undo_log表中,方便事务回滚,当回滚完事务的时候,undo_log表中的数据就会被删除
机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段:
- 提交异步化,非常快速的完成
- 回滚通过第一阶段的回滚日志进行反向补偿,undo_log作用:回滚日志,反向补偿
在一阶段,Seata 会拦截“业务 SQL”,
-
解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
-
执行“业务 SQL”更新业务数据,在业务数据更新之后,
-
其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如是顺利提交的话
因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
顺利提完完毕之后:undo_log会将回滚记录删除
二阶段回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。