紧接上文 Spring Cloud 原生态组件
juejin.cn/post/750317…
由于作者目前只能在实战中成功实现各项功能,
如果有细节错误或无关代码的问题,欢迎提出
SpringCloud alibaba Nacos 2.2.3 8848
一句话描述:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。(替代了Consul)
Nacos就是注册中心 + 配置中心的组合
Nacos = Eureka + Config + Bus
Nacos = Spring Cloud Consul
各种注册中心的比较:
(据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验,Nacos默认是AP模式,但也可以调整切换为CP,我们一般用默认AP即可。)
如何启动Nacos?
github.com/alibaba/nac… 下载2.2.3版本
进入到 E:\nacos-server2.2.3\bin 目录,打开cmd窗口,执行以下命令:(非集群模式)
#windows启动
startup.cmd -m standalone
#访问官网 如果有账号密码输入:都是nacos
http://192.168.37.1:8848/nacos/index.html
Nacos代码实战
创建模块cloudalibaba-provider-payment8003,添加依赖,添加yml配置文件,修改主启动类,新建控制层
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.peng</groupId>
<artifactId>cloud-api-common</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>
server:
port: 8003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
@SpringBootApplication
@EnableDiscoveryClient //启动服务注册必须添加此注解
public class Main8003 {
public static void main(String[] args) {
SpringApplication.run(Main8003.class, args);
}
}
@RestController
public class PayAlibabaController{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/pay/nacos/{id}")
public String getPayInfo(@PathVariable("id") Integer id){
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
创建消费者模块cloudalibaba-consumer-order83,添加依赖,添加yml配置文件,修改主启动类,新建控制层,配置restemplate进行微服务之间负载均衡的调用
<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>
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
service-url:
nacos-user-service: http://nacos-payment-provider
@Configuration
public class RestTemplateConfig{
@Bean
@LoadBalanced //赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@RestController
public class OrderNacosController{
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/pay/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id){
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
}
8003 和 83 服务启动成功后注册进nacos
测试负载均衡
- 使用快捷方式创建多一个提供者8004
- 添加虚拟机选项(配置端口)
- 修改项目名以及端口配置后进行应用
- 启动 8003、8004 和 83 服务
Nacos Config服务配置中心
在之前的案例Consil8500服务配置动态变更功能可以被Nacos取代,通过Nacos和spring-cloud-starter-alibaba-nacos-config实现中心化全局配置的动态变更
基本的配置步骤:建Moudle,引入依赖,改主启动类,添加控制器编写业务逻辑。
- 创建 cloudalibaba-config-nacos-client3377 模块
- 添加依赖
<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>
- 修改启动类
@SpringBootApplication
@EnableDiscoveryClient
@RefreshScope
public class Main3377 {
public static void main(String[] args) {
SpringApplication.run(Main3377.class, args);
}
}
- 添加控制器(添加 @RefreshScope 启动动态配置,只能放在Controller层上)
@RestController
@RefreshScope
public class NacosConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
- 配置yaml文件(注意和consul一样,需要有
application.uml,bootstrap.yml两个文件)
server:
port: 3377
# nacos配置
spring:
application:
name: nacos-config-client
profiles:
active:
#active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml #文件后缀 项目名+文件后缀 -》指定文件
# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client.yaml
测试配置信息的获取
- 在官网上用合适的Date ID 编写对用格式的配置内容(yaml请用合格的两个空格进行编写)
- 启动端口接口进行测试,并观察是否有动态配置
Nacos数据模型之Namespace-Group-DataId
问题1:实际开发中,通常一个系统会准备:dev开发环境test测试环境,prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境… 那怎么对这些微服务配置进行分组和命名空间管理呢?
是什么:类似Java里面的package名和类名,最外层的Namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象
默认值:默认情况:Namespace=public,Group=DEFAULT_GROUP
Nacos默认的命名空间是public,Namespace主要用来实现隔离。比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
Service就是微服务: 一个Service可以包含一个或者多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。见下一节:服务领域模型-补充说明
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml #文件后缀 项目名+文件后缀 -》指定文件
group: GROUP
namespace: NAMESPACE
Spring Cloud Alibaba Sentinel 8400(实现熔断与限流)
1.6.x 版本默认8080端口,1.8.x版本默认8718端口,此次实战作者用的是docker,转到了8400端口
Sentinel是什么(基本概念):
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel哨兵机制的原理(流量降级与容错标准):
Sentinel常见面试题(高并发解决的问题)1.8.6
服务雪崩
(简单来说就是当一个微服务突然面对高并发的请求处理不过来的时候会影响该微服务调用的其他模块和调用他的模块,形成雪崩)
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务降级
服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。
例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。
服务熔断
在分布式与微服务系统中,**如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。**这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务熔断一般情况下会有三种状态:闭合、开启和半熔断;
闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
服务限流
服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
服务隔离
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。
互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。
服务超时
整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。
形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。
下载安装
windows
官网下载 Jar 包
# 默认账号密码都是 sentinel
java -jar sentinel-dashboard-1.8.6.jar
java \
-Dserver.port=8480 \
-Dcsp.sentinel.dashboard.server=localhost:8480 \
-Dproject.name=sentinel-dashboard \
-Dsentinel.dashboard.auth.username=admin \
-Dsentinel.dashboard.auth.password=admin \
-jar sentinel-dashboard-1.8.6.jar
# 指定控制台的端口为8480
-Dserver.port=8480
# 指定要被哪个控制台监控(这里指定的是自己监控自己)
-Dcsp.sentinel.dashboard.server=localhost:8480
# 指定实例名称(名称会在控制台左侧以菜单显示)
-Dproject.name=sentinel-dashboard
# 设置登录的帐号为:admin
-Dsentinel.dashboard.auth.username=admin
# 设置登录的密码为:admin
-Dsentinel.dashboard.auth.password=admin
linux
Docker部署
docker pull bladex/sentinel-dashboard:v1.8.6
镜像拉取失败使用 jar包 进行构建镜像
touch Dockerfile
# 使用 OpenJDK 作为基础镜像
FROM openjdk:17-jdk-slim
# 设置工作目录
WORKDIR /app
# 拷贝 JAR 包到容器中
COPY sentinel-dashboard-1.8.6.jar /app/sentinel-dashboard.jar
# 显式暴露 8400 端口
EXPOSE 8400
# 启动 Sentinel Dashboard
ENTRYPOINT ["java", "-jar", "sentinel-dashboard.jar"]
docker build -t sentinel-dashboard:1.8.6 .
# 因为jar包里的port是8080,所以需要 --server.port=8400
docker run -d \
--name sentinel \
-p 8400:8400 \
-p 8719:8719 \
-e auth.enabled="true" \
-e sentinel.dashboard.auth.username=admin \
-e sentinel.dashboard.auth.password=admin \
-e server.servlet.session.timeout=8400 \
sentinel-dashboard:1.8.6 --server.port=8400
# 开启登录认证,在application.yml中你需要配置对应的用户名和密码
auth.enabled="true"
# 指定Sentinel控制台用户名,默认Sentinel
sentinel.dashboard.auth.username=admin
# 指定Sentinel控制台密码,默认Sentinel
sentinel.dashboard.auth.password=admin
# 用于指定 Spring Boot服务端session的过期时间,如7200表示7200 秒;60m表示60分钟,默认为30分钟;
server.servlet.session.timeout=8400
#打开 8400 和 8719 端口
firewall-cmd --permanent --add-port=8400/tcp #配置Sentinel dashboard控制台服务地址
firewall-cmd --permanent --add-port=8719/tcp #其他应用会通过该端口与 Sentinel 进行交互。
#重新加载/刷新
firewall-cmd --reload
Sentinel入门案例
创建一个新的模块,模块名为:cloudalibaba-sentinel-service8401,这个模块的作用是将哨兵纳入管控的8401微服务提供者
- 添加依赖:
<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>
- 创建yml配置文件:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: 192.168.37.130:8400 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false
log:
dir: G:/SpringCloud软件/sentinel-1.8.6/log/sentinel-log
- 修改主启动类
- 创建控制器编写业务逻辑:
@RestController
public class FlowLimitController{
@GetMapping("/testA")
public String testA(){
return "------testA";
}
@GetMapping("/testB")
public String testB(){
return "------testB";
}
}
启动8401服务,启动Nacos和Sentinel观察该服务是否已经入住Nacos,并访问 localhost:8401/testA与 localhost:8401/testB 观察Sentinel流量监控:
Sentinel之流控模式
在Sentinel控制台中能制定流控规则,如下图:
对下表字段的一些解释:
- 资源名:资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。
- 针对来源:具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。
- 阈值类型:QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。
- 单机阈值:与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。
- 是否集群:选中则表示集群环境,不选中则表示非集群环境。
Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。
流控模式分为三种:
- 直接 流控模式之直接
直接模式即为默认的控流模式,当接口达到限流条件时,直接开启限流功能。
示例:【对 localhost:8401/testA该路径的接口进行流控】
测试结果:当我们快速频繁刷新该地址时,就会出现默认的错误提示
(Blocked by Sentinel (flow limiting))来进行流量的限制。
同时也支持自定义的兜底方法 fallback。
- 关联 流控模式之关联(实用)
当关联的资源达到阈值时,就限流自己,假设A接口与B接口关联,当B接口访问达到阈值时,A就会被限流(B惹事了,A挂了)
演示:
测试说明:使用jmeter对testB接口进行压力测试,会发现A进行了限流的操作。
- 链路 流控模式之链路
来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施,比如C请求来访问就限流,D请求来访问就是ok
演示:
修改微服务cloudalibaba-sentinel-service8401,对yml文件进行修改,创建一个FlowLimitService,并且在控制层新创建两个接口
sentinel:
web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
@Service
public class FlowLimitService{
@SentinelResource(value = "common")
public void common(){
System.out.println("------FlowLimitService come in");
}
}
/**流控-链路演示demo
* C和D两个请求都访问flowLimitService.common()方法,阈值到达后对C限流,对D不管
*/
@Resource private FlowLimitService flowLimitService;
@GetMapping("/testC")
public String testC(){
flowLimitService.common();
return "------testC";
}
@GetMapping("/testD")
public String testD(){
flowLimitService.common();
return "------testD";
}
在Sentinel控制台进行链路配置:【说明:C和D两个请求都访问 flowLimitService.common()方法,对C限流,对D不管】
测试说明:当快速频繁的访问testC接口的时候后端就会报错,但是testD则不会受到影响。
流控效果分为三种:
- 快速失败
Sentinel默认的流控效果为快速失败,这种方式是直接失败抛出异常
- 预热WarmUp
什么是限流 冷启动?
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
公式:阈值除以冷却因子coldFactory(默认值为3),经过预热时长后才会达到阈值
案例演示:【在Sentinel控制台进行如下配置】
案例说明: 单机阈值为10,预热时长设置5秒。
系统初始化的阈值为10 / 3 约等于3,即单机阈值刚开始为3(我们人工设定单机阈值是10,sentinel计算后QPS判定为3开始);然后过了5秒后阀值才慢慢升高恢复到设置的单机阈值10,也就是说5秒钟内QPS为3,过了保护期5秒后QPS为10
测试结果: 刚开始进行频繁快速访问会进行限流,5s后就不会再限流(原因就是设置了5秒的预热时长)
该配置的应用场景:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
- 排队等待
这种场景就非常常见,例如一个时间点同时并发大量的请求,使用这种方法能够让这种并发无序的请求进行排队以免造成服务崩溃。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象以下这样的场景,在某一秒有大量请求到来,而接下来的几秒处于空闲状态,我们希望系统能够在接下来的空闲期间处理这些请求,而不是在第一秒直接拒绝多余的请求。
演示实战:
在FlowLimitController中添加新方法,该方法将时间输出到控制台中
@GetMapping("/testE")
public String testE(){
System.out.println(System.currentTimeMillis()+" testE,排队等待");
return "------testE";
}
在Sentinel控制台中进行如下配置:
【解释下图中的配置:按照单机阈值,一秒钟通过一个请求,10秒后的请求作为超时处理,放弃】
测试:
Sentinel之熔断降级
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,
让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
熔断降级的策略分为三种:
- 慢调用比例:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比列:当==单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。==经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断策略之慢调用比例
首先明确在什么样的情况下服务会进入熔断:在统计时长内,实际请求数目>设定的最小请求数 且 实际**慢调用比例(下面有解释)**>比例阈值 ,进入熔断状态。
熔断流程:
- 熔断状态(保险丝跳闸断电,不可访问):在接下来的熔断时长内请求会自动被熔断
- 探测恢复状态(探路先锋):熔断时长结束后进入探测恢复状态
- 结束熔断(保险丝闭合恢复,可以访问):在探测恢复状态,如果接下来的一个请求响应时间小于设置的慢调用 RT,则结束熔断,否则继续熔断。
慢调用比例实现
先对上面的选项进行说明:
- 调用:一个请求发送到服务器,服务器给与响应,一个响应就是一个调用。
- 最大RT:即最大的响应时间,指系统对请求作出响应的业务处理时间。
- 慢调用:处理业务逻辑的实际时间>设置的最大RT时间,这个调用叫做慢调用。
- 慢调用比例:在所以调用中,慢调用占有实际的比例=慢调用次数➗总调用次数
- 比例阈值:自己设定的 , 比例阈值=慢调用次数➗调用次数
- 统计时长:时间的判断依据
- 最小请求数:设置的调用最小请求数,上图比如1秒钟打进来10个线程(大于我们配置的5个了)调用被触发
在Sentinel控制台中设置上图配置的熔断规则并使用jmeter进行压力测试,循环发送每1s种发10次请求。测试得出,访问http://localhost:8401/testF,会立即报错,只要一值由1s10个的请求发送就会一致无法访问,知道关闭请求发送后,并过5s后(上图设置的熔断时长)才会慢慢恢复访问。
异常比例实现
与慢调用比例相似,当访问超出了比例阈值时就会触发熔断
使用jmeter继续压力测试,1s钟发送20个请求,最终测试的结论为:
按照上述配置,单独访问一次,必然来一次报错一次(int age = 10/0)达到100%,调一次错一次报错error;
测试结论:
开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了。断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务熔断+服务降级,出提示:Blocked by Sentinel (flow limiting)。
异常数实现
与异常比例相似,顾名思义,异常比例是根据比例阈值来判断是否进行熔断,而异常数则是通过异常数来判断是否进行熔断
测试结论:
http://localhost:8401/testH,第一次访问绝对报错,因为除数不能为零,我们看到error窗口;
@SentinelResource注解
概述:@SentinelResource 是一个流量防卫防护组件注解,用于指定防护资源,对配置的资源进行流量控制、熔断降级登功能。
@SentinelResource注解源码分析:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
//资源名称
String value() default "";
//entry类型,标记流量的方向,取值IN/OUT,默认是OUT
EntryType entryType() default EntryType.OUT;
//资源分类
int resourceType() default 0;
//处理BlockException的函数名称,函数要求:
//1. 必须是 public
//2.返回类型 参数与原方法一致
//3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
String blockHandler() default "";
//存放blockHandler的类,对应的处理函数必须static修饰。
Class<?>[] blockHandlerClass() default {};
//用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所
//有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
//1. 返回类型与原方法一致
//2. 参数类型需要和原方法相匹配
//3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
String fallback() default "";
//存放fallback的类。对应的处理函数必须static修饰。
String defaultFallback() default "";
//用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进
//行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
//1. 返回类型与原方法一致
//2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
//3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
Class<?>[] fallbackClass() default {};
//需要trace的异常
Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
//指定排除忽略掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
Class<? extends Throwable>[] exceptionsToIgnore() default {};
}
作用一:自定义限流时返回信息(blockHandler)
不想用默认的限流提示(Blocked by Sentinel (flow limiting)),想返回自定义限流的提示
步骤:只需要在控制层中加上@SentinelResource(value = "控制层方法名",blockHandler = "自定义限流方法名")即可。
演示:
在业务类RateLimitController中进行如下配置:
@GetMapping("/rateLimit/byResource")
@SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException")
public String byResource(){
return "按资源名称SentinelResource限流测试OK";
}
public String handleException(BlockException exception){
return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o";
}
作用二:异常时返回信息(fallback)
SentinelResource配置,点击超过限流配置返回自定义限流提示+程序异常返回fallback服务降级
@SentinelResource注解中存在一个fallback参数,该参数是用来异常的自定义服务降级
编写控制层业务:
@GetMapping("/rateLimit/doAction/{p1}")
@SentinelResource(value = "doActionSentinelResource",
blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
public String doAction(@PathVariable("p1") Integer p1) {
if (p1 == 0){
throw new RuntimeException("p1等于零直接异常");
}
return "doAction";
}
public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){
log.error("sentinel配置自定义限流了:{}", e);
return "sentinel配置自定义限流了";
}
public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){
log.error("程序逻辑异常了:{}", e);
return "程序逻辑异常了"+"\t"+e.getMessage();
}
总结:
- blockHandler,主要针对sentinel配置后出现的违规情况处理
- fallback,程序异常了JVM抛出的异常服务降级
- 两者可以同时共存
Sentinel之热点规则
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
案例演示:
添加新的业务方法:【使用自定义的限流方法】
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "------testHotKey";
}
public String dealHandler_testHotKey(String p1,String p2,BlockException exception){
return "-----dealHandler_testHotKey";
}
配置说明:
- 限流模式只支持QPS模式,固定写死了。(这才叫热点)
- @SentinelResource注解的方法参数索引,0代表第一个参数,1代表第二个参数,以此类推
- 单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。
- 上面的抓图就是第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandler_testHotKey支持方法。
测试说明:
含有参数P1,当每秒访问的频率超过1次时,回触发Sentinel的限流操作
没有热点参数P1,当每秒访问的频率超过1次时,不回触发Sentinel的限流操作
Sentinel之授权规则
在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。
在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型。白放行、黑禁止
设置黑白名单分为两步:【详细演示看代码演示】
写一个业务类实现RequestOriginParser,接口并重写其方法,在该方法中获取到路径的参数名并返回
在Sentinel控制台中进行黑名单配置
黑白名单同理 代码演示:
- 编写一个控制层接口:
@RestController
@Slf4j
public class EmpowerController {//Empower授权规则,用来处理请求的来源{
@GetMapping(value = "/empower")
public String requestSentinel4(){
log.info("测试Sentinel授权规则empower");
return "Sentinel授权规则";
}
}
- 编写一个组件类并实现
RequestOriginParser,接口并重写其方法,在该方法中获取到路径的参数名并返回
@Component
public class MyRequestOriginParser implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("serverName");
}
}
3. 控制台进行配置:
测试说明:
访问 http://localhost:8401/empower?serverName=test 和
http://localhost:8401/empower?serverName=test2 会被限流,原因是配置黑名单
但控制层中的路径并没有接收传入serverName,原因是因为在组件MyRequestOriginParser类中进行了请求参数的配置。
Sentinel规则持久化
问题:一旦我们重启微服务应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
解决方法:将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
具体做法:
- 在所要进行持久化的模块中添加依赖:
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 在所要进行持久化的模块中配置yml文件:
spring:
cloud:
sentinel:
datasource:
ds1: #自定义key
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType
解释rule-type这段配置:
- 在Nacos业务规则配置:
[
{
"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:是否集群。
Sentinel整合openfeign
配置流程补全
- 为 8003 模块添加依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 编写控制层
//openfeign + sentinel 进行服务降级
@GetMapping("/pay/nacos/get/{orderNo}")
@SentinelResource(value = "getPayByorderNo" ,blockHandler = "handlerBlockHandler")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
//模拟从数据库查询出数搏并版值给
PayDTO payDTO = new PayDTO();
payDTO.setId(1024);
payDTO.setOrderNo(orderNo);
payDTO.setAmount(BigDecimal.valueOf(9.9));
payDTO.setPayNo("pay: " + IdUtil.fastUUID());
payDTO.setUserId(1);
return ResultData.success("查询返回值: " + payDTO);
}
public ResultData handlerBlockHandler(@PathVariable( "orderNo") String orderNo, BlockException exception){
return ResultData.fail(ReturnCodeEnum.RC500.getCode() , "getPayByorderNo服务不可用,"+
"触发sentine1流控配置规则"+"\t"+"o(m_m)o" );
}
- 提供 sentinel 配置
server:
port: 8003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
sentinel:
transport:
dashboard: 192.168.37.130:8400 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false
log:
dir: G:/SpringCloud软件/sentinel-1.8.6/log/sentinel-log
- 为 common 模块添加依赖
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 使用 openfeign + sentinel 模式实现限流降级以及异常降级
提供解耦模式
//异常方式降级的全部会进入PayFeignSentineApiFallBack类的实现
//消息限流则会进入 8003 自己配置的 BlockException 限流降级
@FeignClient(value = "nacos-payment-provider",fallback = PayFeignSentineApiFallBack.class)
@Service
public interface PayFeignSentineApi {
@GetMapping("/pay/nacos/get/{orderNo}")
ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
- 实现异常方式的降级
@Component
public class PayFeignSentineApiFallBack implements PayFeignSentineApi {
@Override
public ResultData getPayByOrderNo(String orderNo) {
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"对方服务不可用");
}
}
- 为 83 模块提供依赖
<dependency>
<groupId>com.peng</groupId>
<artifactId>cloud-api-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 编写 83 模块的 yaml 激活sentinel对feign的支持
# 激活sentinel对feign的支持
feign:
sentinel:
enabled: true
- 启动类添加 @EnableFeignClients // 开启Feign客户端
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启Feign客户端
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class, args);
}
}
- 实现 openfeign 的连接
@Resource
private PayFeignSentineApi payFeignSentineApi;
@GetMapping("/consumer/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
return payFeignSentineApi.getPayByOrderNo(orderNo);
}
特特特大坑点!!!
降低父工程的pom版本号,3.0.9 和 2022.0.2 为可以正常开启 sentinel + openfeign 的版本
<!--<spring.boot.version>3.2.0</spring.boot.version>-->
<!--<spring.cloud.version>2023.0.0</spring.cloud.version>-->
<!--仅为了openfeign + alibaba sentinel 降低版本处理-->
<spring.boot.version>3.0.9</spring.boot.version>
<spring.cloud.version>2022.0.2</spring.cloud.version>
测试
正常连接
消息限流则会进入 8003 自己配置的 BlockException 限流降级
在sentinel中添加限流规则
限流显示
异常方式降级的全部会进入PayFeignSentineApiFallBack类的实现
关闭 8003 服务端口,爆出异常降级
Sentinel整合Gateway网关
参考官网的文档进行修改:
代码实战演示:
创建一个新的模块,该模块是用于设置网关的,命名为:cloudalibaba-sentinel-getway9528
- 添加依赖
<dependencies>
<!--使用 lb:/ 负载均衡需要此依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 写yml,进行对某个微服务的网关设置
server:
port: 9528
spring:
application:
name: cloudalibaba-sentinel-gateway # sentinel+gataway整合Case
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: 192.168.37.130:8400 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false
log:
dir: G:/SpringCloud软件/sentinel-1.8.6/log/sentinel-log
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# http://localhost:8003
uri: lb://nacos-payment-provider #匹配后提供服务的路由地址
predicates:
- Path=/pay/** # 断言,路径相匹配的进行路由
- 必须配置此配置类,设置配置规则,设置限流规则,参考官网给出的demo进行修改,修改成如下代码
package com.peng;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer)
{
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean(name = "customSentinelGatewayFilter")
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct //javax.annotation.PostConstruct
public void doInit() {
initBlockHandler();
}
//处理/自定义返回的例外信息
private void initBlockHandler() {
// 清除已有规则,避免重复加载
GatewayRuleManager.loadRules(Collections.emptySet());
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String,String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流(sentinel+gataway整合Case)");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}
}
实现 gateWay连接 + 负载均衡
SpringCloud Alibaba Seata 8400(处理分布式事务)
一些面试题:
- 你简历上写用微服务boot/cloud做过项目,你不可能只有一个数据库吧?请你谈谈多个数据库之间你如何处理分布式事务?
- 阿里巴巴的Seata-AT模式如何做到对业务的无侵入?
- 对于分布式事务问题,你知道的解决方案有哪些?请你谈谈?
分布式事务如何产生
- 在微服务的模式下,正常情况每个模块对应一个数据库,如何保证数据的一致性
- 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
- 关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,
业务操作需要调用三个服务来完成。
此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
Seata简介
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata工作流程简介
想要清楚Seata的工作流程,就要清楚以下三个概念:
- TC(Transaction Coordinator)事务协调器【可理解为班主任】:就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚
- TM(Transaction Manager)事务管理器【可理解为班长】:标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),他是事务的发起者,负责定义全局事务的范围,并根据TC维护全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议
- RM(Rescorce Manager)资源管理器【可理解为学生】:就是Mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的调教或回滚
工作流程图:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
seata下载安装 2.0.0版本
seata下载地址:github.com/apache/incu…
配置seata
- 创建 mysql数据库 seata
CREATE DATABASE seata;
USE seata;
2. mysql专属的建表
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
- 更改seata本身的配置
进入 G:\SpringCloud软件\seata 2.0.0\conf 备份 application.yml(出厂配置)
# 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:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #后续自己新建SEATA_GROUP组,不想就写DEFAULT_GROUP
username: nacos
password: nacos
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_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.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?rewriteBatchedStatements=true
user: root
password: 123456
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/**
- 在bin目录下执行启动
seata-server.bat
@GlobalTransactional注解原理
官网地址:seata.apache.org/zh-cn/docs/…
开启该注解后,无论是在插入过程中出现异常,都会将数据插入到数据库中,并且插入一行记录在undo_log表中,若中途出现了异常就会触发事务的回滚,将当前插入的数据全部清空,undo_log表插入的数据也会删除
机制:【分为两个阶段】
第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
第二阶段:
提交异步化,非常快速的完成
回滚通过一阶段的回滚日志进行反向补偿【undo_log的作用就是进行反向补偿】
细说两大阶段:
在第一阶段,Seata会拦截 ”业务SQL“
解析SQL语义,找到 "业务SQL"更新业务数据,在业务数据被更新前,将其保存成 “before image”
执行 “业务SQL” 更新业务数据,在业务数据更新之后
其保存成 “after image”,最后生成行锁
以上 操作全部在一个数据库事务内完成,这样保证了一阶段的原子性。
二阶段分为两种情况:
- 正常提交
- 出现异常,触发事务回滚
如果是顺利正常提交的话:
因为 “业务SQL” 在一阶段已经提交到数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可
如果是异常回滚的话:
Seata就需要回滚一阶段已经执行的 ”业务SQL“,还原业务数据
回滚方式便是 “before image” 还原业务数据;但是还原业务前先要校验脏写,对比 “数据库当前业务数据” 和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就要进行人工处理。
实战seata
准备三个数据库以及对应的业务表与信息
#存储订单的数据库
create database seata_order;
#存储库存的数据库
create database seata_storage;
#存储账户信息的数据库
create database seata_account;
为三个数据库分别建对应的undo_log回滚日志表
每次处理sql时,undo_log会记录处理之前的数据,当出现异常时,回滚之前的数据
无论是否出现异常,当处理完sql后,undo_log都会删除掉刚保存的数据
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--
-- for AT mode you must to init this sql for you business database. the seata server not need it.
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';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
为上个数据库分别建立对应的业务表
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
CREATE TABLE t_storage(
`id`BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',
`total`INT(11) DEFAULT NULL COMMENT'总库存',
`used`INT(11)DEFAULT NULL COMMENT '己用库存',
`residue`INT(11) DEFAULT NULL COMMENT'剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
CREATE TABLE t_account(
`id` BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11)DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',
`residue` DECIMAL(10,0)DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)
VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
使用 mybatis_gernerator2024 模块 逆向生成
样例 seata_order 其他同理
- 修改 config.properties 文件中的数据库
# seata_order
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url= jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url= jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
- 修改 generatorConfig.xml 中需要构建的表
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<!-- <table tableName="t_pay" domainObjectName="Pay">-->
<!-- <generatedKey column="id" sqlStatement="JDBC"/>-->
<!-- </table>-->
<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
<!-- <table tableName="t_account" domainObjectName="Account">-->
<!-- <generatedKey column="id" sqlStatement="JDBC"/>-->
<!-- </table>-->
<!-- <table tableName="t_storage" domainObjectName="Storage">-->
<!-- <generatedKey column="id" sqlStatement="JDBC"/>-->
<!-- </table> -->
</context>
</generatorConfiguration>
- 刷新maven后,启动此插件生成
- 添加依赖
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</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.peng</groupId>
<artifactId>cloud-api-common</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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 配置 order 的 yaml文件
server:
port: 2001
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.peng
configuration:
map-underscore-to-camel-case: true
# nacos配置
spring:
application:
name: seata-order-service
profiles:
active:
#active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境
cloud:
nacos:
discovery:
server-addr: localhost:8848
group: SEATA_GROUP
sentinel:
transport:
dashboard: 192.168.37.130:8400 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false
log:
dir: G:/SpringCloud软件/sentinel-1.8.6/log/sentinel-log
# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml
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
#读取consul中的配置到这里
username: root
password: 123456
# ========================zipkin===================
management:
zipkin:
tracing:
endpoint: http://192.168.37.130:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: localhost: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
# 激活sentinel对feign的支持
feign:
sentinel:
enabled: true
logging:
level:
io.seata: info
- 配置主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.peng.mapper") //导入 tk 的包后续可以不用写@Mapper注解
public class Main2001 {
public static void main(String[] args) {
SpringApplication.run(Main2001.class, args);
}
}
实现业务调用
seata-storage-service2002
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@RequestMapping("/storage/decrease")
public ResultData decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count){
storageService.decrease(productId,count);
return ResultData.success("扣减库存成功");
}
}
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageMapper.decrease(productId, count);
log.info("------->storage-service中扣减库存结束");
}
}
public interface StorageMapper extends Mapper<Storage> {
void decrease(@Param("productId")Long productId,
@Param("count")Integer count);
}
<!-- void decrease(@Param("productId")Long productId,
@Param("count")Integer count); -->
<update id="decrease">
update t_storage
set
used = used + #{count},
residue = residue - #{count}
where product_id = #{productId}
</update>
seata-account-service2003
@RestController
public class AccountController {
@Resource
private AccountService accountService;
@RequestMapping("/account/decrease")
public ResultData decrease(@RequestParam("userId") Long userId,
@RequestParam("money") Long money) {
accountService.decrease(userId, money);
return ResultData.success("扣减账户余额成功!");
}
}
public interface AccountService {
void decrease(Long userId, Long money);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣减账户余额开始");
accountMapper.decrease(userId, money);
//模拟超时异常,全局事务回滚
//myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣减账户余额结束");
}
private static void myTimeOut(){
try {
TimeUnit.SECONDS.sleep(65);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
public interface AccountMapper extends Mapper<Account> {
void decrease(@Param("userId") Long userId,
@Param("money") Long money);
}
<!--void decrease(@Param("userId") Long userId,
@Param("money") Long money);-->
<update id="decrease">
update t_account
set
used = used + #{money},
residue = residue - #{money}
where user_id = #{userId}
</update>
seata-order-service2001
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public ResultData create(Order order) {
orderService.create(order);
return ResultData.success(order);
}
}
public interface OrderService {
void create(Order order);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource //调用库存微服务
private StorageFeignApi storageFeignApi;
@Resource // 调用账户微服务
private AccountFeignApi accountFeignApi;
@Override
public void create(Order order) {
//xid全局事务id的检查,重要!!!
String xid = RootContext.getXID();
//1 新建订单
log.info("------->开始新建订单,全局事务id为:{}", xid);
//订单在创建时的状态是 0
order.setStatus(0);
int result = orderMapper.insertSelective(order);
Order orderFromDB = null;
if(result > 0){
//插入成功 从mysql中查出刚插入的记录
orderFromDB = orderMapper.selectOne(order);
//2 扣减库存
log.info("------->订单微服务开始调用库存,做扣减Count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
//3 扣减账户
log.info("------->订单微服务开始调用账户,做扣减Money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
//4 修改订单状态
log.info("------->订单微服务开始修改订单状态");
//订单在完成的状态是 1
orderFromDB.setStatus(1);
// 匹配相对应的数据 [where] 跟Lambda表达式的写法差不多
Example example = new Example(Order.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("userId", orderFromDB.getUserId());
criteria.andEqualTo("status", 0);
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, example);
log.info("------->订单微服务修改订单状态结束,结果:{}", updateResult > 0);
}
System.out.println();
log.info("------->结束新建订单,{}", xid);
}
}
测试接口是否连通
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
3个数据库的信息都正常更改
当不加 @GlobalTransactional
开启 myTimeOut 超时异常
(openfeign的超时异常,会是调用服务异常,别调用的服务不会异常)
order 表中记录了一个 未完成(0) 的数据,但是 account 和 storage 表还是更改了,所以出现错误
开启 10/0 异常(account 服务直接异常)
order 表中记录了一个 未完成(0) 的数据,storage 表更改了,但是 account异常所以没有更改,
再次出现错误
加 @GlobalTransactional
@Service
@Slf4j //自己取一个全局服务异常名
@GlobalTransactional(name = "peng-create-order", rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {
开启 myTimeOut 超时异常
因为是 65 秒的 超时异常,order 中照常进入一个 购买信息,
但是当异常出现时,信息被删除了,数据被回滚了
开启 10/0 异常(account 服务直接异常)
数据库没有变化,数据被回滚了