1、SpringCloud Alibaba简介
Spring Cloud Netflix项目进入维护模式,不再更新开发新组件了
Dubbo 也不再维护和更新
需要替代方案,Spring Cloud Alibaba 应用而生
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
此外,阿里云同时还提供了 Spring Cloud Alibaba 企业版 微服务解决方案,包括无侵入服务治理(全链路灰度,无损上下线,离群实例摘除等),企业级 Nacos 注册配置中心和企业级云原生网关等众多产品。
1.1 主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成对应 Spring Cloud 版本所支持的负载均衡组件的适配。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
几乎可以将之前的Spring Cloud代替
除了上述所具有的功能外,针对企业级用户的场景,Spring Cloud Alibaba 配套的企业版微服务治理方案 微服务引擎MSE 还提供了企业级微服务治理中心,包括全链路灰度、服务预热、无损上下线和离群实例摘除等更多更强大的治理能力,同时还提供了企业级 Nacos 注册配置中心,企业级云原生网关等多种产品及解决方案。
1.2 具体组件
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
2、SpringCloud Alibaba Nacos服务注册和配置中心
2.1 Nacos介绍
为什么叫Nacos:前四个字母分别为naming和Configuration的前两个字母,最后的s为service ;一个更易于构建云原生应用的动态服务发现,配置管理和服务管理平台。
==Nacos:Dynamic Naming and Configuration Service ==
服务注册和配置中心的组合
== Nacos=erueka+config+bus==
- 替代Eureka做服务注册中心
- 替代Config做服务配置中心
Nacos官网地址:https://nacos.io/zh-cn/
各种注册中心比较 :
Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。
2.2 Nacos下载安装
Github下载地址:https://github.com/alibaba/nacos/releases/tag/2.2.2
安装Nacos:
本地java8+maven环境已经ok
1. 到github上下载安装包
解压安装包
2. 启动Nacos
解压安装包,直接运行bin目录下的startup.cmd
3. 访问Nacos
命令运行成功后直接访问http://localhost:8848/nacos/index.html
账号密码:默认都是nacos
结果页面:
2.3 使用Nacos作为注册中心
2.3.1 在父工程的pom文件中引入springcloudalibaba依赖
<!-- spring cloud alibaba 2.1.0.RELEASE --> <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>
2.3.2 创建cloudalibaba-provider-payment9001模块
- 新建cloudalibaba-provider-payment9001模块
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-provider-payment9001</artifactId> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 management: endpoints: web: exposure: include: "*"
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author tigerhhzz * @date 2023/4/18 19:49 */ @Slf4j @SpringBootApplication @EnableDiscoveryClient public class NacosProviderMain9001 { public static void main(String[] args) { SpringApplication.run(NacosProviderMain9001.class,args); log.info("NacosProviderMain9001启动成功~~~~~~~~~~~~~"); } }
- 业务类controller:
package com.tigerhhzz.springcloud.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author tigerhhzz * @date 2023/4/18 19:50 */ @RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") public String serverPort; @RequestMapping("/getPayment/{id}") public String getPayment(@PathVariable("id") Integer id){ return "Alibaba Nacos server "+ serverPort+"-----"+id; } }
- 测试
启动cloudalibaba-provider-payment9001
然后查看Nacos的web界面,可以看到nacos-payment-provider已经注册成功
2.3.3 创建cloudalibaba-provider-payment9002模块
创建过程雷同9001模块。
2.3.4 创建cloudalibaba-consumer-nacos-order83模块
- 新建cloudalibaba-consumer-nacos-order83模块
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-consumer-nacos-order83</artifactId> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
server: port: 83 spring: application: name: cloud-nacos-order cloud: nacos: discovery: server-addr: localhost:8848 #消费者将要去访问的微服务名称 server-url: nacos-user-service: http://nacos-payment-provider
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author tigerhhzz * @date 2023/4/18 20:24 */ @Slf4j @SpringBootApplication @EnableDiscoveryClient public class NacosOrderMain83 { public static void main(String[] args) { SpringApplication.run(NacosOrderMain83.class,args); log.info("NacosOrderMain83启动成功~~~~~~~~~~~~~"); } }
- 业务类controller:
package com.tigerhhzz.springcloud.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; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * @author tigerhhzz * @date 2023/4/18 20:30 */ @RestController public class OrderNacosController { @Resource private RestTemplate restTemplate; @Value("${server-url.nacos-user-service}") private String url; @GetMapping("/order/getPayment/{id}") public String getPaymentInfo(@PathVariable("id") Long id) { return restTemplate.getForObject(url+"/payment/getPayment/"+id,String.class); } }
- 编写配置类
因为Naocs要使用Ribbon进行负载均衡,那么就需要使用RestTemplate
package com.tigerhhzz.springcloud.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; /** * @author tigerhhzz * @date 2023/4/18 20:29 */ @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //负载均衡:轮询 public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
- 测试
启动cloudalibaba-consumer-nacos-order83模块
启动cloudalibaba-provider-payment9001模块
启动cloudalibaba-provider-payment9002模块
访问: http://localhost:83/order/getPayment/11
可以看到,实现了负载均衡
Nacos天生就自带netflix-ribbon负载均衡功能,因为它整合了netflix-ribbon依赖:
2.3.5 Nacos与其他服务注册的对比
Nacos它既可以支持CP,也可以支持AP,可以切换
下面这个curl命令,就是切换模式, 在CP与AP之间切换
2.4 使用Nacos作为配置中心
2.4.1创建cloudalibaba-config-nacos-client-3377模块(配置中心的客户端模块)
- 新建cloudalibaba-config-nacos-client-3377模块
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-config-nacos-client-3377</artifactId> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
这里需要配置两个配置文件,application.yml和bootstarp.yml
主要是为了可以与spring clodu config无缝迁移
bootstarp.yml
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: yml #指定yaml格式的配置 #namespace: 7a901d46-e75e-4e6a-b186-5980cca4249b group: TEST_GROUP #TEST_GROUP DEV_GROUP # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} #nacos-config-client-dev.yaml
application.yml
spring: profiles: #active: dev #开发环境 #active: test #测试环境 active: INFO #测试环境
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author tigerhhzz * @date 2023/4/18 21:03 */ @Slf4j @SpringBootApplication @EnableDiscoveryClient public class ConfigNacosMain3377 { public static void main(String[] args) { SpringApplication.run(ConfigNacosMain3377.class,args); log.info("ConfigNacosMain3377启动成功~~~~~~~~~~~~~"); } }
- 业务类controller
package com.tigerhhzz.springcloud.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; /** * @author tigerhhzz * @date 2023/4/18 21:04 */ @RestController @RefreshScope //支持nacos的动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } }
通过springloud的原生注解@RefreshScope,实现了配置自动刷新。
- 在nacos中添加配置信息
Nacos的配置规则:
理论:Nacos中的dataid的组成格式及与SpringBoot配置文件中的配置规则。
https://nacos.io/zh-cn/docs/what-is-nacos.html
配置规则,就是我们在客户端如何指定读取配置文件,配置文件的命名的规则
默认的命名方式:
prefix: 默认就是当前服务的服务名称 也可以通过spring.cloud.necos.config.prefix配置 spring.profile.active: 就是我们在application.yml中指定的,当前是开发环境还是测试等环境 这个可以不配置,如果不配置,那么前面的-也会没有 file-extension 就是当前文件的格式(后缀),目前只支持yml和properties
注意,DataId就是配置文件名字:
名字一定要按照上面的规则命名,否则客户端会读取不到配置文件
- 测试
重启cloudalibaba-config-nacos-client-3377客户端
调用接口查看配置信息 http://localhost:3377/config/info
拿到了配置文件中的值
2.4.2 Nacos默认就开启了自动刷新
此时我们修改了配置文件
客户端是可以立即更新的
因为Nacos支持Bus总线,会自动发送命令更新所有客户端
2.4.3 Nacos配置中心之分类配置:
2.4.3.1 分类配置简介
Namespace+Group+Data ID三者的关系?为什么这么设计?
NameSpace默认有一个:public名称空间
这三个类似java的: 包名 + 类名 + 方法名
2.4.3.2 DataId配置方案
通过配置文件,实现多环境的读取:
2.4.3.3 GroupID配置方案
直接在新建配置文件时指定组
在客户端配置,使用指定组的配置文件:
2.4.3.4 namespace配置方案
客户端配置使用不同名称空间:
要通过命名空间id指定
修改配置文件:
bootstrap.yml
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: yml #指定yaml格式的配置 namespace: dc386f3a-1bda-4b43-8753-d3c2e4b3a187 #group: TEST_GROUP #TEST_GROUP DEV_GROUP # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} #nacos-config-client-dev.yaml
application.yml
spring: profiles: #active: dev #开发环境 active: test #测试环境 #active: INFO #测试环境
重启服务,OK,测试
2.5 Nacos集群和持久化配置:
Nacos支持三种部署模式
- 单机模式 - 用于测试和单机试用。
- 集群模式 - 用于生产环境,确保高可用。
- 多集群模式 - 用于多数据中心场景。
2.5.1 单机版,切换mysql数据库
Nacos默认有自带嵌入式数据库,derby,但是如果做集群模式的话,就不能使用自己的数据库
不然每个节点一个数据库,那么数据就不统一了,需要使用外部的mysql
将nacos切换到使用我们自己的mysql数据库
在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
- nacos默认自带了一个sql文件,在nacos安装目录下;初始化mysql数据库,
数据库初始化文件:mysql-schema.sql - 修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql db.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,nacos所有写嵌入式数据库的数据都写到了mysql
单机模式启动nacos的方法请参考https://tigerhhzz.blog.csdn.net/article/details/130308472
增加一个配置:
配置信息存入到mysql数据库中:
2.5.2 Linux上配置Nacos集群+Mysql数据库
官方架构图:
预计需要,1个Nginx+3nacos注册中心+1个mysql
2.5.2.1 Nacos下载Linux版
下载安装Nacos的Linux版安装包
下载地址:https://github.com/alibaba/nacos/releases/tag/2.2.2
2.5.2.2 集群配置步骤(重点)
- 进入安装目录,现在执行自带的sql文件
进入mysql,执行sql文件
- 修改配置文件,切换为我们的mysql
就是上面windos版要修改的几个属性
- 修改cluster.conf,指定哪几个节点是Nacos集群
这里使用3333,4444,5555作为三个Nacos节点监听的端口
- 我们这里就不配置在不同节点上了,就放在一个节点上
既然要在一个节点上启动不同Nacos实例,就要修改startup.sh,使其根据不同端口启动不同Nacos实例
可以看到,这个脚本就是通过jvm启动nacos
所以我们最后修改的就是,nohup java -Dserver.port=3344
- 配置Nginx:
- 启动Nacos:
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
- 启动nginx
- 测试:
访问192.168.159.121:1111
如果可以进入nacos的web界面,就证明安装成功了
- 将微服务注册到Nacos集群:
- 进入Nacos的web界面
可以看到,已经注册成功
3、SpringCloud Alibaba Sentinel实现熔断与限流
官网链接:https://sentinelguard.io/zh-cn/docs/introduction.html
3.1 什么是Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量
为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即
突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用
应用等。 - 完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒
级数据, 甚至 500 台以下规模的集群的汇总运行情况。 - 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 Spring
Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入
Sentinel。 - 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快
速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /
Spring Cloud 等框架也有较好的支持。 - 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等
应用容器。
实现熔断与限流,就是阿里版的Hystrix(豪猪哥)
- 服务限流
- 服务熔断
- 服务降级
- 服务血崩
3.2 安装Sentinel控制台
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
1 下载jar包,解压到文件夹
下载地址:https://github.com/alibaba/Sentinel/releases/tag/1.8.6
- 运行sentinel
由于是一个jar包,所以可以直接java -jar运行
注意,默认sentinel占用8080端口
java -jar sentinel-dashboard-1.8.6.jar
- 访问sentinel,账号和密码均为sentinel
补充:了解控制台的使用原理
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,
即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口
调用微服务中的监控程序获取微服务的各种信息.
3.2 微服务整合sentinel
3.2.1 启动Nacos 8848
3.2.2 新建一个cloudalibaba-sentinel-service8401模块
主要用于配置sentinel:
8401服务超过注册到nacos8848,并且被sentinel8080保护监护着,可以进行服务熔断、降级和限流的操作。
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-sentinel-service8401</artifactId> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 port: 8719 #如果8719被占用,自动递增1,直到找到没有被占用的端口 management: endpoints: web: exposure: include: "*"
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author tigerhhzz * @date 2023/4/20 11:00 */ @Slf4j @SpringBootApplication @EnableDiscoveryClient public class SentinelMain8401 { public static void main(String[] args) { SpringApplication.run(SentinelMain8401.class,args); log.info("SentinelMain8401启动成功~~~~~~~~~~~~~"); } }
- 业务类controller
package com.tigerhhzz.springcloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.tls.TlsException; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; /** * @author tigerhhzz * @date 2023/4/20 11:05 */ @RestController @Slf4j public class FlowLimitController { @RequestMapping("testA") public String testA(){ return "testA-------"; } @RequestMapping("testB") public String testB(){ return "testB-------"; } /*线程数流控测试接口*/ @RequestMapping("testC") public String testC(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testC 测试RT---"); return "testC 测试RT-------"; } @RequestMapping("testD") public String testD(){ System.out.println("testD 异常比例测试"); int age = 10/0; return "testD--异常比例测试\"-----"; } @RequestMapping("testE") public String testE(){ System.out.println("testD 异常数"); int age = 10/0; return "testD 异常数-----"; } @RequestMapping("testHotKey") @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKet") public String testHotKey( @RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false) String p2 ){ System.out.println("testHotKey 热点Key--测试"); //int age = 10/0; return "testHotKey-------"; } /*兜底的自定义方法*/ public String deal_testHotKet(String p1, String p2, BlockException e) { return "----deal_testHotKet,------"; } }
- 启动Sentinel8080,启动8401微服务
此时我们到sentinel中查看,发现并8401的任何信息
是因为,sentinel是懒加载,需要我们执行一次访问,才会有信息
访问localhost/8401/testA
8080Sentinel正在监视8401微服务。
3.2.3 Sentinel的概念和功能
3.2.3.1 基本概念
- 资源
资源就是Sentinel要保护的东西
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是
一个方法,甚至可以是一段代码。
我们入门案例中的message1方法就可以认为是一个资源 - 规则
规则就是用来定义如何进行保护资源的
作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统
保护规则。
我们入门案例中就是为message1资源设置了一种流控规则, 限制了进入message1的流量.
3.2.3.2 重要功能
Sentinel的主要功能就是容错,主要体现为下面这三个:
- 流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是
随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。 - 熔断降级
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则
对这个资源的调用进行限制,让请求快速失败,避影响到其它的资源而导致级联故障。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源
出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆
积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的
线程完成任务后才开始继续接收请求。 - 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。
当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的
时间窗口之后才重新恢复。
Sentinel 和 Hystrix 的区别 两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务 但是在限制的手段上, 确采取了完全不一样的方法: Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程 切换的成本。 Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
- 系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让
请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其
它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保
护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请
求。
总之一句话: 我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功
能。
3.2.4 sentinel的流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时
对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
第1步: 点击簇点链路,我们就可以看到访问过的接口地址,然后点击对应的流控按钮,进入流控规则配
置页面。新增流控规则界面如下:
**资源名:**唯一名称,默认是请求路径,可自定义
针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流
是否集群:暂不需要集群
接下来我们以QPS为例来研究限流规则的配置
3.2.4.1 简单配置
流控模式
我们先做一个简单配置,设置阈值类型为QPS,单机阈值为1。即每秒请求量大于1的时候开始限流。
接下来,在流控规则页面就可以看到这个配置。
然后快速访问 http://localhost:8401/testA 接口,观察效果。此时发现,当QPS > 1的时候,服务就不能正常响
应,而是返回Blocked by Sentinel (flow limiting)结果。
- QRS每秒请求次数
直接快速失败
直接失败的效果:
每秒请求次数超过1次,就进行流量控制。
- 线程数:
比如a请求过来,处理很慢,在一直处理,此时b请求又过来了 此时因为a占用一个线程,此时要处理b请求就只有额外开启一个线程 那么就会报错
3.2.4.2 配置流控模式
点击上面设置流控规则的编辑按钮,然后在编辑页面点击高级选项,会看到有流控模式一栏。
sentinel共有三种流控模式,分别是:
- 直接(默认):接口达到限流条件时,开启限流
- 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
- 链路:当从某个接口过来的资源达到限流条件时,开启限流
下面呢分别演示三种模式:
- 直接流控模式
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流。上面案例使用的就是直接流控
模式。 - 关联流控模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。
当关联的资源达到阈值时,就限流自己。
应用场景: 比如支付接口达到阈值,就要限流下订单的接口,防止一直有订单
当testA达到阈值,qps大于1,就让testB之后的请求直接失败
可以使用postman压测
访问testB成功:
postman里新建多线程集合组
访问地址添加进新线程组(20个线程每次间隔0.3秒访问一次)
大批量线程高并发访问B,导致A失效了
- 链路流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对
来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度
更细。
3.2.4.3 配置流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的
1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。 - 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设
置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
- 预热Warm up:
- 排队等待
3.2.5 sentinel的降级规则:
就是熔断降级
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
- 平均响应时间(秒级) :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。
如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口
(以 s 为单位)之内,就会对这个方法进行服务降级。
==注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要
变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。 ==
- 异常比例(秒级):当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的
时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0,
1.0]。 - 异常数(分钟级) :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分
钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。
问题:
流控规则和降级规则返回的异常页面是一样的,我们怎么来区分到底是什么原因导致的呢?
3.2.5.1 RT配置:
新增一个请求方法用于测试
/*线程数流控测试接口*/ @RequestMapping("testC") public String testC(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testC 测试RT---"); return "testC 测试RT-------"; }
配置RT:
这里配置的PT,默认是秒级的平均响应时间
默认计算平均时间是: 1秒类进入5个请求,并且响应的平均值超过阈值(这里的200ms),就报错]
1秒5请求是Sentinel默认设置的
测试
默认熔断后.就直接抛出异常
3.2.5.2 异常比例:
修改请求方法
@RequestMapping("testD") public String testD(){ System.out.println("testD 异常比例测试"); int age = 10/0; return "testD--异常比例测试\"-----"; }
配置:
如果没触发熔断,这正常抛出异常:
触发熔断:
3.2.5.3 异常数:
@RequestMapping("testE") public String testE(){ System.out.println("testD 异常数"); int age = 10/0; return "testD 异常数-----"; }
一分钟之内,有5个请求发送异常,进入熔断
3.2.6 sentinel的热点规则:
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
3.2.6.1 热点规则简单使用
@RequestMapping("testHotKey") @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKet") public String testHotKey( @RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false) String p2 ){ System.out.println("testHotKey 热点Key--测试"); //int age = 10/0; return "testHotKey-------"; } /*兜底的自定义方法*/ public String deal_testHotKet(String p1, String p2, BlockException e) { return "----deal_testHotKet,------"; }
比如:
localhost:8080/aa?name=aa
localhost:8080/aa?name=b’b
加入两个请求中,带有参数aa的请求访问频次非常高,我们就现在name==aa的请求,但是bb的不限制
如何自定义降级方法,而不是默认的抛出异常?
使用@SentinelResource直接实现降级方法,它等同Hystrix的@HystrixCommand
定义热点规则:
此时我们访问/testHotkey并且带上才是p1
如果qps大于1,就会触发我们定义的降级方法
但是我们的参数是P2,就没有问题
只有带了p1,才可能会触发热点限流
3.2.6.2 设置热点规则中的其他选项:
需求:
![[外链图片转存失败,源站可能有防盗链机制,建议
测试
注意:
参数类型只支持,8种基本类型+String类
注意:
如果我们程序出现异常,是不会走blockHander的降级方法的,因为这个方法只配置了热点规则,没有配置限流规则
我们这里配置的降级方法是sentinel针对热点规则配置的
只有触发热点规则才会降级
3.2.7 sentinel的系统规则:
系统自适应限流:
从整体维度对应用入口进行限流
对整体限流,比如设置qps到达100,这里限流会限制整个系统不可以
测试:
3.2.7.1 @SentinelResource注解:
用于配置降级等功能
@SentinelResource 注解
注意:注解方式埋点不支持 private 方法。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
value:资源名称,必需项(不能为空)
entryType:entry 类型,可选项(默认为 EntryType.OUT)
blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
返回值类型必须与原函数返回值类型一致;
方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
返回值类型必须与原函数返回值类型一致;
方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
1,环境搭建
- 为8401添加依赖
添加我们自己的commone包的依赖
<dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 额外创建一个controller类RateLimitController
package com.tigerhhzz.springcloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import com.tigerhhzz.springcloud.myhandler.Customerhandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author tigerhhzz * @date 2023/4/22 14:50 */ @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服务不可用"); } @GetMapping("/rateLimit/byUrl") @SentinelResource(value = "byUrl") public CommonResult byUrl() { return new CommonResult(200,"按URL限流测试OK",new Payment(2020L,"serial002")); } @GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler",blockHandlerClass = Customerhandler.class,blockHandler = "handlerException2") public CommonResult customerBlockHandler() { return new CommonResult(200,"按客户自定义OK",new Payment(2020L,"serial002")); } }
- 配置限流
注意,我们这里配置规则,资源名指定的是@SentinelResource注解value的值,
这样也是可以的,也就是不一定要指定访问路径
- 测试.
可以看到已经进入降级方法了
- 此时我们关闭8401服务
可以看到,这些定义的规则是临时的,关闭服务,规则就没有了
可以看到,上面配置的降级方法,又出现Hystrix遇到的问题了
3.2.7.1.1 自定义限流处理逻辑:
- 单独创建一个类Customerhandler ,用于处理限流
package com.tigerhhzz.springcloud.myhandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; /** * @author tigerhhzz * @date 2023/4/22 14:54 */ public class Customerhandler { public CommonResult handlerException(BlockException exception) { return new CommonResult(444,"按客户自定义,global handlerException"); } public CommonResult handlerException2(BlockException exception) { return new CommonResult(444,"按客户自定义,global handlerException2"); } }
- 在controller中,指定使用自定义类中的方法作为降级方法
@GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler",blockHandlerClass = Customerhandler.class,blockHandler = "handlerException2") public CommonResult customerBlockHandler() { return new CommonResult(200,"按客户自定义OK",new Payment(2020L,"serial002")); }
加 @SentinelResource注解进行服务的限流
用给定的限流名称到sentinel控制台中配置,自定义限流类+方法。
- Sentinel中定义流控规则:
这里资源名,是以url指定,也可以使用@SentinelResource注解value的值指定
- 测试:
3.2.7.2 @SentinelResource注解的其他属性:
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能
通过@SentinelResource来指定出现异常时的处理策略。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下
3.2.8 服务熔断:
3.2.8.1 启动nacos和sentinel
3.2.8.2 新建两个pay模块 9003和9004
- cloudalibaba-provider-payment9003
- cloudalibaba-provider-payment9004
- pom(两个模块的依赖一样)
<dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
- 配置文件(修改端口号9004)
server: port: 9003 spring: cloud: nacos: discovery: server-addr: localhost:8848 application: name: nacos-payment-provider management: endpoints: web: exposure: include: '*'
- 主启动类(修改9004)
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author tigerhhzz * @date 2023/4/24 9:59 */ @Slf4j @SpringBootApplication @EnableDiscoveryClient public class SentinelPaymentMain9003 { public static void main(String[] args) { SpringApplication.run(SentinelPaymentMain9003.class,args); log.info("SentinelPaymentMain9003启动成功~~~~~~~~~~~~~"); } }
- controller(两个模块一样)
package com.tigerhhzz.springcloud.controller; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; 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 java.util.HashMap; @RestController public class PaymentController { @Value("${server.post}") private String serverPost; public static HashMap<Long, Payment> hashMap = new HashMap<>(); static { hashMap.put(1L,new Payment(1L,"000000000000000001")); hashMap.put(2L,new Payment(2L,"000000000000000002")); hashMap.put(3L,new Payment(3L,"000000000000000003")); } @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); CommonResult<Payment> result = new CommonResult<>(200,"from mysql,serverPort:"+serverPost); return result; } }
**然后启动9003.9004**
3.2.8.3 新建一个order-84消费者模块:
-cloudalibaba-consumer-nacos-order84
- 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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-consumer-nacos-order84</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
server: port: 84 spring: application: name: cloud-nacos-order cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: dashboard: localhost:8080 port: 8719 #消费者将要去访问的微服务名称 server-url: nacos-user-service: http://nacos-payment-provider
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @Slf4j @SpringBootApplication @EnableDiscoveryClient public class SentinelNacosOrderMain84 { public static void main(String[] args) { SpringApplication.run(SentinelNacosOrderMain84.class,args); log.info("SentinelNacosOrderMain84启动成功~~~~~~~~~~~~~"); } }
- 配置类
package com.tigerhhzz.springcloud.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; /** * @author tigerhhzz * @date 2023/4/18 20:31 */ @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
- controller和service
@SentinelResource注解中
- fallback管运行异常
- blockHandler管配置违规
CircleBreakerController类
package com.tigerhhzz.springcloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import com.tigerhhzz.springcloud.service.PaymentService; 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 CircleBreakerController { @Value("${server.post}") private String serverPost; public static final String SERVICE_URL="http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") // @SentinelResource(value = "fallback") //没有配置 // @SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常 // @SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentine控制台配置违规 // @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler") //handlerFallback和blockHandler都配置 @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler", exceptionsToIgnore = IllegalAccessException.class) //exceptionsToIgnore配置 public CommonResult<Payment> fallback(@PathVariable("id") Long id) { CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL+"/paymentSQL/"+id, CommonResult.class,id); if (id==4) { throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...."); } else if (result.getData() == null) { throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } public CommonResult handlerFallback(@PathVariable("id") Long id, Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment); } public CommonResult blockHandler(@PathVariable("id") Long id, BlockException exception) { Payment payment = new Payment(id,"null"); return new CommonResult(445,"兜底异常handlerFallback,exception内容"+exception.getMessage(),payment); } //--------------Openfeign @Resource private PaymentService paymentService; @GetMapping(value = "/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { return paymentService.paymentSQL(id); } }
两个service类
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class ) public interface PaymentService { @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id); }
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import org.springframework.stereotype.Component; @Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(444,"服务降级返回---PaymentFallbackService",new Payment(id,"errorService")); } }
为业务方法添加fallback来指定降级方法:
fallback是用于管理异常的,当业务方法发生异常,可以降级到指定方法==
注意,我们这里并没有使用sentinel配置任何规则,但是却降级成功,就是因为
fallback是用于管理异常的,当业务方法发生异常,可以降级到指定方法==
为业务方法添加blockHandler,看看是什么效果
blockHandler只对sentienl定义的规则降级
如果fallback和blockHandler都配置呢?
可以看到,当两个都同时生效时,blockhandler优先生效
@SentinelResource还有一个属性,exceptionsToIgnore
exceptionsToIgnore指定一个异常类,
表示如果当前方法抛出的是指定的异常,不降级,直接对用户抛出异常
3.2.9 sentinel整合ribbon+openFeign+fallback
修改84模块,使其支持feign(注解+接口)
- 修改pom
添加opefeign依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
- 配置文件
#激活sentinel对feign的支持 feign: sentinel: enabled: true
- 主启动类,也要修改
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @Slf4j @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class SentinelNacosOrderMain84 { public static void main(String[] args) { SpringApplication.run(SentinelNacosOrderMain84.class,args); log.info("SentinelNacosOrderMain84启动成功~~~~~~~~~~~~~"); } }
- 创建远程调用pay模块的接口
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "nacos-payment-provider") public interface PaymentService { @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id); }
- 创建这个接口的实现类,用于降级
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import org.springframework.stereotype.Component; @Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(444,"服务降级返回---PaymentFallbackService",new Payment(id,"errorService")); } }
- 再次修改接口,指定降级类
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.entities.Payment; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class ) public interface PaymentService { @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id); }
- controller添加远程调用
//--------------Openfeign @Resource private PaymentService paymentService; @GetMapping(value = "/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { return paymentService.paymentSQL(id); }
- 测试
启动9003,84
- 测试,如果关闭9003.看看84会不会降级
**可以看到,正常降级了**
熔断框架比较
3.2.10 sentinel持久化规则
默认规则是临时存储的,重启sentinel就会消失
这里以之前的8401为案例进行修改:
- 修改8401的pom
添加: <!-- SpringCloud ailibaba sentinel-datasource-nacos 持久化需要用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
- 修改配置文件:
添加:
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 port: 8719 #如果8719被占用,自动递增1,直到找到没有被占用的端口 #sentinel流控规则用nocas持久化保存配置 datasource: ds1: nacos: server-addr: localhost:8848 dataId: ${spring.application.name} groupId: DEFAULT_GROUP data_type: json rule_type: flow management: endpoints: web: exposure: include: "*"
**实际上就是指定,我们的规则要保证在哪个名称空间的哪个分组下**
这里没有指定namespace, 但是是可以指定的
注意,这里的dataid要与8401的服务名一致
- 在nacos中创建一个配置文件,dataId就是上面配置文件中指定的
[ { // 资源名 "resource": "/rateLimit/byUrl", // 针对来源,若为 default 则不区分调用来源 "limitApp": "default", // 限流阈值类型(1:QPS;0:并发线程数) "grade": 1, // 阈值 "count": 1, // 是否是集群模式 "clusterMode": false, // 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待) "controlBehavior": 0, // 流控模式(0:直接;1:关联;2:链路) "strategy": 0 // 预热时间(秒,预热模式需要此参数) //"warmUpPeriodSec": 10, // 超时时间(排队等待模式需要此参数) //"maxQueueingTimeMs": 500, // 关联资源、入口资源(关联、链路模式) //"refResource": "rrr" } ]
注意:配置到nacos中时,把注释都去掉,有注释的json配置,规则不保存。
- 关闭8401,然后重启8401,此时需要需要首先访问一下接口地址http://localhost:8401/rateLimit/byUrl
然后再次刷新sentinel,又可以正常读取到规则,那么证明持久化成功
4、SpringCloud Alibaba Seata处理分布式事务
4.1 什么是分布式事务
一次业务操作需要跨多个数据源或需要多个系统进行远程调用,就会产生分布式事务问题
4.2 分布式事务中的一些概念
分布式事务中的一些概念,也是seata中的概念:
seata官网地址:http://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA
和 XA 事务模式,为用户打造一站式的分布式解决方案。
一个经典的分布式事务处理过程=1+3
1个全局唯一的事务ID
3个组件
- TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
4.3 seata安装和启动:
- 下载安装seata的安装包
- 修改bin下面的file.conf和registry.conf
alibaba seata分布式事务中bin/file.conf和registry.conf 修改后的文件下载地址:
https://mp.csdn.net/mp_download/manage/download/UpDetailed
- mysql建库建表
- 上面指定了数据库为seata,所以创建一个数据库名为seata
- 建表,在seata的安装目录下有一个db_store.sql,运行即可
- 继续修改配置文件,修改registry.conf
配置seata作为微服务,指定注册中心
- 启动
先启动nacos
在启动seata-server(运行安装目录下的,seata-server.bat)
4.3 商品交易案例
4.3.1 业务说明
下单—>库存—>账号余额
4.3.2 创建三个数据库
CREATE DATABASE seata_order; CREATE DATABASE seata_storage; CREATE DATABASE seata_account;
4.3.3 创建对应的表以及创建回滚日志表,方便查看
CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT 'id', `user_id` bigint DEFAULT NULL COMMENT '用户id', `total` decimal(10,0) DEFAULT NULL COMMENT '总额度', `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额', `residue` decimal(10,0) DEFAULT NULL COMMENT '剩余可用额度', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='账户表'; CREATE TABLE `undo_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `branch_id` bigint NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int 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 DEFAULT CHARSET=utf8; CREATE TABLE `t_order` ( `int` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '用户id', `product_id` bigint DEFAULT NULL COMMENT '产品id', `count` int DEFAULT NULL COMMENT '数量', `money` decimal(11,0) DEFAULT NULL COMMENT '金额', `status` int DEFAULT NULL COMMENT '订单状态: 0:创建中 1:已完结', PRIMARY KEY (`int`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='订单表'; CREATE TABLE `undo_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `branch_id` bigint NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int 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 DEFAULT CHARSET=utf8; CREATE TABLE `t_storage` ( `int` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint DEFAULT NULL COMMENT '产品id', `total` int DEFAULT NULL COMMENT '总库存', `used` int DEFAULT NULL COMMENT '已用库存', `residue` int DEFAULT NULL COMMENT '剩余库存', PRIMARY KEY (`int`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='库存'; CREATE TABLE `undo_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `branch_id` bigint NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int 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 DEFAULT CHARSET=utf8;
注意每个库都要执行一次这个sql,生成回滚日志表
4.3.4 创建微服务模块
业务需求:下订单----减库存—扣余额—改(订单)状态
每个业务都创建一个微服务,也就是要有三个微服务,订单,库存,账号
- 订单模块,seata-order-service2001
- 库存模块, seata-storage-service2002
- 账号模块,seata-account-service2003
4.3.4.1 订单模块seata-order-service2001
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-order-service2001</artifactId> <dependencies> <!-- 包含了Sleuth--> <!-- <dependency>--> <!-- <groupId>org.springframework.cloud</groupId>--> <!-- <artifactId>spring-cloud-starter-zipkin</artifactId>--> <!-- </dependency>--> <!-- Nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <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>1.0.0</version> </dependency> <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-actuator</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件yml
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中的对应,我们之前在seata的配置文件中配置的名字 tx-service-group: fsp_tx_group nacos: discovery: server-addr: 127.0.0.1:8848 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 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 username: root password: root feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath*:mapper/*.xml
- 配置文件file.conf和registry.conf
- 创建配置文件file.conf
- 创建registry.conf:
两个配置文件的下载地址:https://download.csdn.net/download/weixin_43025151/87722693
==实际上,就是要将seata中的我们之前修改的两个配置文件复制到这个项目下==
- domain
两个domain
- CommonResult
- Order
package com.tigerhhzz.springcloud.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author tigerhhzz * @date 2023/4/24 10:58 */ @Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code, message, null); } }
package com.tigerhhzz.springcloud.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; /** * @author tigerhhzz * @date 2023/4/24 10:56 */ @Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long user_id; private Long product_id; private Integer count; private BigDecimal money; private Integer status; //订单状态:0:创建中;1:已完结 }
- Dao接口及实现
两个方法=下订单+改(订单)状态
package com.tigerhhzz.springcloud.dao; import com.tigerhhzz.springcloud.domain.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @author tigerhhzz * @date 2023/4/24 10:54 */ @Mapper public interface OrderDao { //创建订单 void create(Order order); //修改订单状态 void update(@Param("userId") Long userId, @Param("status") Integer status); }
- Service接口及实现
OrderMapper.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.atguigu.springcloud.alibaba.dao.OrderDao"> <resultMap id="order" type="com.tigerhhzz.springcloud.domain.Order"> <result property="id" column="id" jdbcType="BIGINT"/> <result property="user_id" column="userId" jdbcType="BIGINT"/> <result property="product_id" column="productId" jdbcType="BIGINT"/> <result property="count" column="count" jdbcType="INTEGER"/> <result property="money" column="money" jdbcType="BIGINT"/> <result property="status" column="status" jdbcType="INTEGER"/> </resultMap> <insert id="create"> insert into t_order(user_id,product_id,count,money,status) value (#{userId},#{productId},#{count},#{money},0) </insert> <update id="update"> update t_order set status = 1 where user_id = #{userId} and status = #{status} </update> </mapper>
OrderService
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.domain.Order; /** * @author tigerhhzz * @date 2023/4/24 11:01 */ public interface OrderService { void create(Order order); }
AccountService
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.domain.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; /** * @author tigerhhzz * @date 2023/4/24 11:02 */ @Component @FeignClient(value = "seata-account-service") public interface AccountService { @PostMapping("/account/decrease") CommonResult decrease(@RequestParam("userId")Long userId, @RequestParam("money") BigDecimal money); }
StorageService
package com.tigerhhzz.springcloud.service; import com.tigerhhzz.springcloud.domain.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @author tigerhhzz * @date 2023/4/24 11:03 */ @Component @FeignClient(value = "seata-storage-service") public interface StorageService { @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
OrderServiceImpl
package com.tigerhhzz.springcloud.service.impl; import com.tigerhhzz.springcloud.dao.OrderDao; import com.tigerhhzz.springcloud.domain.Order; import com.tigerhhzz.springcloud.service.AccountService; import com.tigerhhzz.springcloud.service.OrderService; import com.tigerhhzz.springcloud.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * @author tigerhhzz * @date 2023/4/24 11:04 */ @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) @Override public void create(Order order) { log.info("-------->开始创建新订单"); orderDao.create(order); log.info("--------订单微服务开始调用库存,做扣减"); storageService.decrease(order.getProduct_id(),order.getCount()); log.info("-------订单微服务开始调用库存,做扣减end"); log.info("-------订单微服务开始调用账户,做扣减"); accountService.decrease(order.getUser_id(),order.getMoney()); log.info("-------订单微服务开始调用账户,做扣减end"); log.info("-------修改订单状态"); orderDao.update(order.getUser_id(),0); log.info("-------修改订单状态结束"); log.info("--------下订单结束了,哈哈哈哈"); } }
- Controller
OrderController
package com.tigerhhzz.springcloud.controller; import com.tigerhhzz.springcloud.domain.CommonResult; import com.tigerhhzz.springcloud.domain.Order; import com.tigerhhzz.springcloud.service.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author tigerhhzz * @date 2023/4/24 11:07 */ @RestController public class OrderController { @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order) { orderService.create(order); return new CommonResult(200,"订单创建成功!"); } }
- Config配置
- MybatisConfig
- DataSourceProxyConfig
package com.tigerhhzz.springcloud.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; /** * @author tigerhhzz * @date 2023/4/24 11:08 */ @Configuration @MapperScan({"com.tigerhhzz.springcloud.dao"}) public class MybatisConfig { }
package com.tigerhhzz.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.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; /** * @author tigerhhzz * @date 2023/4/24 11:09 */ @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("classpath:mapper/*.xml")); // sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
- 主启动类
SeataOrderMainApp2001
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; 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; /** * @author tigerhhzz * @date 2023/4/24 10:48 */ @Slf4j @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建 @EnableFeignClients @EnableDiscoveryClient public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class,args); log.info("SeataOrderMainApp2001启动成功~~~~~~~~~~~~~"); } }
启动SeataOrderMainApp2001服务;
先启动nacos,再启动seata服务,等seata服务注册进nacos后,最后启动SeataOrderMainApp2001服务;
启动成功,服务注册进nacos;
4.3.4.2 库存模块seata-storage-service2002
- 修改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>springcloud-tigerhhzz</artifactId> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-storage-service2002</artifactId> <dependencies> <!-- 包含了Sleuth--> <!-- <dependency>--> <!-- <groupId>org.springframework.cloud</groupId>--> <!-- <artifactId>spring-cloud-starter-zipkin</artifactId>--> <!-- </dependency>--> <!-- Nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <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> <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-actuator</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.tigerhhzz.springcloud-tigerhhzz</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
- 配置文件
server: port: 2002 spring: application: name: seata-storage-service cloud: alibaba: seata: tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.gjt.mm.mysql.Driver url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapper-locations: classpath:mapper/*.xml
- 配置文件file.conf和registry.conf
同订单模块一样
- Domain
两个domain
- Storage
- CommonResult (同订单模块一样)
package com.tigerhhzz.springcloud.domain; import lombok.Data; /** * @author tigerhhzz * @date 2023/4/24 11:26 */ @Data public class Storage { private Long id; /** * 产品id */ private Long product_id; /** * 总库存 */ private Integer total; /** * 已用库存 */ private Integer used; /** * 剩余库存 */ private Integer residue; }
- Dao层
StorageDao
package com.tigerhhzz.springcloud.dao; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @author tigerhhzz * @date 2023/4/24 11:29 */ @Mapper public interface StorageDao { void decrease(@Param("product_id") 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.tigerhhzz.springcloud.dao.StorageDao"> <resultMap id="order" type="com.tigerhhzz.springcloud.domain.Storage"> <result property="id" column="id" jdbcType="BIGINT"/> <result property="product_id" column="productId" jdbcType="BIGINT"/> <result property="total" column="total" jdbcType="INTEGER"/> <result property="used" column="used" jdbcType="INTEGER"/> <result property="residue" column="residue" jdbcType="INTEGER"/> </resultMap> <update id="decrease"> update t_storage set used = used + #{count},residue = residue -#{count} where product_id = #{productId} </update> </mapper>
- service层
StorageService
package com.tigerhhzz.springcloud.service; public interface StorageService { /** * 扣减库存 * @param productId * @param count */ void decrease(Long productId,Integer count); }
StorageServiceimpl
import com.tigerhhzz.springcloud.dao.StorageDao; import com.tigerhhzz.springcloud.service.StorageService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class StorageServiceimpl implements StorageService { @Resource private StorageDao storageDao; @Override public void decrease(Long productId, Integer count) { log.info("库存扣减开始----"); storageDao.decrease(productId,count); log.info("库存扣减结束----"); } }
- controller层
package com.tigerhhzz.springcloud.controller; import com.tigerhhzz.springcloud.domain.CommonResult; import com.tigerhhzz.springcloud.service.StorageService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping(value = "/storage") public class StorageController { @Resource private StorageService storageService; @PostMapping(value = "/decrease") public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) { storageService.decrease(productId, count); return new CommonResult(200,"库存扣减成功,哈哈哈哈"); } }
- 两个config(同订单模块一样)
- MybatisConfig
- DataSourceProxyConfig
- 主启动类
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; 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; /** * @author tigerhhzz * @date 2023/4/24 11:28 */ @Slf4j @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableFeignClients @EnableDiscoveryClient public class SeataStorageMainApp2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageMainApp2002.class,args); log.info("SeataStorageMainApp2002启动成功~~~~~~~~~~~~~"); } }
4.3.4.3 账号模块seata-account-service2003
- 修改pom
依赖同库存模块一样
- 配置文件yml
端口号和服务名使用自己的,其他配置同库存模块一样
- 配置文件file.conf和registry.conf
同订单模块一样
- Domain
两个domain
- Acount
- CommonResult (同订单模块一样)
package com.tigerhhzz.springcloud.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; /** * @author tigerhhzz * @date 2023/4/24 10:56 */ @Data @AllArgsConstructor @NoArgsConstructor public class Acount { private Long id; /** * 用户id */ private Long user_id; /** * 总额度 */ private BigDecimal total; /** * 已用额度 */ private BigDecimal used; /** * 剩余额度 */ private BigDecimal residue; }
- Dao层
AccountDao
package com.tigerhhzz.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.tigerhhzz.springcloud.dao.AccountDao"> <resultMap id="account" type="com.tigerhhzz.springcloud.domain.Account"> <result property="id" column="id" jdbcType="BIGINT"/> <result property="user_id" column="userId" jdbcType="BIGINT"/> <result property="total" column="total" jdbcType="DECIMAL"/> <result property="used" column="used" jdbcType="DECIMAL"/> <result property="residue" column="residue" jdbcType="DECIMAL"/> </resultMap> <update id="decrease"> update t_account set used = used + #{money},residue = residue - #{money} where user_id=#{userId} </update> </mapper>
- service层
AccountService
package com.tigerhhzz.springcloud.service; import java.math.BigDecimal; public interface AccountService { void decrease(Long userId, BigDecimal money); }
AccountServiceimpl
package com.tigerhhzz.springcloud.service.impl; import com.tigerhhzz.springcloud.dao.AccountDao; import com.tigerhhzz.springcloud.service.AccountService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; @Service @Slf4j public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Override public void decrease(Long userId, BigDecimal money) { log.info("账户扣除余额开始---"); accountDao.decrease(userId, money); log.info("账户扣除余额结束---"); } }
- controller层
package com.tigerhhzz.springcloud.controller; import com.tigerhhzz.springcloud.entities.CommonResult; import com.tigerhhzz.springcloud.service.AccountService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.math.BigDecimal; @RestController @RequestMapping(value = "account") public class AccountController { @Resource private AccountService accountService; @PostMapping(value = "decrease") public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money) { accountService.decrease(userId, money); return new CommonResult(200,"账户余额扣减成功,哈哈哈"); } }
- 两个config(同订单模块一样)
- MybatisConfig
- DataSourceProxyConfig
- 主启动类
SeataAccountMainApp2003
package com.tigerhhzz.springcloud; import lombok.extern.slf4j.Slf4j; 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; @Slf4j @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableFeignClients @EnableDiscoveryClient public class SeataAccountMainApp2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMainApp2003.class,args); log.info("SeataAccountMainApp2003启动成功~~~~~~~~~~~~~"); } }
4.3.4.4 数据库初始情况
数据库seata_account
表t_account
数据库seata_storage
表t_storage
4.3.4.5 正常下单
启动2001 访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
订单库中增加一条记录
库存库中的记录
账户库中的记录
4.3.4.6 超时异常,没加@GlobalTransactional
2003模块AccountServiceImpl
//模拟超时异常,全局事务回滚 try { TimeUnit.SECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); }
重启2003
访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
检查数据库
订单库中增加了数据,但是订单状态为0,代表未支付
库存表中扣除了
账号表中也被扣钱了
故障情况:
当库存和账号金额扣减后,订单状态并没有设置为已完成,没有从0改为1
而且由于feign的重试机制,账号余额还有可能被多次扣减
4.3.4.7 超时异常,添加@GlobalTransactional
修改2001模块OrderServiceImpl 添加注解
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
重启2001,访问地址http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
当出现异常时,三个数据库中并没有增加记录。
发生异常后,直接回滚了,前面的修改操作都回滚了。
4.4 setat原理
seata-server-1.4.2
seata提供了四个模式:
第一阶段:
二阶段之提交:
二阶段之回滚:
断点:
可以看到,他们的xid全局事务id是一样的,证明他们在一个事务下
before 和 after的原理就是
在更新数据之前,先解析这个更新sql,然后查询要更新的数据,进行保存