目录
版本说明:
SpringBoot、SpringCloud、SpringCloudAlibaba 版本对应关系:
地址:版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub
这里我使用的版本
SpringBoot 版本:2.6.11
SpringCloud 版本:2021.0.4
SpringCloudAlibaba 版本:2021.0.4.0
IDEA 工具版本:2022.3
并且使用了新 UI
学习视频链接:
动力节点最新SpringCloud视频教程|最适合自学的springcloud+springcloudAlibaba_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1f94y1U7AB/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.clickSpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务_哔哩哔哩_bilibili
https://www.bilibili.com/video/BV1LQ4y127n4/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click【最新版】Spring Cloud Alibaba全面讲解,通俗易懂_哔哩哔哩_bilibili
https://www.bilibili.com/video/BV1Mt4y1i7JW/?spm_id_from=333.788.top_right_bar_window_custom_collection.content.click&vd_source=2b5b9617ba610ad999ce7f88ef05a4b5尚硅谷SpringCloud框架开发教程(SpringCloudAlibaba微服务分布式架构丨Spring Cloud)_哔哩哔哩_bilibili
https://www.bilibili.com/video/BV18E411x7eT/?from=search&seid=13890042962702708031&spm_id_from=333.337.0.0
一、Nacos
1.1 下载 Nacos
下载地址:
Releases · alibaba/nacos · GitHubhttps://github.com/alibaba/nacos/releases
1.2 启动 Nacos Server
- 注:Nacos 的运行需要至少以 2Cg60g*3 的机器配置下运行。
- 进入解压后文件夹或编译打包好的文件夹,找到如下相对文件夹 nacos/bin,并对照操作系统实际情况执行如下命令。
Linux/Unix/Mac
启动命令(standalone 代表着单机模式运行,非集群模式):
sh startup.sh -m standalone
如果您使用的是 ubuntu 系统,或者运行脚本报错提示符号找不到,可尝试如下运行:
bash startup.sh -m standalone
Windows
启动命令(standalone 代表着单机模式运行,非集群模式):
startup.cmd -m standalone
1.3 WEB 控制台
Nacos Service 启动成功之后用浏览器打开如下地址进行登录。
localhost:8848/nacos
- 默认用户名:nacos
- 默认的密码:nacos
控制台管理界面
1.4 服务管理
1.4.1 环境准备
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 创建子工程 consumer-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.4.2 服务注册
- 在 provider-service/pom.xml 文件中添加以下依赖
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 运行 provider-service 中的启动类
- 在 nacos 控制台查看服务是否注册成功
注册成功!
1.4.3 运行多个实例
在 nacos 控制台查看多个实例是否注册成功
多个实例注册成功!
1.4.4 设置集群
我们将 provider-service 的两个实例分别设置为两个不同的集群。
8020 端口设置 上海 集群。
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
discovery:
cluster-name: SH #上海集群
- 重启 8020 端口的服务
8021 端口设置 杭州 集群。
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8021 #端口
spring:
application:
name: provider-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
discovery:
cluster-name: HZ #杭州集群
- 重启 8021 端口的服务
集群设置好后,我们到 nacos 控制台查看
集群设置成功!
1.4.5 环境隔离-namespace
- 在 nacos 控制台可以创建 namespace,用来隔离不同环境。
- 填写一个新的命名空间信息
- 保存后会在控制台看到这个命名空间的 id
- 创建一个新的 provider-service 服务实例
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8022 #端口
spring:
application:
name: provider-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
discovery:
namespace: e4dca480-8405-47d8-b7cf-15b53400b0a0 #命名空间的id
- 运行 8082 端口的服务
- 运行成功后,到 nacos 控制台查看
这时,如果两个服务的 namespace 不同,那么他们是不能进行访问的。
1.4.6 服务发现
- 在 consumer-service/pom.xml 文件中添加以下依赖
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 consumer-service 中的 application.yml 文件中进行配置
server:
port: 8030 #端口
spring:
application:
name: consumer-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 在 consumer-service 中编写 ConsumerController 类
@RestController
public class ConsumerController {
@Autowired
private DiscoveryClient discoveryClient; // 注入服务发现注解
@GetMapping("/listInstance")
public List<ServiceInstance> listInstance() {
return discoveryClient.getInstances("provider-service"); // 根据实例名称拿到实例集合
}
}
- 运行 consumer-service 中的启动类
- 测试
1.5 配置管理
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.5.2 统一配置管理
- 在 nacos 控制台添加配置信息
- 在弹出的表单中填写配置信息
- 在 provider-service/pom.xml 文件中添加以下依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--2020.X.X版本官方重构了bootstrap引导配置的加载方式,需要添加以下依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
- 在 provider-service 中的 bootstrap.yml 文件中进行配置
这个文件是引导文件,优先级高于 application.yml
spring:
application:
name: provider-service #服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
config:
file-extension: yaml #文件后缀名
namespace: e4dca480-8405-47d8-b7cf-15b53400b0a0 #命名空间的id
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/dateformat")
public String dateformat() {
return this.dateformat;
}
}
- 运行 provider-service 中的启动类
- 测试
1.5.2 配置自动刷新
Nacos 中的配置文件变更后,微服务无需重启就可以感知。不过需要通过以下两种配置实现:
方式一:在 @Value 注入的变量所在类上添加注解 @RefreshScope
@RestController
@RefreshScope
public class ProviderController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/dateformat")
public String dateformat() {
return this.dateformat;
}
}
- 重启 8020 端口的服务
- 修改 nacos 控制台里的 provider-service-dev.yaml 配置文件
- 测试
方式二:使用 @ConfigurationProperties 注解
- 在 provider-service 中编写 Patternproperties 类
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
// 这里的set/get方法可以使用lombok进行简化
public String getDateformat() {
return dateformat;
}
public void setDateformat(String dateformat) {
this.dateformat = dateformat;
}
}
- 修改 provider-service 中的 ProviderController 类
@RestController
public class ProviderController {
@Autowired
private PatternProperties patternProperties;
@GetMapping("/dateformat")
public String dateformat() {
return this.patternProperties.getDateformat();
}
}
- 重启 8020 端口的服务
- 修改 nacos 控制台里的 provider-service-dev.yaml 配置文件
- 测试
1.5.3 多种配置的优先级
1.5.4 多环境配置共享
微服务启动时会从 nacos 读取多个配置文件:
- [spring.application.name]-[spring.profiles.active].yaml,例如:provider-service-dev.yaml
- [spring.application.name].yaml,例如:provider-service.yaml
无论 profile 如何变化,[spring.application.name].yaml 这个文件一定会加载,因此多环境共享配置可以写入这个文件,建议将多个环境相同的配置写入到这个文件。
多环境:开发环境、测试环境、生产环境
1.5.5 多服务共享配置
不同微服务之间可以共享配置文件,通过下面两种方式来指定:
方式一:
spring:
application:
name: provider-service # 服务名称
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 多微服务间共享的配置列表
- dataId: common.yaml # 要共享的配置文件id
方式二:
spring:
application:
name: provider-service # 服务名称
profiles:
active: dev # 环境,
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
extends-configs: # 多微服务间共享的配置列表
- dataId: extend.yaml # 要共享的配置文件id
1.6 集群搭建
- 新建一个 nacos 数据库,并运行 nacos/conf/nacos-mysql.sql 文件
- 将 nacos 文件夹复制成多份
- 修改两个文件夹 conf/application.properties 文件
- 把两个文件夹下 conf/cluster.conf.example 文件复制一份并重命名为 cluster.conf,然后进行修改
- 双击两个文件夹下 bin/startup.cmd 进行启动
- 启动成功
- 在 nacos 控制台查看
二、Loadbalancer
LoadBalancer 只提供了三种负载均衡策略。
- RandomLoadBalancer:随机
- NacosLoadBalancer:基于 Nacos 权重
- RoundRobinLoadBalancer:轮询(默认)
可以通过设置来禁用 Spring Cloud LoadBalancer。
spring:
cloud:
loadbalancer:
enabled: false
2.1 环境准备
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@Value("${server.port}")
private String port;
@GetMapping("/getPort")
public String getPort() {
return this.port;
}
}
- 运行多个实例
- 创建子工程 consumer-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 consumer-service 中的 application.yml 文件中进行配置
server:
port: 8025 #端口
spring:
application:
name: consumer-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 在 consumer-service 中编写 ConsumerConfig 类
@Configuration
public class ConsumerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 在 consumer-service 中编写 ConsumerController 类
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getPort")
public String getPort() {
return restTemplate.getForObject("http://provider-service/getPort", String.class);
}
}
2.2 轮询负载均衡策略
- 在 consumer-service/pom.xml 文件中添加以下依赖
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 运行 consumer-service/pom.xml 中的启动类
- 测试
2.3 随机负载均衡策略
- 在 consumer-service/pom.xml 文件中添加以下依赖
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 在 consumer-service 中编写 CustomLoadBalancerConfiguration 类
public class CustomLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
- 修改 consumer-serice 中的 ConsumerConfig 类
@Configuration
// xxx 服务使用 xxx 负载均衡策略
@LoadBalancerClient(value = "provider-service", configuration = CustomLoadBalancerConfiguration.class)
public class ConsumerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 运行 consumer-service 中的启动类
- 测试 http://localhost:8025/getPort
经过测试可以发现每次结果都是随机的,并没有规律。
2.4 基于Nacos权重的负载均衡策略
- 在 consumer-service/pom.xml 文件中添加以下依赖
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 在 nacos 控制台修改两个 provider-service 实例的权重
- 在 consumer-service 中编写 CustomLoadBalancerConfiguration 类
public class CustomLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory, NacosDiscoveryProperties nacosDiscoveryProperties) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, nacosDiscoveryProperties);
}
}
- 修改 consumer-service 中的 ConsumerConfig 类
@Configuration
// xxx 服务使用 xxx 负载均衡策略
@LoadBalancerClient(value = "provider-service", configuration = CustomLoadBalancerConfiguration.class)
public class ConsumerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 运行 consumer-service 中的启动类
- 测试 http://localhost:8025/getPort
经过测试可以发现,权重大的比权重少的访问的次数更多。
三、OpenFeign
3.1 环境准备
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@GetMapping("/index")
public String index() {
return "hello";
}
}
- 运行启动类
- 创建子工程 consumer-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 consumer-service 中的 application.yml 文件中进行配置
server:
port: 8025 # 端口
spring:
application:
name: consumer-service # 服务名
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
3.2 进行远程调用
3.2.1 方式一
- 在 consumer-service/pom.xml 文件中添加以下依赖
<!--openfeign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 在 consumer-service 中的启动类上添加 @EnableFeignClients 注解
- 在 consumer-service 中编写 ProviderClient 接口(编写接口声明)
@FeignClient("provider-service") //服务名称
public interface ProviderClient {
@GetMapping("/index")
String index();
}
- 在 consumer-service 中编写 ConsumerController 类
@RestController
public class ConsumerController {
@Autowired
private ProviderClient providerClient;
@GetMapping("/index")
public String index() {
return this.providerClient.index();
}
}
- 运行 consumer-service 中的启动类
- 测试
3.2.2 方式二
- 创建子工程 feign-api,并在 pom.xml 文件中添加以下依赖
<!--openfeign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 在 feign-api 中编写 ProviderClient 接口(编写接口声明)
@FeignClient("provider-service") // 服务名称
public interface ProviderClient {
@GetMapping("/index")
String index();
}
- 在 consumer-service/pom.xml 文件中引入 feign-api 的 GAV 坐标
- 在 consumer-service 中的启动类上添加 @EnableFeignClients 注解
- 在 consumer-service 中编写 ConsumerController 类
@RestController
public class ConsumerController {
@Autowired
private ProviderClient providerClient;
@GetMapping("/index")
public String index() {
return this.providerClient.index();
}
}
- 测试
3.3 修改日志级别
- Feign 的有四种不同的日记级别:NONE(默认)、BASIC、HEADERS、FULL
- 建议使用 NONE 或 BASIC
3.3.1 配置文件方式
- 全局生效
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
logger-level: FULL # 日志级别
- 局部生效
feign:
client:
config:
provider-service: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
logger-level: FULL # 日志级别
- 如果配置了不生效,在 application.yml 文件中添加以下配置
logging:
level:
com.xxx.clients: debug # 将哪个目录的调试级别设置为debug级别
3.3.2 Java 代码方式
准备工作:首先需要先声明一个 Bean
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel() {
// return Logger.Level.NONE;
// return Logger.Level.BASIC;
// return Logger.Level.HEADERS;
return Logger.Level.FULL;
}
}
- 全局配置
把它加到 @EnableFeignClients 这个注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
- 局部配置
把它加到 @FeignClient 这个注解中
@FeignClient(value = "provider-service",configuration = FeignClientConfiguration.class)
- 如果配置了不生效,在 application.yml 文件中添加以下配置
logging:
level:
com.xxx.clients: debug # 将哪个目录的调试级别设置为debug级别
3.4 Feign 的性能优化
Feign 添加 HttpClient 的支持:
- consumer-service/pom.xml 添加依赖
<!--httpClient的依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 在 application.yml 文件中配置连接池
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
# 真实中需要进行压测来查看这两个值设置为多少比较合适
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
四、Sentinel
4.1 获取 Sentinel
获取压缩包或者源码: Releases · alibaba/Sentinel · GitHub
Sentinel 官网:home | Sentinel
启动 Sentinel
使用 java -jar 命令进行启动
在浏览器进行登录
- 默认用户名:sentinel
- 默认的密码:sentinel
如果要修改 Sentinel 的默认端口、账户、密码,可以通过下列配置:
配置项 | 默认值 | 说明 |
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
举例说明:
java -jar sentinel-dashboard-1.8.5.jar -Dserver.port=8090
4.2 环境准备
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
4.3 限流规则
4.3.1 限流规则-直接
- 在 provider-service/pom.xml 中添加以下依赖
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
sentinel:
transport:
dashboard: localhost:8080 #sentinel地址
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@GetMapping("/index")
public String index() {
return "index";
}
}
- 运行 provider-service 中的启动类
- 测试
- 在 sentinel 控制台设置限流规则
- 测试
我们1秒发送一次请求
我们1秒发送多次请求
4.3.2 限流规则-关联
- 在 provider-service/pom.xml 文件中添加以下依赖
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
sentinel:
transport:
dashboard: localhost:8080 #sentinel地址
- 在 provider-service 中的编写 ProviderController 类
@RestController
public class ProviderController {
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/list")
public String list() {
return "list";
}
}
- 运行 provider-service 中的启动类
- 在 provider-service 中编写 Test 测试类并启动
public class Test {
public static void main(String[] args) {
while (true) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8020/list", String.class);
}
}
}
- 在 sentinel 控制台设置限流规则
- 测试
4.3.2 限流规则-链路
- 在 provider-service/pom.xml 文件中添加以下依赖
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
sentinel:
transport:
dashboard: localhost:8080 #sentinel地址
web-context-unify: false #关闭 context 整合
- 在 provider-service 中编写 ProviderService 类
@Service
public class ProviderService {
// Sentinel默认只标记Controller中的方法为资源,如果要标记其他方法,需要使用@SentinelResource注解
@SentinelResource("test")
public void test() {
System.out.println("test");
}
}
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@Autowired
private ProviderService providerService;
@GetMapping("/test1")
public String test1() {
providerService.test();
return "test1";
}
@GetMapping("/test2")
public String test2() {
providerService.test();
return "test2";
}
}
- 运行 provider-service 中的启动类
- 测试
- 在 sentinel 控制台设置限流规则
- 测试
1秒只请求一次 /test1
1秒请求多次 /test1
1秒请求多次 /test2
4.4 流控效果
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出 FlowException 异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长。
4.4.1 流控效果-warm up
warm up 也叫预热模式,是应对服务启动的一种方案。请求阈值初始值是 threshold / coldFactor,持续指定时长后,逐渐提高到 threshold 值。而 coldFactor 的默认值是 3
例如,我设置 QPS 的 threshold 为 10,预热时间为 5 秒,那么初始阈值就是 10/3,然后在 5 秒后逐渐增长到 10
4.4.2 流控效果-排队等待
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常
4.5 热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过 QPS 阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过 QPS 阈值。
配置示例:
代表的含义是:对 hot 这个资源的 0 号参数(第一个参数)做统计,每 1 秒相同参数值的请求数不能超过 5
在热点参数限流的高级选项中,可以对部分参数设置例外配置:
结合上一个配置,这里的含义是对 0 号的 long 类型参数限流,每 1 秒相同参数的 QPS 不能超过 5,有两个例外:
- 如果参数值是 100,则每 1 秒允许的 QPS 为 10
- 如果参数值是 101,则每 1 秒允许的 QPS 为 15
注意:热点参数限流对默认的 SpringMVC 资源无效
案例:
- 在 provider-service/pom.xml 文件中添加以下依赖
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service #服务名称
cloud:
sentinel:
transport:
dashboard: localhost:8080 #sentinel地址
- 在 provider-service 中编写 ProviderController 类
@RestController
public class ProviderController {
@SentinelResource("test")
@GetMapping("/test/{id}")
public String test(@PathVariable("id") Integer id) {
return "ok";
}
}
- 运行 provider-service 中的启动类
- 在 sentinel 控制台设置热点规则
- 测试
参数值为 0
1 秒请求 1 或 2 次
1 秒请求超过 2 次
参数值为 1
1 秒请求 1 或 3 次
1 秒请求超过 3 次
五、Gateway
5.1 环境准备
- 创建父工程,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建子工程 provider-service,并在 pom.xml 中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在 provider-service 中的 application.yml 文件中进行配置
server:
port: 8020 #端口
spring:
application:
name: provider-service
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
- 在 provider-service 中编写 ProviderController 类
@RestController
@RequestMapping("/provider")
public class ProviderController {
@GetMapping("/index")
public String index() {
return "hello";
}
}
- 运行启动类
- 创建子工程 gateway,并在 pom.xml 文件中添加以下依赖
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
5.2 路由映射
- 在 gateway/pom.xml 文件中添加以下依赖
<!--gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 在 gateway 中的 application.yml 文件中进行配置
第一种配置
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: provider-service #路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8020 #路由的目标地址 http就是固定地址
uri: lb://provider-service #路由的目标地址 lb是负载均衡,后面跟服务名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/provider/** #这个是按照路径匹配,只要以 /provider/ 开头就符合要求
第二种配置(动态路由)
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
discovery:
locator:
enabled: true #是否启动自动识别nacos服务
lower-case-service-id: true #开启服务名称小写
- 运行 gateway 中的启动类
- 测试
第一种配置
第二种配置
5.2 全局过滤器
- 案例
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
1. 参数中是否有 authorization
2. authorization 参数值是否为 admin
如果同时满足则放行,否则拦截
- 在 gateway/pom.xml 文件中添加以下依赖
<!--gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 在 gateway 中的 application.yml 文件中进行配置
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: provider-service #路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8020 #路由的目标地址 http就是固定地址
uri: lb://provider-service #路由的目标地址 lb是负载均衡,后面跟服务名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/provider/** #这个是按照路径匹配,只要以 /provider/ 开头就符合要求
- 在 gateway 中编写 AuthorizeFilter 类
@Order(1) // 过滤器执行的顺序,数字越小,优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
HttpHeaders params = exchange.getRequest().getHeaders();
// 2.获取参数中的 authorization 参数
String auth = params.getFirst("authorization");
System.out.println(auth);
// 3.判断参数值是否等于 admin
if ("admin".equals(auth)) {
// 4.是,放行
return chain.filter(exchange);
}
// 5.否,拦截
// 5.1 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2 拦截请求
return exchange.getResponse().setComplete();
}
}
- 运行 gateway 中的启动类
- 测试
5.3 跨域问题处理
跨域:域名不一致就是跨域,主要包括:
- 域名不同:www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
- 域名相同,端口不同:localhost:8080 和 localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域 ajax 请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是 CORS 方案,并且只需要简单配置即可实现:
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
六、Seata
6.1 分步式事务问题
6.1.1 本地事务
本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则:
6.1.2 分布式事务
分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:
- 跨数据源的分布式事务
- 跨服务的分布式事务
- 综合情况
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:
- 创建新订单
- 扣减商品库存
- 从用户账户扣除金额
完成上面的操作需要访问三个不同的微服务和三个不同的数据库。
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证 ACID 原则。
但是我们把三件事情看做一个 "业务",要满足保证 "业务" 的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。
此时 ACID 难以满足,这就是分布式事务要解决的问题
6.1.3 演示分布式事务问题
我们通过一个案例来演示分布式事务的问题:
1. 创建数据库
- 创建一个名为 seata_demo 的数据库
create database seata_demo;
- 在 seata_demo 里创建表并插入数据
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`count` int(11) NULL DEFAULT 0,
`money` int(11) NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 1
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = COMPACT;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`money` int(11) UNSIGNED NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 2
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = COMPACT;
INSERT INTO `account_tbl`
VALUES (1, 'user202103032042012', 1000);
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`count` int(11) UNSIGNED NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `commodity_code` (`commodity_code`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 2
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = COMPACT;
INSERT INTO `storage_tbl`
VALUES (1, '100202003032041', 10);
2. 项目搭建
- account-service:账户服务,负责管理用户的资金账户。提供扣减余额的接口
- storage-service:库存服务,负责管理商品库存。提供扣减库存的接口
- order-service:订单服务,负责管理订单。创建订单时,需要调用 account-service 和 storage-service
seata-demo
- 创建父工程 seata-demo,并在 pom.xml 文件中添加以下依赖
<!--打包方式-->
<packaging>pom</packaging>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
account-service
- 创建子工程 account-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
- 在 application.yml 文件中进行配置
server:
port: 8080 #端口
spring:
application:
name: account-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///seata_demo?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: root
- 创建 Account 类
@Data
@TableName("accountTbl")
public class Account {
@TableId("id")
private Integer id;
@TableField("user_id")
private String userId;
@TableField("money")
private Integer money;
}
- 创建 AccountMapper 接口
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
@Update("update account_tbl set money = money - ${money} where user_id = #{userId}")
int deduct(@Param("userId") String userId, @Param("money") int money);
@Update("update account_tbl set money = money + ${money} where user_id = #{userId}")
int refund(@Param("userId") String userId, @Param("money") int money);
}
- 创建 AccountService 接口、AccountServiceImpl 实现类
public interface AccountService {
// 从用户账户中扣款
void deduct(String userId, int money);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
try {
accountMapper.deduct(userId, money);
} catch (Exception e) {
throw new RuntimeException("扣款失败,可能是余额不足", e);
}
}
}
- 创建 AccountController 类
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@PutMapping("/{userId}/{money}")
public String deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money) {
accountService.deduct(userId, money);
return "account-service扣减余额成功";
}
}
storage-service
- 创建子工程 storage-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
- 在 application.yml 文件中进行配置
server:
port: 8081 #端口
spring:
application:
name: storage-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///seata_demo?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: root
- 创建 Storage 类
@Data
@TableName("storage_tbl")
public class Storage {
@TableId
private Long id;
@TableField("commodity_code")
private String commodityCode;
@TableField("count")
private Integer count;
}
- 创建 StorageMapper 接口
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
@Update("update storage_tbl set `count` = `count` - #{count} where commodity_code = #{code}")
void deduct(@Param("code") String commodityCode, @Param("count") int count);
}
- 创建 StorageService 接口、StorageServiceImpl 实现类
public interface StorageService {
// 扣除存储数量
void deduct(String commodityCode, int count);
}
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
@Transactional
public void deduct(String commodityCode, int count) {
try {
storageMapper.deduct(commodityCode, count);
} catch (Exception e) {
throw new RuntimeException("扣减库存失败,可能是库存不足");
}
}
}
- 创建 StorageController 类
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
@PutMapping("/{code}/{count}")
public String deduct(@PathVariable("code") String code, @PathVariable("count") Integer count) {
storageService.deduct(code, count);
return "storage-service扣减库存成功";
}
}
order-service
- 创建子工程 order-service,并在 pom.xml 文件中添加以下依赖
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--openfeign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--Spring Cloud Loadbalancer 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 在 application.yml 文件中进行配置
server:
port: 8082 #端口
spring:
application:
name: order-service #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///seata_demo?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: root
- 在启动类中添加注解 @EnableFeignClients
- 创建 Order 类
@Data
@TableName("order_tbl")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("user_id")
private String userId;
@TableField("commodity_code")
private String commodityCode;
@TableField("count")
private Integer count;
@TableField("money")
private Integer money;
}
- 创建 OrderMapper 接口
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
- 创建 AccountClient 接口、StorageClient 接口
@FeignClient(value = "account-service", path = "/account")
public interface AccountClient {
@PutMapping("/{userId}/{money}")
String deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money);
}
@FeignClient(value = "storage-service", path = "/storage")
public interface StorageClient {
@PutMapping("/{code}/{count}")
String deduct(@PathVariable("code") String code, @PathVariable("count") Integer count);
}
- 创建 OrderService 接口、OrderServiceImpl 实现类
public interface OrderService {
// 创建订单
Long create(Order order);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountClient accountClient;
@Autowired
private StorageClient storageClient;
@Override
@Transactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
}
- 创建 OrderController 类
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public Long createOrder(@RequestBody Order order) {
return orderService.create(order);
}
}
3. 测试
- 启动三个服务进行测试
上面测试的是库存足够的情况下,我们再测试一次
测试完我们发现报错了,而且库存不足时,库存已经扣减了,并没有回滚,出现了分布式事务问题。
6.2 理论基础
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。
6.2.1 CAP 定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
他们的第一个字母分别是 C、A、P。
Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
1. 一致性
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
比如现在包含两个节点,其中的初始数据是一致的:
当我们修改其中一个节点的数据时,两者的数据产生了差异:
要想保住一致性,就必须实现node01 到 node02的数据 同步:
2. 可用性
Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
如图,有三个节点的集群,访问任何一个都可以及时得到响应:
当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:
3. 分区容错
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
4. 矛盾
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此 Partition Tolerance 不可避免。
当节点接收到新的数据变更时,就会出现问题了:
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那 node01、node02 与 node03 之间就会出现数据不一致。
也就是说,在P一定会出现的情况下,A和C之间只能实现一个。
6.2.2 BAE 理论
BASE理论是对CAP的一种解决思路,包含三个思想:
-
Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
-
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
-
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
但不管是哪一种模式,都需要子系统事务之间互相通讯,协调事务状态,也就是需要一个 事务协调者(TC)
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
6.3 初始 Seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
6.3.1 Seata 的架构
Seata事务管理中有三个重要的角色:
-
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
-
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
整体的架构如图:
Seata基于上述架构提供了四种不同的分布式事务解决方案:
-
XA 模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
-
TCC 模式:最终一致的分阶段事务模式,有业务侵入
-
AT 模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
-
SAGA 模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
6.3.2 部署 TC 服务
首先需要先下载 seata
Releases · seata/seata (github.com)
参考:
分布式事务Seata-1.5.2使用全路线指北 - 掘金 (juejin.cn)
seata 1.5.2 window安装与配置_ldj2020的博客-CSDN博客_seata1.5.2
1. 建表
- 新建一个数据库 seata
- 解压 seata 压缩包之后运行 seata\script\server\db\mysql.sql 文件
- 建表完成
2. 在 nacos 中配置
- 创建命名空间
- 进入配置列表,进入 seata 命名空间,新建配置
#公共部分
transport.serialization=seata
transport.compressor=none
transport.heartbeat=true
registry.type=nacos
config.type=nacos
#server端
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
#存储模式我这里选用的db,不用file和redis,这里很多配置都没有默认值但是必须指定,按情况配置即可
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
#这里url跟上rewriteBatchedStatements=true,原因看官网-参数配置-附录7,简单来说就是增加批量插入效率
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_new?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
#默认1和20,稍微调大点
store.db.minConn=5
store.db.maxConn=30
store.db.maxWait=5000
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table
store.db.queryLimit=100
#监控,只支持prometheus,我这里没有现成的就没用
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
#client端
seata.enabled=true
seata.enableAutoDataSourceProxy=true
seata.useJdkProxy=false
transport.enableClientBatchSendRequest=true
client.log.exceptionRate=100
service.disableGlobalTransaction=false
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.rm.reportSuccessEnable=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
#一阶段全局提交和回滚结果上报TC重试次数,默认1,这里改成3
client.tm.commitRetryCount=3
client.tm.rollbackRetryCount=3
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.logTable=undo_log
client.undo.onlyCareUpdateColumns=true
client.rm.sqlParserType=druid
#自定义事务组scm_tx_group和my_test_tx_group
service.vgroupMapping.scm_tx_group=default
配置详情:
seata 1.5.2 window安装与配置_ldj2020的博客-CSDN博客_seata1.5.2
3. 修改 application.yml 文件
- 配置 seata\conf\application.yml 文件
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${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:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 3cab7a65-d40a-44f0-afba-0cc96485e923
group: SEATA_GROUP
username: nacos
password: nacos
data-id: seataService.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-service
server-addr: 127.0.0.1:8848
namespace: 3cab7a65-d40a-44f0-afba-0cc96485e923
cluster: default
group: SEATA_GROUP
username: nacos
password: nacos
# store:
# support: file 、 db 、 redis
# mode: nacos
# 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,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
4. 运行 Seata 服务端
- 双击 seata\bin\seata-server.bat 文件
我装了两个 jdk,运行闪退,解决:指定 jdk
- 启动成功之后注册至 nacos
6.3.3 微服务集成 Seata
三个微服务同样都是以下操作
1. 在 pom.xml 文件中引入以下依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
2. 配置 TC 地址
seata:
registry: #TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos #注册中心类型
nacos:
application: seata-service #服务名称
server-addr: 127.0.0.1:8848 #nacos地址
namespace: 3cab7a65-d40a-44f0-afba-0cc96485e923 #namespace
group: SEATA_GROUP #分组,默认是 DEFAULT_GROUP
username: nacos #用户名
password: nacos #密码
tx-service-group: seata-demo #事务组名称
service:
vgroup-mapping: #事务组与cluster的映射关系
seata-demo: default
6.4 动手实践
6.4.1 XA 模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
两阶段提交
XA 是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
正常情况:
异常情况:
一阶段:
- 事务协调者通知每个事务参与者执行本地事务
- 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
二阶段:
- 事务协调者基于一阶段的报告来判断下一步操作
- 如果一阶段都成功,则通知所有事务参与者,提交事务
- 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
Seata 的 XA 模型
Seata 对原始的 XA 模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM 一阶段的工作:
- 注册分支事务到 TC
- 执行分支事务 sql 但不提交
- 报告执行状态到 TC
TC 二阶段的工作:
- TC 检测各分支事务执行状态
- 如果都成功,通知所有 RM 提交事务
- 如果有失败,通知所有 RM 回滚事务
RM 二阶段的工作:
- 接收 TC 指令,提交或回滚事务
优缺点
XA 模式的优点是什么?
- 事务的强一致性,满足 ACID 原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA 模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现 XA 模式
三个微服务同样都是以下操作
- 修改 application.yml 文件,开启 XA 模式
seata:
data-source-proxy-mode: XA
- 给发起全局事务的入口方法添加注解 @GlobalTransactional
- 重启服务并测试
测试发现,无论怎样,三个微服务都能成功回滚。
6.4.2 AT 模式
AT 模式同样是分阶段提交的事务模型,不过弥补了 XA 模型中资源锁定周期过长的缺陷。
Seata 的 AT模型
基本流程图
阶段一 RM 的工作:
- 注册分支事务
- 记录 undo-log(数据快照)
- 执行业务 sql 并提交
- 报告事务状态
阶段二提交时 RM 的工作:
- 删除 undo-log 即可
阶段二回滚时 RM 的工作:
- 根据 undo-log 恢复数据到更新前
流程梳理
我们用一个真实的业务来梳理下 AT 模式的原理。
比如,现在有一个数据表,记录用户余额:
其中一个分支业务要执行的 SQL 为:
update tb_account set money = money - 10 where id = 1
AT 模式下,当前分支事务执行流程如下:
一阶段:
- TM 发起并注册全局事务到 TC
- TM 调用分支事务
- 分支事务准备执行业务 SQL
- RM 拦截业务 SQL,根据 where 条件查询原始数据,形成快照。
{
"id": 1, "money": 100
}
- RM 执行业务 SQL,提交本地事务,释放数据库锁。此时 money = 90
- RM 报告本地事务状态给 TC
二阶段:
- TM 通知 TC 事务结束
- TC 检查分支事务状态
- 如果都成功,则立即删除快照
- 如果有分支事务失败,需要回滚。读取快照数据({ "id": 1, "money": 100 }),将快照恢复到数据库。此时数据库再次恢复为 100
流程图
AT 与 XA 的区别
简述 AT 模式与 XA 模式最大的区别是什么?
-
XA 模式一阶段不提交事务,锁定资源;AT 模式一阶段直接提交,不锁定资源。
-
XA 模式依赖数据库机制实现回滚;AT 模式利用数据快照实现数据回滚。
-
XA 模式强一致;AT 模式最终一致
脏写问题
在多线程并发访问 AT 模式的分布式事务时,有可能出现脏写问题,如图:
解决思路就是引入了全局锁的概念。在释放 DB 锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
优缺点
AT 模式的优点
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT 模式的缺点
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比 XA 模式要好很多
实现 AT 模式
AT 模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
只不过,AT 模式需要一个表来记录全局锁、另一张表来记录数据快照 undo_log
三个微服务同样都是以下操作
- 导入数据库表(导入到微服务关联的数据库)
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
- 修改 application.yml 文件,开启 AT 模式
seata:
data-source-proxy-mode: AT
- 给发起全局事务的入口方法添加注解 @GlobalTransactional
- 重启服务并测试
测试发现,无论怎样,三个微服务都能成功回滚。
6.4.3 TCC 模式
TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功
- Cancel:预留资源释放,可以理解为 try 的反向操作
流程分析
举例,一个扣减用户余额的业务。假设账户 A 原来余额是 100,需要余额扣减 30 元。
- 阶段一(Try):检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30
初始余额:
余额充足,可以冻结:
此时,总金额 = 冻结金额 + 可用金额,数量依然是 100 不变。事务直接提交无需等待其它事务。
- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减 30
确定可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了
此时,总金额 = 冻结金额 + 可用金额 = 0+70=70 元
- 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:
Seata 的 TCC 模型
Seata 中的 TCC 模型依然延续之前的事务架构,如图:
优缺点
TCC 模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC 的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比 AT 模式,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC 的缺点是什么?
- 有代码侵入,需要人为编写 try、Confirm 和 Cancel 接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理
事务悬挂和空回滚
1)空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的 cancel 操作。在未执行 try 操作时先执行了 cancel 操作,这时 cancel 不能做回滚,就是空回滚。
如图:
执行 cancel 操作时,应当判断 try 是否已经执行,如果尚未执行,则应该空回滚。
2)业务悬挂
对于已经空回滚的业务,之前被阻塞的 try 操作恢复,继续执行 try,就永远不可能 confirm 或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行 try 操作时,应当判断 cancel 是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
实现 TCC 模式
解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在 try、还是 cancel?
1)思路分析
这里我们定义一张表(在 seata_demo 中):
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money`int(11)unsigned DEFAULT'0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
在 account-service 的 domain 包中定义实体类
@Data
@TableName("account_freeze_tbl")
public class AccountFreeze {
@TableId(type = IdType.INPUT)
private String xid; // 事务id
@TableField("user_id")
private String userId; // 用户id
@TableField("freeze_money")
private Integer freezemoney; // 冻结金额
@TableField("state")
private Integer state; // 状态
public static abstract class State {
public final static int TRY = 0;
public final static int CONFIRM = 1;
public final static int CANCEL = 2;
}
}
在 account-service 的 mapper 包中定义接口
@Mapper
public interface AccountFreezeMapper extends BaseMapper<AccountFreeze> {
}
其中:
- xid:是全局事务 id
- freeze_money:用来记录用户冻结金额
- state:用来记录事务状态
那此时,我们的业务该怎么做呢?
- Try 业务:
- 记录冻结金额和事务状态到 account_freeze 表
- 扣减 account 表可用金额
- Confirm 业务
- 根据 xid 删除 account_freeze 表的冻结记录
- Cancel 业务
- 修改 account_freeze 表,冻结金额为 0,state 为 2
- 修改 account 表,恢复可用金额
- 如何判断是否空回滚?
- cancel 业务中,根据 xid 查询 account_freeze,如果为 null 则说明 try 还没做,需要空回滚
-
如何避免业务悬挂?
-
try 业务中,根据 xid 查询 account_freeze ,如果已经存在则证明 Cancel 已经执行,拒绝执行 try 业务
-
接下来,我们改造 account-service,利用 TCC 实现余额扣减功能。
2)声明 TCC 接口
TCC 的 Try,Confirm、Cancel 方法都需要在接口中基于注解来声明,语法如下:
我们在 accout-service 项目中的 service 包中新建一个接口,声明 TCC 三个接口:
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
3)编写实现类
在 account-service 服务中新建一个类,实现 TCC 业务
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.获取事务id
String xid = RootContext.getXID();
// 1.判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,我要拒绝业务
AccountFreeze oldFreeze = freezeMapper.selectById(xid);
if (oldFreeze != null) {
// CANCEL 执行过,我要拒绝业务
return;
}
// 1.扣减可用余额
accountMapper.deduct(userId, money);
// 2.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezemoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.获取事务id
String xid = ctx.getXid();
// 2.根据id删除冻结记录
int count = freezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查询冻结记录
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
AccountFreeze freeze = freezeMapper.selectById(xid);
// 1.空回滚的判断,判断freeze是否为null,为null证明try没执行,需要空回滚
if (freeze == null) {
// 证明try没执行,需要回滚
freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezemoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
}
// 2.幂等判断
if (freeze.getState() == AccountFreeze.State.CANCEL) {
// 已经处理过一次CANCEL了,无需重复处理
return true;
}
// 1.恢复可用余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezemoney());
// 2.将冻结金额清零,状态改为CANCEL
freeze.setFreezemoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
4)修改 AccountController
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountTCCService accountService;
@PutMapping("/{userId}/{money}")
public String deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money) {
accountService.deduct(userId, money);
return "account-service扣减余额成功";
}
}
5)重启服务测试
测试之前我们将余额和库存进行更改
接着我们发起请求
接着我们再次查看余额和库存
6.4.4 SAGA 模式
原理
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
-
一阶段:直接提交本地事务
-
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
优缺点
优点:
-
事务参与者可以基于事件驱动实现异步调用,吞吐高
-
一阶段直接提交事务,无锁,性能好
-
不用编写 TCC 中的三个阶段,实现简单
缺点:
-
软状态持续时间不确定,时效性差
-
没有锁,没有事务隔离,会有脏写
四种模式对比
我们从以下几个方面来对比四种实现:
-
一致性:能否保证事务的一致性?强一致还是最终一致?
-
隔离性:事务之间的隔离性如何?
-
代码侵入:是否需要对业务代码改造?
-
性能:有无性能损耗?
-
场景:常见的业务场景
如图: