目录
16.SpringCloud Alibaba Nacos服务注册和配置中心
SpringCloud Alibaba简介
1. 为什么出现 SpringCloud Alibaba
2. SpringCloud Alibaba带来了什么
2.1 能干什么
2.2 去哪里下载
2.3 怎么玩
3. 学习资料的获取
17.SpringCloud Alibaba Nacos服务注册和配置中心
一、 Nacos简介
1 是什么
2 能干什么
3 下载地址
4 各注册中心比较
二、 安装并运行Nacos
三、 Nacos作为服务注册中心演示
1 基于Nacos的服务提供者
1.1 建moudle
1.2 pom
1.3 yml
1.4 主启动类
1.5 业务类
1.6 测试
1.7 相同配置再新建一个module 9002
2 基于Nacos的服务消费者
2.1 新建module cloudalibaba-consumer-nacos-order83
2.2 pom
2.3 yml
2.4 主启动类
2.5 业务类
2.6 测试
3. 服务注册中心对比
3.1 Nacos与CAP
3.2 CP与AP的切换
四、 Nacos作为服务配置中心演示
1. Nacos作为配置中心——基础配置
1.1 建module cloudalibaba-config-nacos-client3377
1.2 pom
1.3 yml
1.4 主启动类
1.5 业务类
1.6 在Nacos中添加配置信息
1.7 测试
2. Nacos作为配置中心——分类配置
2.1 分布式开发中的多环境多项目管理问题
2.2 Nacos的图形化管理界面
2.3 Namespace+Group+Data ID三者关系?为什么这么设计?
3. 三种方案加载配置
3.1 DataID方案
3.2 Group方案
1新建一个配置文件,添加到RPOD_GROUP分组
2新建一个配置文件,添加到TEST_GROUP分组
3在config下增加一条group的配置即可。可配置为PROD_GROUP或TEST_GROUP
4测试:
3.3 namesapce方案
1.新建Namesapce
2.回到服务管理-服务列表查看
3.在这两个新建的namespace中分别新建配置文件
4.修改3377的yml文件
5.测试
3.4 总结
4. Nacos集群和持久化配置(重点)
4.1 官网说明
4.2 Nacos嵌入式数据库derby切换到mysql-windows单机版derby到mysql切换步骤
1.首先在nacos安装目录的conf目录下找到一个名为nacos-mysql.sql的sql脚本
2.在conf目录下找到application.properties
3.重新启动nacos,可以看到是个全新的空记录界面,以前是记录进derby
4.在naocs新建配置时,会自动保存到mysql数据库中
(踩坑)1.1.4版本nacos使用外部数据库Mysql8 (这种方式我也亲自试了OK)
5.修改nacos源码(可行)
4.3 Linux版Nacos+MySQL生产环境配置
4.3.1 Nacos 下载Linux版
4.3.2 集群配置步骤(重点)
4.4 微服务cloudalibaba-provider-payment9002启动注册进nacos集群
18.SpringCloud Alibaba Sentinel实现熔断与限流
16.SpringCloud Alibaba Nacos服务注册和配置中心
SpringCloud Alibaba简介
1. 为什么出现 SpringCloud Alibaba
Spring Cloud Netflix项目进入维护模式
Spring Cloud Greenwich.RC1 available now
进入维护模式意味着Spring Cloud Netflix 将不再开发新的组件
我们都知道Spring Cloud 版本迭代算是比较快的,因而出现了很多重大ISSUE都还来不及Fix就又推另一个Release了。进入维护模式意思就是目前一直以后一段时间Spring Cloud Netflix提供的服务和功能就这么多了,不在开发新的组件和功能了。以后将以维护和Merge分支Full Request为主
新组件功能将以其他替代平代替的方式实现
2. SpringCloud Alibaba带来了什么
官网
2.1 能干什么
服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
2.2 去哪里下载
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
之前在父工程的pom文件中已经引入了alibaba的依赖
2.3 怎么玩
3. 学习资料的获取
官网:Spring Cloud Alibaba
英文:
https://github.com/alibaba/spring-cloud-alibaba
Spring Cloud Alibaba Reference Documentation
中文:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
17.SpringCloud Alibaba Nacos服务注册和配置中心
对应着我们前面学的:Eureka/Consul/Zookeeper(服务注册) Config+Bus(配置中心)
一、 Nacos简介
Nacos——Naming Configuration Service
1 是什么
●一个更易于构建云原生应用的动态服务发现、配置管理和服务的管理平台。
●Nacos:Dynamic Naming and Configuration Service
●Nacos就是注册中心 + 配置中心的组合, Nacos = Eureka + Config + Bus
2 能干什么
替代Eureka做服务注册中心;替代Config做服务配置中心
3 下载地址
https://github.com/alibaba/Nacos
官网文档:Redirecting to: https://nacos.io/
Spring Cloud Alibaba Reference Documentation
4 各注册中心比较
二、 安装并运行Nacos
本地需要准备java8+maven环境。
官网下载:https://github.com/alibaba/nacos/releases
老师安装的1.1.4,我这里使用的最新版2.0.3
解压安装包,运行bin目录下的startup.cmd
默认的是集群模式,我们需要单机启动cmd中输入:
startup.cmd -m standalone
(nacos单机模式启动命令) 即可通过单机模式启动
命令运行成功后直接访问http://localhost:8848/nacos 默认账号密码都是nacos
启动成功。
三、 Nacos作为服务注册中心演示
官网手册
1 基于Nacos的服务提供者
1.1 建moudle
cloudalibaba-provider-payment9001
1.2 pom
父工程中需要引入alibaba的依赖,之前已经引入过了
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope>
</dependency>
本模块pom
<dependencies><!--SpringCloud ailibaba nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- SpringBoot整合Web组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--日常通用jar包配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
1.3 yml
按照官网上的说明配置。
server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址 management: endpoints: web: exposure: include: '*'
1.4 主启动类
package com.springcloud.alibaba; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 { public static void main(String[] args) { SpringApplication.run(PaymentMain9001.class,args); } }
1.5 业务类
package com.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController; @RestController
public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping(value = "/payment/nacos/{id}") public String getPayment(@PathVariable("id") Integer id) { return "nacos registry, serverPort: "+ serverPort+"\t id"+id; }
}
1.6 测试
这里不像eureka还要写注册中心微服务,直接安装打开nacos即可。
启动9001,查看nacos控制台,注册成功
访问http://localhost:9001/payment/nacos/1
nacos服务注册中心+服务提供者9001都OK了
1.7 相同配置再新建一个module 9002
步骤与9001一致,就是端口号改成9002
这里有一个简单的方法,虚拟映射一个和9001配置相同的9011微服务模块,端口号是9011
控制台服务名称
现在有两个支付模块,9001和9002。
2 基于Nacos的服务消费者
Nacos自带负载均衡
2.1 新建module cloudalibaba-consumer-nacos-order83
2.2 pom
<dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>org.xu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0.0</version> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>
nacos自带负载均衡
Ribbon:支持负载均衡,自带RestTemplate
2.3 yml
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
2.4 主启动类
package com.springcloud.alibaba; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain83.class,args); } }
2.5 业务类
package com.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @RestController
public class OrderNacosController { @Resource private RestTemplate restTemplate; @Value("${service-url.nacos-user-service}") private String serverURL; @GetMapping("/consumer/payment/nacos/{id}") public String paymentInfo(@PathVariable("id") Long id){ return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class); } }
配置类
package com.springcloud.alibaba.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate; @Configuration
public class ApplicationContextBean { @Bean @LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力 RestTemplate getRestTemplate(){ return new RestTemplate(); } }
2.6 测试
启动9001、9002、83
nacos控制台
测试链接:http://localhost:83/consumer/payment/nacos/1
发现9001与9002交替出现,即轮询负载均衡。
可能出现的问题:
当消费者通过http://nacos-payment-provider去调用微服务时抛出UnknowHostException:未知主机名异常。
这是因为nacos-payment-provider对应两个微服务实例,他不知道用哪个微服务,所以报错。
解决:加上负载均衡注解 @LoadBalanced
3. 服务注册中心对比
3.1 Nacos与CAP
Nacos可以在CP与AP之间切换
3.2 CP与AP的切换
C(consistency):所有节点在同一时间看到的数据是一致的,强一致性;
A(availability):的定义是所有的请求都会收到响应,最起码有一个兜底回复,高可用性。
何时选择使用何种模式?
AP:
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如 Spring cloud 和 Dubbo 服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。
CP:
如果需要在服务级别编辑或者存储配置信息,那么 CP 是必须,K8S服务和DNS服务则适用于CP模式。
CP模式下则支持注册持久化实例,此时则是以 Raft 协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
怎么切换:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
四、 Nacos作为服务配置中心演示
以前我们将所有的配置信息写到了GitHub上,用Config+Bus来进行自动刷新和动态的更新。
现在我们可以直接把配置文件写进Nacos,然后再用Nacos做类似于config这样的功能,直接从Nacos上抓取我们的配置信息。
1. Nacos作为配置中心——基础配置
主要是添加一个:nacos-config依赖
com.alibaba.cloudspring-cloud-starter-alibaba-nacos-config
1.1 建module cloudalibaba-config-nacos-client3377
1.2 pom
<dependencies><!--nacos-config--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--nacos-discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--web + actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--一般基础配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
1.3 yml
这里需要配置两个,一个bootstrap和一个application。
原因:Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application。
全局的放在:bootstrap.yml 自己的放在:application.yml
bootstrap:
微服务端口:3377,服务名nacos-config-client,注册进nacos:localhost:8848 作为配置客户端
# nacos配置
server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
application:
spring: profiles: active: dev # 表示开发环境
bootstrap + application 就表示我要去配置中心找名为dev.yaml的文件。
1.4 主启动类
package com.springcloud.alibaba; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 { public static void main(String[] args) { SpringApplication.run(NacosConfigClientMain3377.class,args); }
}
1.5 业务类
通过Spring Cloud原生注解@RefreshScope 实现配置自动更新
package com.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo(){ return configInfo; } }
1.6 在Nacos中添加配置信息
Nacos中的匹配规则
理论:
●Nacos中的dataid的组成格式与SpringBoot配置文件中的匹配规则
官网:Nacos 融合 Spring Cloud,成为注册配置中心 | Nacos 官网
注意nacos只识别yaml,不支持yml。
最终公式:{spring.application.name}-{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
结果: nacos-config-client-dev.yaml
公式图解说明
实操
配置新增:
自己新建一个配置
1.7 测试
启动3377
发送请求:http://localhost:3377/config/info
测试成功
自带动态刷新:修改nacos中的yaml配置文件,再次调用查看配置,发现配置刷新了。
2. Nacos作为配置中心——分类配置
2.1 分布式开发中的多环境多项目管理问题
●问题1:
实际开发中,通常一个系统会准备
dev开发环境
test测试环境
prod生产环境。
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
●问题2:
一个大型分布式微服务系统会有很多微服务子项目,
每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境......
那怎么对这些微服务配置进行管理呢?
2.2 Nacos的图形化管理界面
配置管理:
命名空间:
2.3 Namespace+Group+Data ID三者关系?为什么这么设计?
类似Java里面的package名和类名,最外层的namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。
三者情况
默认情况:
Namespace=public,Group=DEFAULT_GROUP, 默认Cluster是DEFAULT
Nacos默认的命名空间是public,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。
比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
最后是Instance,就是微服务的实例。
3. 三种方案加载配置
3.1 DataID方案
指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置
默认空间+默认分组+新建dev和test两个DataID
●dev配置DataID,上一讲配置过
●新建test配置DataID
这里命名空间是默认的public,Group也是默认的。
通过spring.profile.active属性就能进行多环境下配置文件的读取
重启3377 测试http://localhost:3377/config/info,成功读取到test配置下的config.info
3.2 Group方案
默认Group是DEFAULT_GROUP,现在通过Group实现环境分区
1新建一个配置文件,添加到RPOD_GROUP分组
2新建一个配置文件,添加到TEST_GROUP分组
界面就可以看到我们配置的文件了
3在config下增加一条group的配置即可。可配置为PROD_GROUP或TEST_GROUP
4测试:
3.3 namesapce方案
1.新建Namesapce
2.回到服务管理-服务列表查看
3.在这两个新建的namespace中分别新建配置文件
4.修改3377的yml文件
bootstrap:
5.测试
3.4 总结
DataID方案是在默认namesapce和默认Group下,创建两个不同的DataID。
Group方案是在默认namespace下,新建两个DataID相同的配置文件,通过指定不同的分组来读取不同的配置。
Namespace方案,是相同的Group,相同的DataID,创建并指定不同的namespace来读取不同配置。
4. Nacos集群和持久化配置(重点)
我们之前在学eureka的时候,配置了两个eureka注册中心微服务。学Nacos时,不用我们单独新建注册中心微服务模块了,直接安装使用即可,很方便。
但是:如果这个注册中心挂了怎么办?我们目前就开了一个Nacos程序,也没有配置集群。显然,实际情况中不可能只有一个Nacos注册中心,因此需要用到nacos集群。
集群官网文档
4.1 官网说明
1.架构图:
这里vip表示virtual ip(虚拟IP)
2.要将配置持久化到数据库中:MySQL 不用nacos内嵌的数据库
3.通俗易懂的Nacos集群架构图
4.重点说明:
默认Nacos使用嵌入式数据库实现数据的存储,我们重启Nacos后,以前的配置文件不会消失。
但是,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。每个nacos都有自己独立的嵌入式数据库,存放的数据不一致。
为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
nacos支持的三种部署模式:
●单机模式:用于测试和单机使用
●集群模式:用于生产环境,确保高可用
●多集群模式:用于多数据中心场景
4.2 Nacos嵌入式数据库derby切换到mysql-windows单机版derby到mysql切换步骤
Nacos默认自带的是嵌入式数据库derby,那么如果做集群时每个nacos都自带一个derby,那么就有三个存储配置稳健的数据库,显然数据的统一性存在问题。
1.首先在nacos安装目录的conf目录下找到一个名为nacos-mysql.sql的sql脚本
然后执行nacos-mysql.sql脚本
注意:数据库nacos_config需要自己创建
新建完数据库以后打开这个文件复制sql执行一下。
nacos-mysql.sql(10 KB)
2.在conf目录下找到application.properties
到这一步的时候请再次确认一下你的nacos版本号是多少,我因为自己没注意,
每次启动的时候都可以看到,或者登录web页面的时候,我这里用的版本竟然是2.0.3,所以不能按照视频里的来,配置写法根本就不一样,看下图解释
3.重新启动nacos,可以看到是个全新的空记录界面,以前是记录进derby
在bin目录下的cmd 输入 startup.cmd -m standalone 执行,这里还是单机模式,只不过数据库从derby迁移到了mysql
可以看到,重启之后之前的配置都没了,说明迁移成功。
4.在naocs新建配置时,会自动保存到mysql数据库中
到数据库中查看一下有没有生产新的数据
(踩坑)1.1.4版本nacos使用外部数据库Mysql8 (这种方式我也亲自试了OK)
后面Seata章节由于Nacos、sentinel、seata存在版本对应关系,因此需要安装1.1.4版本nacos。
我在安装nacos1.1.4配置MYSQL8的application.properties后,报错:
尝试了如下方法:
1在nacos\plugins\mysql(自己创建)文件夹下放mysql8对应版本的jar包(不管用)
2修改application.properties文件名为bootstrap.properties:可以正常启动nacos,但是数据库并没有从derby切换为mysql
3添加时区:
一般我在使用mysql8的时候都会添加上时区,仍然不管用。
5.修改nacos源码(可行)
找到nacos官网 Nacos 快速开始 | Nacos 官网
点击
然后一直翻,找到1.1.4
修改nacos源码
下载nacos1.1.4源码,修改父工程pom的mysql-connector-java的版本,将5.1.34改成自己mysql对应的版本
这里一改,naming模块下的com.alibaba.nacos.naming.healthcheck.MysqlHealthCheckProcessor就会报错 ,
把import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;改为import com.mysql.cj.jdbc.MysqlDataSource;
这里我注释的一定要删掉(这里我注释掉主要是为了区分),不然会报错:
然后在源码根目录下打开cmd,使用maven打包(实际我是直接在idea里面打包的)
执行:mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
打包好的nacos存放在根目录\distribution\target目录下 ,当前项目目录下去找
这个压缩包就相当于我们从官网上下的。然后复制到你想解压的地方,解压。
随后,配置conf\application.properties里面的数据库:
serverTimezone=UTC不要忘记了。(每次配置mysql8,都要带上这个设置)
然后启动nacos:
启动成功,我们试一下,能不能存到数据库中,新建一个配置,然后查看数据库中是否生成!
4.3 Linux版Nacos+MySQL生产环境配置
一定要先把环境准备好! centos7/Ubuntu/ + maven(3.2+)+mysql(5.6.5+)+JDK1.8 一站式部署环境配置地址: Linux服务器服务搭建及项目部署超详细 - 哩个啷个波 - 博客园
预计需要1个Nginx+3个nacos注册中心+1个mysql
4.3.1 Nacos 下载Linux版
下载nacos-linux:https://github.com/alibaba/nacos/releases/tag/2.0.3 我放在了/usr/local 下
解压: tar -zxvf nacos-server-2.0.3.tar.gz
cp -r nacos /mynacos/ 递归拷贝nacos 文件夹到mynacos文件夹下 (我这里没有执行这步)
4.3.2 集群配置步骤(重点)
(1) Linux服务器上mysql数据库配置
跟windows一样,linxu的nacos 在 /nacos/conf 目录下有一个nacos-mysql.sql的sql脚本
我是通过SQLyog客户端远程连接,然后执行。
安装MySQL可以参考我这里的文章: Linux服务器服务搭建及项目部署超详细 - 哩个啷个波 - 博客园
直接使用连接远程的方式去执行创建mysql的表。表创建成功以后我们就可以去Linux服务器上面的MySQL去验证这些表是否已经创建好了。
(2) application.properties 配置
/usr/local/nacos/conf 下,修改内容:数据库密码自己要配置自己的并且要配置正确
spring.datasource.platform=mysqldb.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
配置的时候注意一下几个地方,但是如果你的nacos版本是2.0以上,我自己用的那么这个配置文件中是不需要手动写或者复制的,在注释里面是可以找到的,只需要打开注释就可以了。
(3) Linux服务器上nacos的集群配置cluster.conf
梳理出3台nacos集器的不同服务端口号 3333 4444 5555
cp cluster.conf.example cluster.conf
vim cluster.conf
查找自己的ip地址 或者使用命令ip addr
(4) 编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口
/usr/local/nacos/bin 目录下有startup.sh,平时单机版的启动,都是./startup.sh即可。
集群启动,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例。
命令:./startup.sh -p 3333 表示启动端口号为3333的nacos服务器实例,和上一步的cluster.conf配置的一致。
注意:2.0版本的-p被占用了,可以用大写P或者其他字母,刚改完浏览器会有延迟,过一会才会好。
●1.1.4 版本修改前后对比方式: 下面的信息一定要配置对 PORT=$OPTARG
这里是大写O,可不是0,我写错了第一次的时候 ,下面是-Dserver.port=${PORT}
|
●2.0.3版本修改前后对比
修改前 | 修改后 |
| |
| |
如果防火墙没关,记得将3333,4444,5555端口号设置可访问
最终只能启动一个nacos,尝试各种办法未解决,更换nacos1.1.4后没有问题。
(番外)nacos linux踩坑必看
最开始我用的nacos2.0.3,到集群那就出现了问题,只能开一个nacos,剩下两个开起来就报错。
尝试了各种方法,改JVM配置等等,还是不行。最后选择使用跟视频里面一样的nacos版本1.1.4
使用1.1.4的时候数据库的配置 /usr/local/mynacos/nacos/conf/
(1.1.4的安装路径) application.properties 文件使用mysql8+ 需要加上时区 2.0.3 不需要
cluster.conf 还是跟之前一样配置;
startup.sh 跟老师一样的改法,然后需要将JVM的配置修改,因为我的虚拟机只有4G内存,所以我把JVM配置调整为如下:
重启方式:
上面启动完以后一定要去日志表里面看一下有没有报错信息ERROR,日志启动的可能比较慢,耐心等待一下,没有的话用 ps -ef|grep nacos
看一下服务是不是正常启动着
然后分别执行开启3333、4444、5555三个nacos,成功开启nacos集群
使用nacos2+的朋友可以尝试开三台虚拟机,参考官方手册配置集群nacos官方手册
注意:用nacos1.4以上的,貌似就用不了这个模拟集群的方法了,老实复制3个nacos文件夹再逐个设置端口号启动 同一台机启动多个nacos的问题
(5) Nginx的配置,由它作为负载均衡器
Nginx基础
/usr/local/nginx/conf 下nginx.conf文件为默认配置文件 启动nginx通过-c可以指定配置文件启动。我这里将原来的配置文件拷贝为nginx_nacos.conf
启动nginx,cd到/usr/local/nginx/sbin下 ,执行:
./nginx -c /usr/local/nginx/conf/nginx_nacos.conf
别忘了防火墙开启1111端口的访问:
firewall-cmd --add-port=1111/tcp --permanent
firewall-cmd --reload
(6) 测试
测试通过nginx访问nacos,成功登陆
然后我们新建一个配置文件:
成功存到Linux的数据库中!
4.4 微服务cloudalibaba-provider-payment9002启动注册进nacos集群
修改9002的application.yml文件。
做监控需要把这个全部暴露出来
18.SpringCloud Alibaba Sentinel实现熔断与限流
一、Sentinel
https://github.com/alibaba/Sentinel 中文
Sentinel 是轻量级的流量控制、熔断降级Java库;功能类似于Hystrix
Web端界面特点:采用的是懒加载模式,服务必须被请求一次后才能在界面中看到,这点需要注意了
下载地址
怎么玩:
入门文档
服务使用中的各种问题:服务雪崩、服务降级、服务熔断、服务限流
二、安装Sentinel控制台
Sentinel分为两个部分:
●核心库(Java客户端)不依赖任何框架/库,能够云星宇所有Java运行时环境,同时对Dubbo/Spring Cloud等框架也有较好的支持——后台;
●控制台(Dashboard)基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器——前台 8080
安装步骤
下载到本地sentinel-dashboard-1.8.2.jar
运行命令:
前提需要Java8,且8080端口不能被占用;java -jar sentinel-dashboard-1.8.2.jar (Sentinel启动命令)
访问 localhost:8080,账号密码均为sentinel
三、初始化演示工程
3.1 启动Nacos8848
3.2 新建Module cloudalibaba-sentinel-service8401
3.2.1 pom
以后基本上nacos 跟sentinel一起配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud2020</artifactId> <groupId>org.xu.springcloud</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-sentinel-service8401</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud ailibaba 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> <!-- SpringBoot整合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> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
3.2.2 yaml
spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。
比如 Sentinel 控制台添加了1个限流规则,会把规则数据push给这个Http Server接收,Http Server再将规则注册到Sentinel中。
spring.cloud.sentinel.transport.port:指定与Sentinel控制台交互的端口,应用本地会启动一个占用该端口的Http Server
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*'
3.2.3 主启动类
package com.cloudalibaba.sentinel; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } }
3.2.4 业务类
流量控制controller:FlowLimitController
package com.cloudalibaba.sentinel.controller; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } }
3.3 测试
启动Sentinel8080 java -jar sentinel-dashboard-1.8.2.jar、启动微服务8401
查看Sentinel控制台,发现什么也没有。
原因:Sentinel采用懒加载机制
执行一下:http://localhost:8401/testA
sentinel8080正在监控微服务8401
四、流控规则
流量限制控制规则,分为:流控模式和流控效果
各选项含义:
4.1 流控模式
流控模式有三种:直接、关联、链路
4.1.1 直接(默认)+快速失败(默认)
(1) QPS直接快速失败
QPS:query per second,每秒钟的请求数量,当调用该api的QPS达到阈值时,进行限流。
下面设置表示1秒钟内查询一次就是OK,若QPS>1,就直接-快速失败,报默认错误
编辑好以后返回界面可以看到自己的设置
测试一下,当/testA的访问超过1次/s是,页面报错。被Sentinel限流,还能继续请求只要QPS<=1。
小结:表示1秒钟内请求次数大于1,就直接快速失败,报默认错误。
直接调用默认的报错信息在技术上是OK的,但是是否应该有自定义的后续处理?应该有类似Hystrix的fallback的兜底方法。
(2) 线程数直接快速失败
当调用该api的线程数达到阈值的时候,进行限流。
与QPS直接快速失败不同的是,QPS情况下限制的是流量,比如银行的人流量只能是1人/s,也就是说每次只能一个人进入银行办理业务;而线程数就好比银行只有一个窗口开放,一群人都可以进入银行,但是每次只能处理一个人的业务。
演示效果:
先修改一下8401的业务类
然后重启8401,测试/testA,最好用两个浏览器访问,效果更明显
4.1.2 关联
当关联的资源达到阈值时,就限流自己。比如当与A关联的资源B达到阈值后,就限流A自己。
支付接口达到阈值,限流下订单的接口。
- 配置
设置效果:当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名
- 测试
单独访问testB成功。
postman模拟并发密集访问testB
先创建一个集合,名字自己随便取。
然后将创建的访问/testB的请求保存在创建的集合中
设定集合运行参数,20个线程,每次间隔0.3s访问一次(QPS>1),执行:
然后再访问/testA,发现被限流,等postman执行完毕,testA又可以访问了
4.1.3 链路
需要测试链路的话,springcloud 阿里巴巴版本需要2.1.1.RELEASE以上,在父工程的pom中修改,不要直接在子module的pom中修改,版本有对应关系,不然报错。
●Sentinel从1.6.3版本开始,Sentinel Web Filter 默认收敛所有的URL入口的Context,因此链路限流不生效
●1.7.0版本开始,官方在CommomFilter中引入了WEB_CONTEXT_UNIFY
这个init parameter,用于控制是否收敛context,将其配置为false
即可根据不同的URL进行链路限流
●Spring Cloud Alibaba 在2.1.1.RELEASE版本后,可以通过配置spring.cloud.sentinel.web-context-unify=false关闭
https://github.com/alibaba/Sentinel/issues/1313
测试
启动8401,给/testA设置链路+快速失败流控规则:
这里入口资源就是簇点链路中,资源名称的上一级。
访问http://localhost:8401/linktestA ,多次刷新出现限流
,但是这种情况我个人觉得跟直接快速失败区别不大,只直接是监控/testA资源,而链路是监控/testA的资源入口sentinel_web_servlet_context。
然后我又参看了其他博客,流控模式——链路。增加了FlowLimitService 修改了controller
分别通过/linktetA 和 /linktestB都是message的入口,然后设置/linktestA入口的流量限制,发现不起作用。。。
4.2 流控效果
快速失败在上面的流控模式演示过了,他是默认的流控效果,直接失败,抛出异常。源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
4.2.1 warm up 预热
官网 源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
配置图片说明
测试
5秒之前得阈值不是10,5秒时间过了以后才能达到设定的阈值,得有前戏才行,不能一上来就。。。
狂点请求,可以看通过的QPS逐渐增加,最开始会报错限流,之后就可以抗住10/s的QPS了
**应用场景:**秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。
4.2.2 排队等待
官网 源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。
/testA的QPS最大为1,超过的话就排队等待,等待的超时时间为20000ms。
修改一下业务代码,把线程名打印出来以验证是否排队。
postman配置成20个请求,延时300毫秒
测试
postman:
遍历20次,耗时时间接近20,说明每秒请求限制为1个。
也可以监测idea控制台输入语句看看频率是多少。但是我发现Sentinel配置的话要检查一下配置是不是还在,刷新页面以后看看是不是就消失了。
可以看到刚好满足1s一个请求,说明请求的执行进行了排队。
五、降级规则(熔断规则)
官网
老版本的Sentinel的断路器是没有半开状态的,半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。具体可以参考Hystrix。(在Hystrix中 快照时间窗口是值 阈值检测时间 ,而休眠时间窗口是指 断路器从开启到半开状态间隔的时间)
新版本的Sentinel加入了半开状态!
5.1 降级策略
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
5.1.1 慢调用比例,RT(平均响应时间,秒级)
老版本:
新版本:
●慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。(跟豪猪科类似)
实战测试
业务类中加一个rest 接口,以用于测试:
@GetMapping("/testD")
public String testD()
{//暂停几秒钟线程try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }log.info("testD 测试RT");return "------testD";
}
访问没有问题
编辑熔断规则:
在1000ms的统计时间内,总请求数(超过5次)中有80%的请求最大RT超过了200ms,那么触发熔断机制,熔断2s。
jmeter压测:
设置线程数10个,响应时间1秒,
添加取样器,HTTP请求
添加请求路径
永远一秒钟打进来10个线程(大于5个了)调用testD,我们希望200毫秒处理完本次任务,如果超过200毫秒还没处理完,在未来s秒钟的时间内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了。
testD被熔断了
从实时监控也可以看到,看看什么时间熔断的。
后续我停止jmeter,没有这么大的访问量了,断路器半开到关闭(保险丝恢复),微服务恢复OK。
5.1.2 异常比例
新版本:
●异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
实战测试
修改业务类:
@GetMapping("/testD")
public String testD()
{log.info("testD 测试RT");int age = 10/0;return "------testD";
}
编辑熔断规则:
1000ms统计时长内,大于5次的请求中超过80%的请求出现异常,则熔断2s。
jmeter压测:
单独访问一次,必然来一次报错一次(int age = 10/0),调一次错一次;开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了。断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务降级了。
testD被熔断。
停掉jemeter后过2s。报/zero错误,因为业务类中有个10/0。
5.1.3 异常数
老版本:
时间窗口一定要大于等于60秒。
新版本
●异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
实战测试
修改业务类:
@GetMapping("/testE")
public String testE()
{log.info("testE 测试异常比例");int age = 10/0;return "------testE 测试异常比例";
}
编辑熔断规则:
手动测试:
这里我测试有bug,虽然熔断了但是熔断时长不是我配置的5s,大约是一分钟,统计时长也不是1s,好像也是一分钟,同时没达到最小请求数,只达到3次异常就直接熔断了。虽然我用的新版本,但是逻辑好像跟老版本的一样?
六、热点key限流
6.1 基本介绍
官网
何为热点:热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作。比如:
●商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
●用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限制会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限制。热点参数限流可以看作是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel利用LRU策略统计最近最常访问的热电参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
6.2 基本使用
兜底防范分为系统默认和客户自定义;两种,根据之前的case,都是使用sentinel系统默认的提示:Blocked by Sentinel (flow limiting)。那我们能不能自定义兜底方法呢?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?
类似于@HystrixCommand, 引入@SentinelResource注解。
热点规则共有资源名、限流模式(只支持QPS模式)、参数索引、单机阈值、统计窗口时长、是否集群6种参数,还有一些高级选项,用到时会详细介绍。这里会用到注解中的value作为资源名,兜底方法会在后面详细介绍@SentinelResource注解详解
注意:
资源名:唯一路径,默认为请求路径。此处必须是 @SentinelResource 注解的 value 属性值,配置@GetMapping 的请求路径无效)
6.2.1 测试方法
还是在8401的controller中,加入热点测试方法。
@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";
}
注解@SentinelResource(value = "testHotKey", blockHandler = "del_testHotKey")
分析:
●其中 value = "testHotKey" 是一个标识(Sentinel资源名),与rest的/testHotKey
对应,这里value的值可以任意写,但是我们约定与rest地址一致,唯一区别是没有/
。
●blockHandler = "del_testHotKey" 则表示如果违背了Sentinel中配置的流控规则,就会调用我们自己的兜底方法del_testHotKey
6.2.2 配置热点key限流规则
绑定testHotKey资源,把testHotKey对应的第一个参数作为热点key进行监控。设定热点限流规则:当该资源的访问QPS超过1次/s的时候,产生限流并执行自定义的del_testHotKey兜底方法。
简而言之:方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理。
6.2.3 测试
1访问http://localhost:8401/testHotKey?p1=a&p2=b
1次/s正常显示,迅速点击两次,触发热点限流,执行自定义兜底方法:
2仅传入参数p2没有任何影响: http://localhost:8401/testHotKey?p2=b
3现在开两个访问,一个通过jmeter压测http://localhost:8401/testHotKey?p1=a&p2=b,另外再单独使用浏览器访问http://localhost:8401/testHotKey?p2=b,发现只带参数p2访问没有任何影响。
4不配置blockeHandler(兜底方法)
触发热点限流降级会出现error page,对用户不友好。
6.3 参数例外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流
特例情况:我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样,比如当p1的值等于5时,它的阈值可以达到200。
测试
狂点http://localhost:8401/testHotKey?p1=5&p2=b 没有限流
6.4 其他
手动添加一个异常:
测试直接错误页面。
要注意: Sentinel它只管你有没有触发它的限流规则,也可以说只管这个web交互页面(控制台)里面的东西。 配置类的东西Sentinel可以管,java异常的错误我不管。
@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
RuntimeException
int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管
总结:
@SentinelResource主管配置出错,运行出错该走异常走异常
七、系统规则(系统自适应限流)
官网
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
●Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
●CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
●平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
●并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
●入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
案例——配置全局QPS
不管是/testA还是/testB 只要QPS > 1 整个系统就不能用。
这个粒度太粗,就相当于一个窗口人很多,整个银行就不接待人了,不太建议使用。
八、@SentinelResource 注解详解
8.1 按资源名称限流+后续处理
启动nacos+sentinel
8.1.1 修改8401
(1) pom
引入我们自定义的公共api jar包
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><groupId>com.atguigu.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</version></dependency>
(2) 业务类
package com.cloudalibaba.sentinel.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource",blockHandler = "handleException") public CommonResult byResource() { return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001")); } public CommonResult handleException(BlockException exception) { return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用"); } }
8.1.2 配置流控规则——按资源名称添加流控规则
8.1.3 测试
自测:
触发流控规则:
8.1.4 问题
如果我们重启8401会发现之前配置的一些规则都没有了。难道每次重启服务器都要重新配置一遍规则吗?规则如何进行持久化?
8.2 按照Url地址限流+后续处理
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
8.2.1 修改controller
@GetMapping("/rateLimit/byUrl")@SentinelResource(value = "byUrl")public CommonResult byUrl(){return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));}
8.2.2 设置流控规则及测试
先自测,没有问题:
按rest URI设置流控规则
触发流控:
这个没有自定义的兜底的方法,返回Sentinel自带的限流处理结果。
8.3 总结以及面临的问题
不管是@GetMapping(rest url)还是 @SentinelResource,只要是唯一的,就可以作为流控规则的资源名称。如果没有自定义自己的兜底方法,那么就使用系统自带的。
问题:
●依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。如果都用系统默认的,就没有体现我们自己的业务要求。
●如果每个业务方法/API接口都添加一个兜底的,那代码膨胀加剧。
●全局统一的处理方法没有体现。
8.4 客户自定义限流处理逻辑
为了解决代码耦合与膨胀的问题
8.4.1 创建CustomerBlockHandler类用于自定义限流处理逻辑
在CustomerBlockHandler类中统一的处理限流提示、服务降级的说明等等。。
package com.cloudalibaba.sentinel.myHandler; import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.java.cloud.common.CommonResult; public class CustomerBlockHandler { public static CommonResult handleException(BlockException exception){ return new CommonResult(2020,"自定义的限流处理信息......CustomerBlockHandler------1"); } public static CommonResult handleException2(BlockException exception){ return new CommonResult(2020,"自定义的限流处理信息......CustomerBlockHandler------2"); } }
8.4.2 修改RateLimitController,使用自定义处理逻辑类
/** * 自定义通用的限流处理逻辑, blockHandlerClass = CustomerBlockHandler.class blockHandler = handleException2 上述配置:找CustomerBlockHandler类里的handleException2方法进行兜底处理 */
/** * 自定义通用的限流处理逻辑 */
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public CommonResult customerBlockHandler()
{ return new CommonResult(200,"按客户自定义限流处理逻辑");
}
8.4.3 测试
启动8401,先测试一次http://localhost:8401/rateLimit/customerBlockHandler
设置流控规则:
触发流控,看是否是我们自定义提示:
自定义提示出来了!
8.4.4 结构说明
这样就实现了兜底方法与业务方法的解耦。
8.5 更多属性说明
注解支持文档
九、服务熔断
主要内容:
●sentinel分别整合ribbon+openFeign以及设置fallback
●熔断框架比较
9.1 Ribbon系列
nacos中整合了Ribbon,所以直接使用nacos就行。启动nacos和Sentinel。
9.1.1 服务提供者9003/9004
新建cloudalibaba-provider-payment9003/9004两个一样的做法
(1) pom
2020版和springcloud和nacos记得引入spring-cloud-starter-loadbalancer依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud2020</artifactId> <groupId>org.xu.springcloud</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-provider-payment9003</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <groupId>org.xu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0.0</version> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
(2) yml
9004 别忘了改端口号
server:port: 9003spring:application:name: nacos-payment-providercloud:nacos:discovery:server-addr: localhost:8848 #配置Nacos地址management:endpoints:web:exposure:include: '*'
(3) 主启动类
package com.java.cloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 { public static void main(String[] args) { SpringApplication.run(PaymentMain9003.class,args); }
}
(4) 业务类
这里图方便,就没有连接数据库。
package com.java.cloud.controller; import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; @RestController
public class PaymentController { @Value("${server.port}") private String serverPort; public static HashMap<Long, Payment> hashMap = new HashMap<>(); static { hashMap.put(1L,new Payment(1L,"565123155216156315153131")); hashMap.put(2L,new Payment(2L,"565123155216156315153131")); hashMap.put(3L,new Payment(3L,"565123155216156315153131")); } public CommonResult<Payment> paymentSql(@PathVariable("id") Long id){ Payment payment = hashMap.get(id); CommonResult<Payment> commonResult = new CommonResult<>(200,"from mysql,serPort:"+serverPort,payment); return commonResult; } }
(5) 测试
http://localhost:9003/paymentSQL/1
http://localhost:9004/paymentSQL/1
9.1.2 服务消费者84
新建cloudalibaba-consumer-nacos-order84
(1) pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud2020</artifactId> <groupId>org.xu.springcloud</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-consumer-nacos-order84</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>org.xu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0.0</version> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
</project>
(2) yml
server: port: 84 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url: nacos-user-service: http://nacos-payment-provider
(3) 主启动类
package com.java.cloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain84.class,args); }
}
(4) 业务类
因为用的Ribbon,需要使用其提供的RestTemplate
package com.java.cloud.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate; @Configuration
public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
业务类:
package com.java.cloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @RestController
@Slf4j
public class CircleBreakerController { public static final String SERVECE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback") public CommonResult<Payment> fallback(@PathVariable Long id) throws IllegalAccessException { CommonResult forObject = restTemplate.getForObject(SERVECE_URL + "/paymentSql/"+id, CommonResult.class, id); if(id==4){ throw new IllegalAccessException("IllegalAccessException,非法参数异常"); }else if(forObject.getData()==null){ throw new NullPointerException("NullPointerException,空指针异常,没有获取到对应id的数据"); } return forObject; } }
(5) 测试
负载均衡实现。
9.1.3 fallback 和 blockHandler
加深@SentinelResource(value = "xxx", fallback = "fffff", blockHandler = "bbbbbb")注解理解:
fallback管运行异常,blockHandler管配置违规。
fallback对应服务降级,就是服务出错了应该怎么办(需要有个兜底方法);
blockHandler对应服务熔断,就是我现在服务不可用,我应该怎么办,怎么给客户一个户提示(同样需要一个兜底方法)
9.1.4 差异化配置
这里改的业务类代码均是消费者84端的业务类代码,不要搞错了。
降级是服务业务代码出现错误的兜底,熔断是服务不可用。降级是兜底方法,熔断是对服务保护时间窗口期服务不可用。
(1) 没有任何配置
前面我们的84消费端,@SentinelResource里面只配置了value,fallback和blockHandler都没有配置,该情况下我们测试一下http://localhost:84/consumer/fallback/4
出现了错误页面,error page对客户不友好,所以我们需要有兜底方法。
(2) 只配置fallback
fallback对应服务降级,就是服务可以正常访问,但是业务逻辑出现错误,需要降级兜底。
访问http://localhost:84/consumer/fallback/4,可以看到业务异常
(3) 只配置blockHandler
blockHandler对应服务熔断,当前sentinel配置已经违规(RT数过多、异常过多),服务熔断后不可用,需要给客户提示,进行一个熔断的兜底。
配置sentinel
访问:http://localhost:84/consumer/fallback/5
如果异常数达到规定的标准就会触发熔断,比如我们设置的异常数是2,那么请求两次以后就会报以下异常
这里还是跟之前一样的bug,很迷。
(4) fallback和blockHandler都配置
同时有降级跟熔断的兜底方法,当降级达到sentinel配置规则后,触发熔断。
配置sentinel
删除之前的熔断规则(这里不是删除,因为每次重启以后就没有了),配置流控:
没有触发限流时,我们触发业务异常http://localhost:84/consumer/fallback/4,会被降级方法fallback兜底:
触发限流时,我们仍然访问可以触发业务异常的连接,此时服务已经被限流(可以理解为服务不可用即熔断),此时触发的是限流(熔断blockHandler):
也就是说,同时配置fallback:处理业务异常(微服务自身异常,服务降级)和blockHandler:处理触发sentinel配置(微服务不可用,服务熔断)时。在没有违反sentinel规则时,出现业务异常(降级)走fallback方法;违反了sentinel规则时,直接微服务不可用(熔断),走blockHandler指定的自定义方法。
(5) 异常忽略属性
可以选择性的配置当某些异常发生时,不触发fallback的兜底方法。
测试一下,访问:http://localhost:84/consumer/fallback/4
直接报错误页面,没有了降级兜底方法。
其他异常不受影响:
当然,触发流控之后,仍然通过blockHandler指定的方法进行熔断兜底。
测试代码:
package com.java.cloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @RestController
@Slf4j
public class CircleBreakerController { public static final String SERVECE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}")
// @SentinelResource(value = "fallback",fallback = "handlerFallback")
// @SentinelResource(value = "fallback",blockHandler = "blockHandler") @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler", exceptionsToIgnore = {IllegalAccessException.class}) public CommonResult<Payment> fallback(@PathVariable Long id) throws IllegalAccessException { CommonResult forObject = restTemplate.getForObject(SERVECE_URL + "/paymentSql/"+id, CommonResult.class, id); if(id==4){ throw new IllegalAccessException("IllegalAccessException,非法参数异常"); }else if(forObject.getData()==null){ throw new NullPointerException("NullPointerException,空指针异常,没有获取到对应id的数据"); } return forObject; } public CommonResult handlerFallback(@PathVariable Long id,Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment); } public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) { Payment payment = new Payment(id,"null"); return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment); }
}
9.2 Feign系列
9.2.1 修改84模块
修改84模块,Feign组件一般是在消费侧。
(1) pom
pom 加入feign的依赖
<!--SpringCloud openfeign --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
(2) yml
激活Sentinel对Feign的支持
(3) 业务类
后续84的controller不找restTemplate(Ribbon),不是restTemplate去调用payment微服务中的接口。而是通过调用PaymentFeignService,service再去调用payment微服务中的端口。
Feign需要定义一个业务逻辑(service)接口+ @FeignClient注解以调用服务提供者。
新建PaymentFeignService interface:
package com.java.cloud.service; import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import com.java.cloud.service.impl.PaymentFallbackService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/** * @auther zzyy * @create 2019-12-10 17:17 * 使用 fallback 方式是无法获取异常信息的, * 如果想要获取异常信息,可以使用 fallbackFactory参数 */
@FeignClient(value="nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentFeignService { @GetMapping(value = "/paymentSql/{id}") public CommonResult<Payment> paymentSql(@PathVariable("id") Long id); }
调用失败的兜底方法:
package com.java.cloud.service.impl; import com.java.cloud.common.CommonResult;
import com.java.cloud.entities.Payment;
import com.java.cloud.service.PaymentFeignService;
import org.springframework.stereotype.Component; @Component
public class PaymentFallbackService implements PaymentFeignService { @Override public CommonResult<Payment> paymentSql(Long id) { return new CommonResult<>(444,"服务降级返回,没有该流水信息",new Payment(id, "errorSerial......")); }
}
84端口controller加入openFeign的接口:
@Resource
private PaymentFeignService paymentFeignService; @GetMapping("/consumer/openFeign/{id}")
public CommonResult<Payment> paymentSql(@PathVariable("id") Long id){ if(id == 4){ throw new RuntimeException("没有该id"); } return paymentFeignService.paymentSql(id);
}
(4) 主启动类
加上@EnableFeignClient注解开启OpenFeign
package com.java.cloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain84.class,args); }
}
(5) 测试
启动9003、9004、84
访问:http://localhost:84/consumer/paymentSQL/1
9003、9004负载均衡
关闭9003、9004微服务提供者,看到84消费者自动执行降级兜底方法。
如果yaml没有配置Sentinel对Feign的支持,就不会执行降级方法,而是直接报错误页面。
9.3 熔断框架比较
十、持久化规则
前面我们微服务新增的限流规则后,微服务关闭后就会丢失,当时配置都限流规则都是临时的。 将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则 就能看到。只要nacos里面的配置不删除,针对8401上的sentinel上的流控规则就持续存在。 (也可以持久化到文件,redis,数据库等)
案例——修改8401已完成持久化设置
(1) pom
导入持久化所需依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId></dependency>
(2) yaml
添加nacos数据源配置
server:port: 8401spring:application:name: cloudalibaba-sentinel-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址sentinel:transport:dashboard: localhost:8080 #配置Sentinel dashboard地址port: 8719datasource:ds1:nacos:server-addr: localhost:8848dataId: cloudalibaba-sentinel-servicegroupId: DEFAULT_GROUPdata-type: jsonrule-type: flowmanagement:endpoints:web:exposure:include: '*'feign:sentinel:enabled: true # 激活Sentinel对Feign的支持
(3) 添加nacos业务规则配置
我们将sentinel的流控配置保存在nacos中,因为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:是否集群。
注意:这里如果要配nacos的命名空间(public、dev、test)的话,应该是配namespace的id,不是名称
(4) 测试
启动8401,访问8401任意接口,刷新Sentinel。可以看到Sentinel中加载了通过nacos持久化的规则配置文件。
关掉8401后发现流控规则没有了。
再次启动8401查看sentinel,访问几次8401后流控规则又出现了。
查看数据库,发现规则持久化到数据库中了。
19. SpringCloud Alibaba Seata处理分布式事务
分布式事务问题
只要用到分布式,必然会提及分布式的事务。
在分布式之前,一切组件全都在一台机器上。
在使用分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。
业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
一、Seata简介与安装
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 官网
1.1 相关术语
一个典型的分布式事务过程,可以用分布式处理过程的一ID+三组件模型来描述。
一ID(全局唯一的事务ID):Transaction ID XID,在这个事务ID下的所有事务会被统一控制
三组件:
●Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;(Server端,为单独服务器部署)
●Transaction Manager (TM):事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
●Resource Manager (RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
●Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成(微服务)。
1.2 典型的分布式控制事务流程
1.TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
2.XID 在微服务调用链路的上下文中传播;(也就是在多个TM,RM中传播)
3.RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
4.TM 向 TC 发起针对 XID 的全局提交或回滚决议;
5.TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
1.3 Seata-Server的下载与配置
我这里下载了0.9.0版本跟1.4.2版本(配了半天没配好,后面再填坑),差别还是蛮大的。
github地址:https://github.com/seata/seata/releases
官网地址:Apache Seata
1.3.1 修改file.conf文件
解压到指定目录并修改conf目录下的file.conf配置文件。
1.备份原始file.conf文件。
* a 主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息。
2.修改file.conf文件
* a service模块(1.4.2里面没有这个模块,需要自己加)
* b store模块
注意:上面配置文件中的driver-class-name,如果你的mysql是8.0以后的版本,记得名称修改成:com.mysql.cj.jdbc.Driver
,同时url里面要配置上时区
1.3.2 数据库中建库建表
数据库新建库seata,建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
1.3.3 修改seata-server-0.9.0\seata\conf目录下的registry.conf配置文件
目的是:指明注册中心为nacos,及修改nacos连接信息
1.3.4 启动nacos和seata (先启动nacos)
seata-server-0.9.0\seata\bin\seata-server.bat
启动失败,报错:
解决:0.9.0默认的mysql是5.1.30版本,将lib文件夹下mysql-connector-java-5.1.30.jar删除,替换成自己mysql版本的jar包,我的是mysql-connector-java-8.0.22.jar。
再次启动:
出现这些提示信息代表seata启动成功。
nacos中成功注册了seate:
**Seata 1.4.2版本填坑---nacos作为seata的注册/配置中心
seata1.2.0 Seata1.4.0+nacos
Seata0.9.0版本不支持集群,生产环境下需要使用1.0.0以上版本。我们这里配置seata的最新版本1.4.2,下载后文件目录如下图所示:
0. 启动nacos
启动nacos,新建一个命名空间seata用于存放seata的配置信息。
注意这里的命名空间ID,后面会用到。这里不新建也可以,seata使用的是public。
我们使用nacos充当seata的注册中心和配置中心!
1. 修改配置文件
①进入conf文件夹,修改file.conf文件
1.4.2版本可以参考file.conf.example(server端)和conf文件夹下README-zh.md中的client端配置
总共需要修改的地方:我这里用seate_1_4_2数据库来对应seata1.4.2版本
②修改conf\registry.conf文件
思考:这里我们把seata-server端的config设置为了nacos,那么是不是第一步的file.conf文件就不再需要了。因为直接从nacos读取配置?
2. 将配置导入到nacos
① 准备nacos-config.sh脚本
在conf文件夹下,需要有个nacos-config.sh文件,这个文件1.4.2版本没有。README-zh.md文件中访问config-center超链接(https://github.com/seata/seata/tree/develop/script/config-center),nacos文件夹下:
用这个可以直接下当前页的文件github-directory-downloader
② config.txt准备及修改
在conf目录下还需要一个config.txt文件,1.4.2版本同样没有,还是去README里面的config-center超链接。
config.txt需要放在conf的上级目录下。
修改config.txt文件中的内容,主要是下面这几项:
改为使用db存储:
注意这里store.db.url中数据库的名字就是我们之后需要新建的数据库的名字。
相比于其他版本,1.4.2这里多了个distributedLockTable。
整个config.txt文件中,store.publicKey、store.redis.sentinel.masterName、store.redis.sentinel.sentinelHosts、store.redis.password四个属性默认都是空的
。所以后面在将config.txt文件中的配置注册到nacos的时候,会出现四个失败项。
③ 导入seata相应的配置项到Nacos
config.txt就是seata各种详细的配置,执行nacos-config.sh即可将这些配置导入到nacos。这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。(这句话是参考博客Seata 1.4.0 + nacos配置和使用,超详细_seata config.txt-CSDN博客,我觉不完全对,后面registry.conf里的配置项虽然不需要.conf文件配置,但是需要在yml或properties文件中配置,而file.conf可以直接在nacos中读取)
导入配置:
然后在git bash界面输入:
注:h表示nacos的地址,p表示端口号,g表示配置的分组,t表示命名空间的ID,u跟w表示nacos的账户密码。如果没有设置命名空间,而且都是默认选项直接 sh nacos-config.sh -h localhost就行。
可以看到共98项,导入失败4项,就是上面没有值的那四项(不影响,如果用到直接在nacos里面新建配置即可)
可以看到,nacos的seata命名空间中已经导入了配置项。(seata命名空间是我自己创建的,可以按自己的需求创建,不创建默认的就是public。)
3. 数据库中建库建表
我们先创建数据库seata1_4_2(数据库要与config.txt中db设置那里对应),数据库的建表语句在README文件的server连接中:
然后执行mysql.sql(1.4.2多了个distributed_lock表,和一些插入语句):
,这四张表跟config.txt文件中的配置对应。
4. 启动seata
运行bin目录下的seata-server.bat。
出现下面字段表示seata启动成功。seata启动日志在C:\Users\admin\logs\seata文件夹下。
nacos中在seata命名空间内也成功注册,注意这里服务名对应的是registry.conf文件中nacos下面application的值。0.9.0版本好像设置不了这个,默认是serverAddr。
二、订单/库存/账户业务数据库准备
以下演示都需要先启动Nacos后启动Seata,保证两个都OK。Seata没启动报错no available server to connect。
2.1 分布式事务业务说明
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单--->扣库存--->减账户(余额)
2.2 创建业务数据库与表
1. 创建业务数据库
●seata_order:存储订单的数据库;
●seata_storage:存储库存的数据库;
●seata_account:存储账户信息的数据库。
CREATE DATABASE seata_order;CREATE DATABASE seata_storage;CREATE DATABASE seata_account;
2. 按照上述3库分别创建对应业务表
seata_order库下建t_order表:
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=7 DEFAULT CHARSET=utf8;SELECT * FROM t_order;
seata_storage库下建t_storage 表:
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=2 DEFAULT CHARSET=utf8;INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0', '100');SELECT * FROM t_storage;
seata_account库下建t_account 表:
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 seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');SELECT * FROM t_account;
3. 按照上述3库分别建对应的回滚日志表
订单-库存-账户3个库下都需要建各自的回滚日志表,\seata-server-0.9.0\seata\conf目录下的db_undo_log.sql;
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
1.4.2版本的在README_ZH文件中的client:
注意:0.9版本跟1.4.2版本的undo_log表的属性有差别,1.4.2版本没有id跟ext这两个属性,个人觉得使用上没有影响
这里的话我还是按0.9.0版本来建表。
最终效果: 这里展示了1.4.2版本跟0.9版本,实际中用一个就行,别的版本一样。
三、订单/库存/账户业务微服务准备
业务需求:下订单->减库存->扣余额->改(订单)状态
版本对应关系——很重要
注意:由于seata0.9.0版本跟1.0之后的版本(支持yml、properties配置)区别巨大,这里使用0.9.0版本(跟视频一致),其版本对应关系见版本说明。(seata0.9.0 + nacos 1.1.4 + sentinel 1.7.0 + SpringCloud Alibaba 2.1.1RELEASE)前面用的各组件版本得对应上(头疼)
3.1 新建订单Order-Module——seata-order-service2001
新建seata-order-service2001
(1) pom
<dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
- Nacos 服务发现:用于服务注册和发现,使得微服务可以相互通信。
- Seata 分布式事务:用于实现分布式事务管理,确保多个微服务之间的数据一致性。
- Feign 客户端:用于简化 HTTP 客户端调用,实现声明式 REST 调用。
- Web 和 Actuator:用于构建 Web 应用程序,并提供了 Actuator 端点,用于监控和管理应用程序。
- MySQL 驱动:用于连接 MySQL 数据库。
- Druid 数据库连接池:用于优化数据库连接管理。
- MyBatis 集成:用于简化 MyBatis 的使用,实现 ORM(对象关系映射)。
- Spring Boot 测试:用于编写和运行单元测试。
- Lombok 插件:用于减少样板代码,如 getter、setter、toString 等。
(2) application.yml
这里配置的是我们自己微服务的数据源
server: port: 2001 # 指定服务运行的端口为2001 spring: application: name: seata-order-service # 指定应用程序的名称为seata-order-service cloud: alibaba: seata: tx-service-group: tx_group # 指定Seata事务分组名称 #自定义事务组名称需要与seata-server中的对应 nacos: discovery: server-addr: localhost:8848 # 指定Nacos服务发现地址 datasource: driver-class-name: com.mysql.cj.jdbc.Driver # 指定MySQL数据库驱动类 url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 # 指定MySQL数据库连接URL username: root # 指定MySQL数据库用户名 password: root # 指定MySQL数据库密码 feign: hystrix: enabled: false # 禁用Feign的Hystrix支持 logging: level: io: seata: info # 设置Seata的日志级别为info mybatis: mapper-locations: classpath:mapper/*.xml # 指定MyBatis的Mapper XML文件位置
(3) file.conf
程序中依赖的是 seata-all,对应于 *.conf 文件,所以需要在resource新建.conf文件,高版本的支持yml、properties配置。这里仅仅是seata-order-service2001模块的file.conf(配置2001的分布式事务),seata软件那里配置的是总控file.conf。
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none"
} service { vgroup_mapping.xu_group = "default" #修改自定义事务组名称 default.grouplist = "127.0.0.1:8091" enableDegrade = false disable = false max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" disableGlobalTransaction = false
} client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1
} ## transaction log store
store { ## store mode: file、db mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" #driver-class-name = "com.mysql.jdbc.Driver" driver-class-name = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8" user = "root" password = "root" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 }
}
lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server }
}
recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000
} transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log"
} ## metrics settings
metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898
} support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false }
}
注意修改这两处:
几个配置文件对应关系
(4) registry.conf
指明注册到nacos中:
registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848"namespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}
(5) domain
domain 就是entity(pojo,bean),对应数据库的表,不同公司习惯不一样。
新建Order类与CommonResult类
Order
package com.java.springcloud.domain; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data
@AllArgsConstructor
@NoArgsConstructor
public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; /** * 订单状态:0:创建中;1:已完结 */ private Integer status;
}
CommonResult
package com.java.springcloud.domain; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; @Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult<T> { private Integer code; private String message; private T data; /** * 这种构造函数重载的方式常用于简化对象的创建过程。 * 例如,在创建CommonResult对象时,如果不需要提供data参数, * 可以直接使用只接受code和message的构造函数,这样可以减少代码的复杂度和冗余。 * @param code * @param message */ public CommonResult(Integer code, String message){ this(code,message,null); } }
(6) Dao接口及实现(SQL映射文件)
dao中至少要有两个方法,一个是创建订单,一个是修改订单状态
OrderDao
package com.java.springcloud.dao; import com.java.springcloud.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; @Mapper
public interface OrderDao { /** * 创建订单 * @param order */ void create(Order order); /** * 修改订单状态 */ void update(@Param("userId")Long userId,@Param("status")Integer status); }
OrderMapper.xml
resources文件夹下新建mapper文件夹后添加OrderMapper.xml。完成dao的具体实现。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.java.springcloud.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.java.springcloud.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="count" property="count" jdbcType="INTEGER"/> <result column="money" property="money" jdbcType="DECIMAL"/> <result column="status" property="status" jdbcType="INTEGER"/> </resultMap> <insert id="create"> INSERT INTO t_order(`id`, `user_id`, `product_id`, `count`, `money`, `status`) VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0); </insert> <update id="update"> UPDATE t_order SET status = 1 WHERE user_id = #{userId} AND status = #{status}; </update>
</mapper>
(7) Service接口及实现
Order2001驱动自己,外加调用库存和账户:共3个service
OrderService
package com.java.springcloud.service; import com.java.springcloud.domain.Order;
import org.springframework.cloud.openfeign.EnableFeignClients; public interface OrderService { void create(Order order);
}
StorageService
package com.java.springcloud.service; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "seata-storage-service")
public interface StorageService { /** * 修改库存 */ @PostMapping("/storage/decrease") void decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
AccountService
package com.java.springcloud.service; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; @FeignClient(value="seata-account-service")
public interface AccountService { /** * 修改库存 * 必须使用RequestParam注解,不然启动就会报错 */ @PostMapping("/account/decrease") void decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money); }
OrderServiceImpl
package com.java.springcloud.service.impl; import com.java.springcloud.dao.OrderDao;
import com.java.springcloud.domain.Order;
import com.java.springcloud.service.AccountService;
import com.java.springcloud.service.OrderService;
import com.java.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service
@Slf4j
public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private AccountService accountService; @Resource private StorageService storageService; @Override public void create(Order order) { log.info("新建订单,用户id是{}",order.getUserId()); orderDao.create(order); //远程调用库存服务扣减库存 log.info("调用库存服务,扣减库存-开始"); storageService.decrease(order.getUserId(),order.getCount()); log.info("调用库存服务,扣减库存-结束"); //远程调用账户服务扣减余额 log.info("调用账户服务,扣减金额-开始"); accountService.decrease(order.getUserId(),order.getMoney()); //修改订单状态为已完成 log.info("调用账户服务,扣减金额-结束"); orderDao.update(order.getUserId(),0); log.info("新建订单结束,用户id是{}",order.getUserId()); }
}
(8) controller
package com.java.springcloud.controller; import com.java.springcloud.domain.CommonResult;
import com.java.springcloud.domain.Order;
import com.java.springcloud.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
public class OrderController
{ @Autowired private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order){ orderService.create(order); return new CommonResult(200,"订单创建成功"); } }
(9) config
MyBatisConfig
mybatis配置类,绑定实现文件OrderMapper.xml与Dao接口
package com.java.springcloud.config; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration; @Configuration
@MapperScan({"com.java.springcloud.dao"})
public class MyBatisConfig {
}
DataSourceProxyConfig
package com.java.springcloud.config; import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; @Configuration
public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
DataSouce的包是sql下的,DataSourceProxy是seata下的,不要搞错了。
(10) 主启动类
package com.java.springcloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class, args); }
}
启动测试
先启动nacos-1.1.4和seata-0.9.0,再启动2001。
2001启动成功,成功注册到nacos中
测试nacos-2.0.3和seata-0.9.0,再启动2001 也可以启动成功
3.2 新建库存Storage-Module——seata-storage-service2002
(1) pom
<dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
(2) application.yml
用到的数据库跟上面的是不一样的,我们就是为了测试分布式数据库的
server: port: 2002 spring: application: name: seata-storage-service cloud: alibaba: seata: tx-service-group: xu_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&useInformationSchema=false # 指定MySQL数据库连接URL username: root password: root logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
(3) file.conf & registry.conf
跟2001的一模一样
(4) domain
Storage
package com.java.springcloud.domain; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; @Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage
{ private Long id; /** * 产品id */ private Long productId; /** * 总库存 */ private Integer total; /** * 已用库存 */ private Integer used; /** * 剩余库存 */ private Integer residue; }
CommonResult
package com.java.springcloud.domain; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; @Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult<T> { private Integer code; private String message; private T data; /** * 这种构造函数重载的方式常用于简化对象的创建过程。 * 例如,在创建CommonResult对象时,如果不需要提供data参数, * 可以直接使用只接受code和message的构造函数,这样可以减少代码的复杂度和冗余。 * @param code * @param message */ public CommonResult(Integer code, String message){ this(code,message,null); } }
(5) Dao接口及实现(SQL映射文件)
StorageDao
package com.java.springcloud.dao; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; @Mapper
public interface StorageDao { /** * 扣减库存 */ void decrease(@Param("productId") Long productId, @Param("count") Integer count); }
StorageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.java.springcloud.dao.StorageDao"> <resultMap id="BaseResultMap" type="com.java.springcloud.domain.Storage"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="total" property="total" jdbcType="INTEGER"/> <result column="used" property="used" jdbcType="INTEGER"/> <result column="residue" property="residue" jdbcType="INTEGER"/> </resultMap> <update id="decrease"> UPDATE t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId} </update> </mapper>
(6) Service接口及实现
StorageService
package com.java.springcloud.service; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; public interface StorageService { /** * 修改库存 */ @PostMapping("/storage/decrease") void decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
StorageServiceImpl
package com.java.springcloud.service.impl; import com.java.springcloud.dao.StorageDao;
import com.java.springcloud.service.StorageService;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; @Service
public class StorageServiceImpl implements StorageService { @Autowired private StorageDao storageDao; @Override public void decrease(@Param("productId") Long productId, @Param("count") Integer count) { storageDao.decrease(productId,count); }
}
(7) Controller
package com.java.springcloud.controller; import com.java.springcloud.domain.CommonResult;
import com.java.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; @RestController
public class StorageController { @Autowired private StorageService storageService; @RequestMapping("/storage/decrease") public CommonResult decrease(Long productId, Integer count) { storageService.decrease(productId, count); return new CommonResult(200,"扣减库存成功!"); } }
(8) config配置
与2001的一模一样
package com.java.springcloud.config; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration; @Configuration
@MapperScan({"com.java.springcloud.dao"})
public class MyBatisConfig {
}
package com.java.springcloud.config; import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; @Configuration
public class DataSourceProxyConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
(9) 主启动类
package com.java.springcloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; @EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SeataStorageServiceApplication2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageServiceApplication2002.class, args); }
}
启动测试
启动nacos、seata、2002;启动成功,注册进nacos。
3.3 新建账户Account-Module——seata-account-service2003
(1) pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud2020</artifactId> <groupId>org.xu.springcloud</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-account-service2003</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <artifactId>seata-all</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
</project>
(2) application.yml
server: port: 2003 spring: application: name: seata-account-service cloud: alibaba: seata: tx-service-group: xu_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&useInformationSchema=false # 指定MySQL数据库连接URL username: root password: root feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
(3) file.conf & registry.conf
跟2001一模一样
(4) domain
Account
package com.java.springcloud.domain; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data
@NoArgsConstructor
@AllArgsConstructor
public class Account { private Long id; /** * 用户id */ private Long userId; /** * 总额度 */ private BigDecimal total; /** * 已用额度 */ private BigDecimal used; /** * 剩余额度 */ private BigDecimal residue; }
CommonResult
(5) Dao接口及实现
AccountDao
package com.java.springcloud.dao; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; @Mapper
public interface AccountDao { void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.java.springcloud.dao.AccountDao"> <resultMap id="BaseResultMap" type="com.java.springcloud.domain.Account"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="total" property="total" jdbcType="DECIMAL"/> <result column="used" property="used" jdbcType="DECIMAL"/> <result column="residue" property="residue" jdbcType="DECIMAL"/> </resultMap> <update id="decrease"> UPDATE t_account SET residue = residue - #{money},used = used + #{money} WHERE user_id = #{userId}; </update> </mapper>
(6) Service接口及实现
AccountService
package com.java.springcloud.service; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; public interface AccountService { void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
AccountServiceImpl
package com.java.springcloud.service.impl; import com.java.springcloud.dao.AccountDao;
import com.java.springcloud.service.AccountService;
import org.springframework.stereotype.Service; import javax.annotation.Resource;
import java.math.BigDecimal; @Service
public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Override public void decrease(Long userId, BigDecimal money) { accountDao.decrease(userId,money); }
}
(7) Controller
package com.java.springcloud.controller; import com.java.springcloud.domain.CommonResult;
import com.java.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit; @RestController
public class AccountController { @Resource private AccountService accountService; @PostMapping("/account/decrease") public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) { try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } accountService.decrease(userId, money); return new CommonResult(200, "扣减账户余额成功!"); } }
(8) config配置
和2001一模一样
(9) 主启动类
package com.java.springcloud; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SeataAccountMainApp2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMainApp2003.class,args); }
}
启动测试
启动nacos、seata、2003;启动成功,成功注册进nacos
填坑高版本seata1.4.2client配置
seata1.2.0 Seata1.4.0+nacos
使用seata1.4.2 各个微服务整体上的代码是差不多的,区别的地方在于1.4.2支持yml、properties文件里配置client端的seata,不再需要file.conf/registry.conf文件。同时支持@EnableAutoDataSourceProxy注解开启数据源的自动代理(不需要手动配置数据源)
① 修改父工程版本控制
seata高版本各微服务可以直接通过yml、properties来配置seata,不需要在微服务中加入file.conf和registry.conf文件。
注意版本对应关系,这里要使用SpringCloud Hoxton.SR9+SpringCloud Alibaba 2.2.6.RELEASE+Spring Boot 2.3.2RELEASE+Nacos 1.4.2(我用的2.0.3)+Seata 1.3.0(我用的1.4.2)。
修改父工程的依赖版本,主要是让springboot、SpringCloud、SpringCloud alibaba版本对应。
② 修改微服务模块seata依赖pom
官网上对seata依赖是这么描述的:
官网推荐依赖配置方式:
但是经过我测试发现,还是需要排除掉spring-cloud-starter-alibaba-seata里面的seata-all
实际上我有微服务只配置了seata-spring-boot-starter依赖,spring-cloud-starter-alibaba-seata没有配置,并不影响正常使用。
③ 修改微服务application.yml
直接将seata的相关配置,配置到application.yml文件中,几个微服务的yml类似:
注意对应关系:
④ 修改主启动类和DataSourceProxyConfig类
主启动类
主启动类加上@EnableAutoDataSourceProxy注解,这里以storage的微服务为例:
配置类
使用@EnableAutoDataSourceProxy注解后,不再需要DataSourceProxyConfig配置数据源代理。强行写会报错。如果需要自己配置数据源代理的话,在application.yml中设置seata.enable-auto-data-source-proxy为false,主启动类上去掉@EnableAutoDataSourceProxy注解即可。
参考博客
1Seata1.4.2+Nacos搭建使用
2Seata1.4.2整合SpringCloud H——Seata安装与搭建
3最详细实际项目中seata踩坑、部署教程以及多链路调用_seata lock.retrytimes-CSDN博客
4spring cloud使用nacos和seata(windows环境)
5SEATA配合nacos使用
四、测试
Seata全局事务怎么使用
Spring提供的本地事务:@Transactional
Seata提供的全局事务:@GlobalTransactional
4.0 数据库初始情况
下订单->减库存->扣余额->改(订单)状态
4.1 测试正常下单
启动nacos、seata、2001、2002、2003;
测试:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
报错
java.sql.SQLException:Failed to fetch schema of t_order
Connector/J 5.0.0以后的版本有一个名为useInformationSchema的数据库连接参数,Connector/J 在mysql8.0中默认配置连接属性useInformationSchema为true,使查询table信息时更为有效。用户依然可以配置useInformationSchema为false,但是在8.0.3及其之后的版本中,由于不能支持早期的特性,某些数据字典的查询可能会失败。
useInformationSchema配置为false的时候,也可能会造成REMARKS信息(对应数据库中各字段的comment)的缺失。
在各微服务的application.yml 文件的spring.datasource.url 后面加上&useInformationSchema=false设置useInformationSchema为false,即可解决该问题。 参考:jdbc中useInformationSchema属性分析 - 简书
再次测试
访问成功
数据库情况:
4.2 测试超时异常:不加@GlobalTransactional
AccountServiceImpl添加超时:
我们使用的是Openfeign,默认超时时长是1s,这里我们延迟30s。
报错超时异常:
数据库情况:
当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1;而且由于feign的重试机制,账户余额还有可能被多次扣减。
4.3 测试超时异常:加@GlobalTransactional
OrderServiceImpl添加@GlobalTransactional注解,注意改注解只能用在方法上!
这个注解加在哪个业务类上呢? 谁是最开始的发起者就加在谁上面
●name:给定全局事务实例的名称,随便取,唯一即可
●rollbackFor:当发生什么样的异常时,进行回滚
●noRollbackFor:发生什么样的异常不进行回滚。
测试:
依然超时异常 ,
重点是数据库有没有发生变化:
我们发现数据库中的数据根本就没有变化,记录都添加不进来,说明回滚成功!
4.4 小结
做好配置后,我们只需要使用一个 @GlobalTransactional(name = "lsp-create-order", rollbackFor = Exception.class) 放在业务的入口,即可实现控制全局的事务。注意该注解只能放在方法上。
五、补充说明
Seata:Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
0.9不支持集群,生产环境请使用1.0以上的版本。
5.0 undo_log表的作用
模块内方法也可以加@Transactional注解,如果一个模块的事务提交了,Seata会把提交了哪些数据记录到undo_log表中,如果这时TC通知全局事务回滚,那么RM就从undo_log表中获取之前修改了哪些资源,并根据这个表回滚。(有待考证)
5.1 再看TC/TM/RM三大组件
TC:seata服务器; (我们电脑上启动的seata )
TM:事物的发起者,业务的入口。 哪个微服务使用了@GlobalTransactional哪个就是TM
RM:事务的参与者,一个数据库就是一个RM。
分布式事务的执行流程:
1TM 开启分布式事务(TM 向 TC 注册全局事务记录);
2按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
3TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
4TC 汇总事务信息,决定分布式事务是提交还是回滚;
5TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。
5.2 AT模式(默认)如何做到对业务的无侵入
Seata有四大模式:AT(默认)、TCC、SAGA、XA。(阿里云上的AT叫做GTS,收费)
AT模式
AT模式两阶段提交协议的演变:
●一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
●二阶段:
○提交异步化,非常快速地完成。
○回滚通过一阶段的回滚日志进行反向补偿(前面insert,后面回滚时就delete)。
每个数据库除了自身存储数据的表以外,都会有一个事务回滚表:undo_log
Seata库中存在:branch_table\global_table\lock_table\distributed_lock(高版本才有)这样一些表
5.2.1 一阶段加载
在一阶段,Seata 会拦截“业务 SQL”,
1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”(前置镜像)
2 执行“业务 SQL”更新业务数据,在业务数据更新之后,
3 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
5.2.2 二阶段提交
因为“业务 SQL”在一阶段已经提交至数据库,二阶段如果顺利提交的话,那么Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
5.2.3 二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”。如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
5.3 debug查看流程
最开是seata库中的三张表是没有数据的
2003打上断点,debug启动
访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100。
此时seata库中的三个表都是有数据的:
看一下branch_table,记录了各个RM的信息,分别对应order、storage、account三个微服务
可以看到xid跟global_table中的xid一致。
再看global_table:
查看lock_table:
rollback_info是JSON字符串,存储了beforeimage、afterimage:
查看seata_storage库中的undo_log表的roobal_info信息,可以看到beforeimage和afterimage分别保存了修改前后的信息。
debug放行,seata库中表中的中间数据和undo_log表的数据都删除了。(我的seata_account表的undo_log中没有被删除,等了半天也没有。)异步任务阶段的分支提交请求将异步和批量地删除相应的undo_log记录。
发现account2003微服务的日志跟2001和2002都不一样