重点 : 约定 > 配置 > 编码
降级方法的参数跟原方法的原始值相同
编码五部曲:
- 建module
- 改pom
- 写yml
- 主启动
- 业务类
好的环境配置比代码更重要
所有源码地址:https://gitee.com/xyy-kk_admin/spring-cloud
入门篇链接:https://blog.csdn.net/qiwunongqingyin/article/details/117927188
初级篇链接:https://blog.csdn.net/qiwunongqingyin/article/details/118028552
中级篇链接:https://blog.csdn.net/qiwunongqingyin/article/details/118151377
高级篇链接:https://blog.csdn.net/qiwunongqingyin/article/details/118411828
- SpringCloud Alibaba 入门简介
- SpringCloud Alibaba Nacos 服务注册和配置中心
- SpringCloud Alibaba Sentinel 现实熔断与限流
- SpringCloud Alibaba Seata处理分布式事务
17. SpringCloud Alibaba 入门简介
官网: https://spring.io/projects/spring-cloud-alibaba
官方参考手册:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
中文文档 : https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
Spring Cloud Dalston、Edgware、Finchley 和 Greenwich 都已达到生命周期终止状态,不再受支持。
-
什么是维护模式?
将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能。
他们将修复block级别的 bug 以及安全问题,他们也会考虑并审查社区的小型pull request。
-
微服务关系:
17.1 SpringCloud Alibaba 带来了什么?
服务限流降级:默认支持Servlet、Feign、RestTemplate、Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics 监控。
服务注册与发现:适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于Spring Cloud Stream为微服务应用构建消息驱动能力。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker (schedulerx-client)上执行。
18. SpringCloud Alibaba Nacos 服务注册和配置中心
官网: https://spring.io/projects/spring-cloud-alibaba#learn
下载地址: https://github.com/alibaba/nacos/tags 自己寻找版本下载,(我自己2.0.2
)
18.1 概述
为什么叫 Nacas?
- 前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。
是什么?
- 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- Nacos: Dynamic Naming and Configuration Service
- Nacos就是注册中心+配置中心的组合 -> Nacos = Eureka+Config+Bus
能干嘛?
- 替代Eureka做服务注册中心
- 替代Config做服务配置中心
框架对比
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本闭源) |
Zookeeper | CP | 不支持 | 中 |
consul | CP | 支持 | 高 |
Nacos | AP | 支持 | 高 |
据说Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。
18.2 下载与安装
-
本地 Java8 与 maven 环境 ok !!
-
下载地址: https://github.com/alibaba/nacos/tags 自己寻找版本下载,(我自己
2.0.2
,目前推荐的最稳定的版本) -
解压进入bin目录,启动
startup.cmd
(直接启动是集群环境会报错…)
打开cmd命令窗口,输入:startup.cmd -m standalone
启动单机模式
注: wdnmd(唯独你没懂),第一次加载时间超长,可以去吃个饭再回来… -
cmd命令窗口会显示访问路径的: http://localhost:8848/nacos
账号和密码默认都是nacos
18.3 基于 Nacos 的服务提供者
-
新建子项目
cloudalibaba-provider-payment9001
-
pom
父pom:(应该已经有了)<!--spring cloud alibaba 2.2.0.RELEASE--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
9001pom:
<?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>cloud</artifactId> <groupId>com.xyy</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> </dependencies> </project>
-
yml
server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848 management: endpoints: web: exposure: include: '*'
-
启动类
PaymentMain9001
,注解:@EnableDiscoveryClient
-
业务controller:
PaymentNacosController
@RestController @RequestMapping("payment/nacos") public class PaymentNacosController { @Value("${server.port}") private String serverPort; @GetMapping(value = "getport/{id}") public String getPayment(@PathVariable("id") Integer id) { return "nacos registry, serverPort: "+ serverPort+"\t id"+id; } }
-
访问测试: http://localhost:9001/payment/nacos/getport/1
nacos页面: http://localhost:8848/nacos -> 服务管理 -> 服务列表 可以看到已经注册成功
-
为了演示集群,根据9001 新建
cloudalibaba-provider-payment9002
或者不新建,使用模拟的方式copy9001:- 右击server栏的9001,复制配置
- 在VM options(VM选项)一栏中填入 ->
-DServer.port=9002
没空格有点,我没写错 - 确认即可
18.4 基于 Nacos 的服务消费者
nacos内嵌ribbon
:
- 新建
cloudalibaba-consumer-nacos-order80
- 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>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-consumer-nacos-order80</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>com.xyy</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
- yml
server: port: 80 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 # 可写可不写,Controller要用,提出来 类 也一样 #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) service-url: nacos-user-service: http://nacos-payment-provider
- 启动类
OrderNacosMain80
,注解:@EnableDiscoveryClient
- 配置类:
ApplicationContextConfig
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
- 业务类controller:
OrderNacosController
@RestController @RequestMapping("order/nacos") public class OrderNacosController { @Resource private RestTemplate restTemplate; //这个就是在配置类中写的路径 @Value("${service-url.nacos-user-service}") private String serverURL; @GetMapping(value = "getport/{id}") public String getPayment(@PathVariable("id") Integer id) { return restTemplate.getForObject(serverURL+"/payment/nacos/getport/"+id,String.class); } }
- 启动测试: http://localhost/order/nacos/getport/1
nacos:
18.5 Nacos服务注册中心对比提升
-
Nacos全景图:
Nacos和CAP -
Nacos与其他注册中心特性对比
-
Nacos服务发现实例模型
-
Nacos支持AP和CP模式的切换
C是所有节点在同一时间看到的数据是一致的;而A的定义是所有的请求都会收到响应。
何时选择使用何种模式?
—般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring cloud和Dubbo服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
切换命令:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP
18.6 Nacos 配置中心 -> 基础配置
-
新建子项目
cloudalibaba-config-nacos-client3377
-
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>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-config-nacos-client3377</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> </dependencies> </project>
-
yml
配置规则: https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
bootstrap.yml是云配置优先,application.yml是本地bootstrap.yml:
# 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格式的配置 # nacos 新建配置Data ID的命名规则: # 默认项目名(或者: spring.cloud.nacos.config.prefix) - application.yml中的spring.profile.active - config.file-extension # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} # nacos-config-client-dev.yaml # nacos-config-client-test.yaml ----> config.info
applicaton.yml:
spring: profiles: active: dev # 表示开发环境
-
启动类
NacosConfigClientMain3377
,注解@NacosConfigClientMain3377
-
业务类controller:
ConfigClientController
@RestController @RequestMapping("/config") @RefreshScope //支持nacos动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @RequestMapping("/info") public String getConfigInfo(){ return configInfo; } }
-
进入nacos页面:http://localhost:8848/nacos
配置列表添加以下内容发布: (目前仅支持为yaml,Properties,要跟bootstrap.yml中匹配)
-
测试启动3377,访问路径: http://localhost:3377/config/info
nacos: -
修改nacos配置中心内容将
nacos-config-client-dev.yaml
文件的版本号从1改为2,发布,重新刷新3377页面,会发现直接修改成功
18.7 Nacos 配置中心 -> 分类配置
问题 - 多环境多项目管理
-
实际开发中,通常一个系统会准备?
dev开发环境
test测试环境
prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
-
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…那怎么对这些微服务配置进行管理呢?
Nacos的图形化管理界面
Namespace+Group+Data lD三者关系?为什么这么设计?
-
是什么
类似Java里面的package名和类名最外层的namespace是可以用于区分部署环境的,Group和DatalD逻辑上区分两个目标对象。
-
三者情况
默认情况:Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
Nacos默认的Namespace是public,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
Service就是微服务:一个Service可以包含多个Cluster (集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。
比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ) ,给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
最后是Instance,就是微服务的实例。
三种方案:
-
DataID方案:
指定spring.profile.active和配置文件的DatalD来使不同环境下读取不同的配置
默认空间+默认分组+新建dev和test两个DatalD
通过spring.profile.active属性就能进行多环境下配置文件的读取
测试: http://localhost:3377/config/info
配置什么就加载什么!!! -
Group 方案:
新建配置:
DEV_GROUP
下的nacos-config-client-info.yaml
发布:
新建配置:TEST_GROUP
下的nacos-config-client-info.yaml
发布:
结果:修改yml: 都写在下面了
# bootstrap.yml spring: cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 #新增: group: DEV_GROUP # appliction.yml spring: profiles: active: info # active: test # active: dev # 表示开发环境
访问3377: http://localhost:3377/config/info
修改bootstrap.yml的group为TEST_GROUP
,刷新3377!!!配置完成!!!
-
Namespace 方案:
访问nacos地址,
新建
dev
|test
两个namespace
在配置列表dev中,新增:(自行修改配置内容)
修改yml#bootstrap.yml spring: cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 group: DEV_GROUP #新增: namespace: 37b5cf1e-5ccb-4fdb-bd8e-38cd2129301e #application.yml spring: profiles: active: dev # 表示开发环境
访问测试:http://localhost:3377/config/info
修改分组,在刷新3377
18.8 Nacos 集群和持久化
官网: https://nacos.io/zh-cn/docs/deployment.html
我们需要mysql数据库:
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
- 单机模式-用于测试和单机试用。
- 集群模式-用于生产环境,确保高可用。
- 多集群模式-用于多数据中心场景。
单机模式支持mysql
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
- 安装数据库,版本要求:5.6.5+
- 初始化mysq数据库,数据库初始化文件: nacos-mysql.sql
- 修改conf/application.properties文件,增加支持mysql数据源配置(目前只- 支持mysql),添加mysql数据源的url、用户名和密码。
再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql。#*************** Config Module Related Configurations ***************# ### If use MySQL as datasource: spring.datasource.platform=mysql ### Count of DB: db.num=1 ### Connect URL of DB: db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=root
nacos默认存储库: derby
github的pom文件查看: https://github.com/alibaba/nacos/blob/develop/pom.xml
derby切换mysql 步骤:
-
nacos下的conf目录下找到sql脚本:
nacos-mysql.sql
,在数据库执行脚本导入 -
nacos下的conf目录下找到
application.properties
,将以下注释解除并修改: -
重新启动Nacos,发现之前的配置全部消失 ,是因为使用的为mysql的数据库,其中什么都没有
Linux版Nacos+MySql生产环境配置:
- 预计需要 1个Nginx + 3个nacos注册中心 + 1个mysql
- Nacos下载linux版:https://github.com/alibaba/nacos/tags
- 安装mysql (Ubuntu OS 中Mysql的安装,操作与卸载)
- 安装nginx:(Ubuntu 中nginx安装)
- 将nacos放到linux中,解压
tar -zxvf nacos-2.0.tar.gz
,并且拷贝副本为mynacos
集群配置步骤:
-
Linux 服务器上mysql数据库配置
//进入mynacos下的conf文件夹,mysql表在此文件夹下(`nacos-mysql.sql`) cd mynacos/conf //登陆mysql mysql -uroot -p //查看所有数据库 show databases; //没有数据库nacos-config的话新建 CREATE DATABASE `nacos_config`; //选择数据库 use `nacos_config`; //执行sql脚本 source nacos-mysql.sql; //查看 show tables;
注,新的nacos比旧的多了一张
permissions
表 -
修改
application.properties
:把这些注解打开,修改数据库名称和用户名密码即可
-
按照1,2步骤,再配置两台机器
-
Nginx配置,由他作为负载均衡
-
启动nginx
-
启动nacos
-
修改
cloudalibaba-provider-payment9002
的ymlspring: cloud: nacos: discovery: # server-addr: 127.0.0.1:8848 server-addr: nginx运行服务器的ip:1234
-
启动测试
19. SpringCloud Alibaba Sentinel 现实熔断与限流
gibhub: https://github.com/alibaba/Sentinel
官网文档1: https://sentinelguard.io/zh-cn/
github文档: https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
下载地址: https://github.com/alibaba/Sentinel/tags
19.1 概述
-
sentinel跟Hystrix差别?
- Hystrix 需要手动搭建监控平台
- Hystrix 没有web界面更加细的配置
流控,速率控制,服务熔断,服务降级…越来越多,很麻烦
- sentinel 单独一个组件,可以独立出来
- sentinel 支持界面化细粒度统一配置
尽量使用配置和注解少写代码
-
是什么?
- 丰富的应用场景:哨兵接了阿里巴巴近10年的双十一大促流量的核心场景,例如杀(即爆发流量控制在系统可控制的范围)、消息削峰填谷、流量控制、实时熔断断不能应用等。
- 您可以在实时监控中同时提供监控功能。您可以在单台实时查看接入应用的机器,甚至台下规模的500秒的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其他开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。进入哨兵。
- 改进的 SPI 扩展点:Sentinel 提供简单易用、改进的 SPI 扩展接口。您可以通过实现扩展来接口快速地自定义逻辑。例如源定制规则管理、适配动态数据等。
-
主要特性:(绿)
-
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
19.2 安装运行
下载地址: https://github.com/alibaba/Sentinel/tags
运行java -jar sentinel1.8.2.jar
,默认端口8080(需要java8)
web访问地址: http://localhost:8080
账户: sentinel
密码: sentinel
19.3 初始化演示工程
- 启动nacos
startup.cmd -m standalone
- 启动sentinel
java -jar sentinel1.8.jar
访问路径: http://localhost:8080/ - 新建子项目
cloudalibaba-sentinel-service8401
- pom (openfeign和sentinel-datasource-nacos后面要用,现在先加上)
<?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>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-sentinel-service8401</artifactId> <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> <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</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>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> <dependency> <groupId>com.xyy</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
- yml
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址 #默认8719,如果被占用会自动+1直到找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*' endpoint: sentinel: enabled: false feign: sentinel: enabled: true # 激活Sentinel对Feign的支持
- 启动类
SentinelMain8401
,注解:@EnableDiscoveryClient
- 业务类controller:
FlowLimitController
@RestController @RequestMapping("sentinel/flow") public class FlowLimitController { //这个是显示在sentinel上的方法别名 @SentinelResource("getA") @RequestMapping("A") public String testA(){ return "---A"; } @SentinelResource("getB") @RequestMapping("B") public String testB(){ return "---B"; } }
- 启动8401测试: http://localhost:8401/sentinel/flow/A
访问sentinel: http://localhost:8080
注: 需要先访问过8401后再查看sentinel的页面,否则是空的,因为sentinel是懒加载
模式,并且如果一段时间不访问方法,则不会显示该方法
19.4 流控规则
基本介绍:
- 资源名:唯一名称,默认请求路径。
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。
- 线程数:当调用该API的线程数达到阈值的时候,进行限流。
- 是否集群:不需要集群。
- 流控模式:
- 直接:API达到限流条件时,直接限流。
- 关联:当关联的资源达到阈值时,就限流自己。
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。
- 流控效果:
- 快速失败:直接失败,抛异常。
- Warm up:根据Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效。
19.4.1 流控模式
直接:
-
点击
簇点链路
给getA添加流控规则:
-
再次访问http://localhost:8401/sentinel/flow/A,快速刷新,结果500
(因为配置了@SentinelResource
,否则页面显示Blocked by Sentinel (flow limiting)
,我不配置这个直接加载不到sentinel控制台) -
不管是
500
还是Blocked by Sentinel (flow limiting)
,都不是想要的,这时候需要自定义报错页面
,之后讲… -
流控规则为并发线程数: 并发情况下,超过指定数量以外的会报错
getA流控规则换为并发
testA方法修改:
@RequestMapping("A") public String testA(){ ThreadUtil.sleep(800); return "---A"; }
疯狂刷新http://localhost:8401/sentinel/flow/A,结果也会报错
关联:
- 当自己关联的资源达到阈值时,就限流自己
- 当与A关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)
- 将流控规则改为:
- 并发访问B(我用的是
API POST
)
- 在访问 getB 的过程中浏览器访问 getA,getA出错
19.4.2 流控效果
快速失败
直接失败: 抛出异常
预热:
公式: 阈值 / coldFactor(默认3), 经过预热时长后才会到达阈值
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
源码 -> com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
–
案例,阀值为10+预热时长设置5秒。
系统初始化的阀值为10/ 3约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10
效果: 在前五秒钟疯狂刷新getB链接(超过每秒3个)会有报错信息,五秒之后疯狂刷新(低于每秒10个),则不会有任何报错信息
排队等待
匀速排队: 让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
测试: 设置以下内容:
每秒钟最多五个请求,排队访问,多出来的顺延,访问超过2000毫秒时报错
19.5 降级规则
官网: https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
慢调用比例
每次未熔断时统计时长为ms,在此时间内. 响应超过RT时间被标记为慢调用,慢调用比例超过比例阈值后熔断 [取值为 0 - 1
之间] ,熔断时长为s,超过该时间取消熔断, 每秒调用数 < 最小请求数 则不启动熔断,
异常比例
在3000ms内,至少有8个请求,错误率达到60%,则熔断3s,然后重新统计时间
异常数
在3000ms内,至少有8个请求,异常5个,则熔断3s,然后重新统计时间
19.6 Sentinel 热点 (服务降级)
官网: https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81
基础配置
-
修改8401的controller:
//deal_testHotKey 为降级服务名 @SentinelResource(value = "testHotKey",blockHandler="deal_testHotKey") @RequestMapping("testHotKey") public String testHotKey(@RequestParam(value = "p1",required = false)String p1, @RequestParam(value = "p2",required = false)String p2){ return "---testHotKey"; } //BlockException public String deal_testHotKey(String p1, String p2, BlockException blockException){ return "---deal_testHotKey---o(╥﹏╥)o"+p1+p2; }
-
配置sentinel:
访问链接: http://localhost:8401/sentinel/flow/testHotKey?p1
=a&p2=b解释: QPS(Queries-per-second)模式是每秒查询速率; 每秒钟,请求包含第0个参数(
p1,下标从0开始,方法的参数,不是请求的参数
), 访问超过3个,降级5秒
高级配置
p1=浪
的时候限流阈值为10,其他的还是3;
注: 支持基本类型和String类型,参数的值=指定值,限流阈值更改(中文需要URL编码改一下)
将testHotKey
方法抛异常,加入以下代码int age =10/0;
运行测试???
将会直接500,并不会进入兜底方法
注:
@SentinelResource - 处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
RuntimeException int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管
后面会讲fallback
可以配
19.7 系统限流
官网: https://github.com/alibaba/Sentinel/wiki/系统自适应限流
简介:
- 从整体维度对应用入口流量进行控制
支持模式:
- 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 达到阈值即触发系统保护。
19.8 @SentinelResource
上述兜底方案面领的问题:
- 系统默认的,没有体现我们自己的业务要求。
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
- 每个业务方法都添加—个兜底的,那代码膨胀加剧。
- 全局统—的处理方法没有体现。
19.8.1 自定义限流处理逻辑
- 自定义公有兜底类:
public class CustomerBlockHandler { public static Output handlerException(BlockException exception){ return Output.failure(444,exception.getClass().getCanonicalName()+"\t客户自定义兜底方法=========1"); } public static Output handlerException2(BlockException exception){ return Output.failure(444,exception.getClass().getCanonicalName()+"\t客户自定义兜底方法==========2"); } }
- 新增controller方法:
// blockHandlerClass = 兜底类,blockHandler = 兜底类中的那个方法? @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") @GetMapping("/customerBlockHandler") public Output byURL1(){ return Output.success("自定义 限流成功",new Payment(2020L,"serial001")); }
- 增加限流规则:
- 访问测试: 快速刷新http://localhost:8401/sentinel/rate/customerBlockHandler
19.8.2 更多注解说明
地址:https://github.com/alibaba/Sentinel/wiki/注解支持sentinelresource-注解
注意:注解方式埋点不支持 private 方法。
-
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 逻辑中,而是会原样抛出。
19.9 服务熔断
sentinel 整合 ribbon + openFeign + fallback
两种降级方法
-
fallback
运行异常降级配置 -
blockHandler
sentinel控制台异常降级配置@SentinelResource(value = "test",blockHandler = "blockHandler降级方法",fallback ="fallback降级方法")
19.9.1 -> 熔断之 Ribbon 系列:
19.9.1 程序准备
准备三个子项目:
cloudalibaba-provider-payment9003
服务提供者
cloudalibaba-provider-payment9004
服务提供者
cloudalibaba-consumer-nacos-order84
服务消费者
服务提供者:
- 新建子项目
cloudalibaba-provider-payment9003
和cloudalibaba-provider-payment9004
的集群 - 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>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-provider-payment9003</artifactId> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </dependency> <dependency> <groupId>com.xyy</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
- yml
server: port: 9003 #9004 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址 management: endpoints: web: exposure: include: '*'
- 启动类:
PaymentMain9003/9004
,注解:@EnableDiscoveryClient
- 业务类controller:
PaymentController
@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,"28a8c1e3bc2742d8848569891fb42181")); hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182")); hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183")); } @GetMapping(value = "/paymentSQL/{id}") public Output<Payment> paymentSQL(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); Output<Payment> result = Output.success("from mysql,serverPort: "+serverPort,payment); return result; } }
服务消费者
-
新建子项目
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>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloudalibaba-consumer-nacos-order84</artifactId> <dependencies> <!--SpringCloud openfeign --> <!-- <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> --> <!--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> <!-- 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-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>com.xyy</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
-
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 # 激活Sentinel对Feign的支持 feign: sentinel: enabled: true
-
启动类
OrderNacosMain84
,注解:@EnableDiscoveryClient
-
配置类config:
ApplicationContextConfig
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
-
业务类controller:
CircleBreakerController
@RestController public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback")//没有配置 public Output<Payment> fallback(@PathVariable Long id) { Output<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,Output.class,id); if (id == 4) { throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } }
19.9.2 运行错误降级
只加fallback
- 修改84的controller
@RestController public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") //@SentinelResource(value = "fallback")//没有配置 @SentinelResource(value="fallback",fallback = "handlerFallback") //运行异常 降级方法 public Output<Payment> fallback(@PathVariable Long id) { Output<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,Output.class,id); if (id == 4) { throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } //Throwable 可以将报错信息带过来 public Output handlerFallback(Long id,Throwable e) { return Output.failure(444,"兜底异常 handlerFallback,exception内容 \t id:"+id+"\t"+e.getMessage()); } }
- 测试: http://localhost:84/consumer/fallback/4
结果:
只加 blockHandler
- 修改84controller:
@RestController public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") //@SentinelResource(value = "fallback")//没有配置 //@SentinelResource(value="fallback",fallback = "handlerFallback") //运行异常 降级方法 @SentinelResource(value = "fallback",blockHandler = "blockHandler") //sentinel 控制台配置违规 public Output<Payment> fallback(@PathVariable Long id) { Output<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,Output.class,id); if (id == 4) { throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } // //Throwable 可以将报错信息带过来 // public Output handlerFallback(Long id,Throwable e) // { // return Output.failure(444,"兜底异常 handlerFallback,exception内容 \t id:"+id+"\t"+e.getMessage()); // } public Output blockHandler(Long id, BlockException e) { return Output.failure(444,"兜底异常 blockHandler,exception内容 id:"+id+" "+e.getMessage()); } }
- 配置sentinel:
- 运行测试:
正确参数:http://localhost:84/consumer/fallback/1
快速刷新会进入兜底方法
错误参数:http://localhost:84/consumer/fallback/4
快速刷新会进入兜底方法,慢速刷新会爆500的错误
fallback和blockHandler都配置
-
修改84 controller
@RestController public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") //@SentinelResource(value = "fallback")//没有配置 //@SentinelResource(value="fallback",fallback = "handlerFallback") //运行异常 降级方法 //@SentinelResource(value = "fallback",blockHandler = "blockHandler") //sentinel 控制台配置违规 @SentinelResource(value="fallback",fallback = "handlerFallback",blockHandler = "blockHandler") public Output<Payment> fallback(@PathVariable Long id) { Output<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,Output.class,id); if (id == 4) { throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } //Throwable 可以将报错信息带过来 //运行异常兜底 public Output handlerFallback(Long id,Throwable e) { return Output.failure(444,"兜底异常 handlerFallback,exception内容 id:"+id+" "+e.getMessage()); } //sentinel控制台违规兜底 public Output blockHandler(Long id, BlockException e) { return Output.failure(444,"兜底异常 blockHandler,exception内容 id:"+id+" "+e.getMessage()); } }
-
sentinel配置:
-
测试:
参数正确: http://localhost:84/consumer/fallback/1快速访问会进入blockHandler
兜底方法(sentinel配置)
错误参数:http://localhost:84/consumer/fallback/4快速访问,会进入blockHandler
兜底方法,而慢速访问则会因为参数不正确抛出异常进入handlerFallback
兜底方法
19.9.3 异常特例
exceptionsToIgnore
出现什么异常不降级
exceptionsToTrace
出现什么异常降级
@SentinelResource(value="fallback",fallback = "handlerFallback",exceptionsToIgnore/exceptionsToTrace = {IllegalArgumentException.class} )
19.9.4 -> 熔断之 openFeign 系列:
- 84消费者调用提供者9003
- openFeign一般是消费侧
修改84
- pom: 加入以下openfeign依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
- pom: 加入以下支持
# 激活Sentinel对Feign的支持 feign: sentinel: enabled: true
- 启动类加入注解:
import org.springframework.cloud.openfeign.EnableFeignClients; @EnableFeignClients
- 创建service:
PaymentService
接口//fallback 服务报错降级类 @FeignClient(value = "nacos-payment-provider",fallback = PaymentServiceFallback.class) public interface PaymentService { @GetMapping(value = "/paymentSQL/{id}") Output<Payment> paymentSQL(@PathVariable("id")Long id); }
PaymentServiceFallback
实现接口//千万不要忘记注解 @Component public class PaymentServiceFallback implements PaymentService{ @Override public Output<Payment> paymentSQL(Long id) { return Output.failure(400,"openfeign服务降级返回-->> PaymentServiceFallback"); } }
- 创建controller:
FeignOrderController
@RestController @RequestMapping("/consumer/feign") public class FeignOrderController { @Resource public PaymentService paymentService; @GetMapping("/getSQL/{id}") public Output<Payment> paymentSQL(@PathVariable("id")Long id){ return paymentService.paymentSQL(id); } }
- 测试:http://localhost:84/consumer/feign/getSQL/1
关闭服务端9003,9004后在次测试
19.10 熔断框架比较
- | Sentinel | Hystrix | resilience4j |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔商/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式匀速器模式、预热排队模式 | 不支持 | 简单的Rate Limiter模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控,机器发观等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
19.11 持久化(8401)
这个持久化要跟alibaba的nacos集成的,而且感觉不完整,因为目前只支持简单的流控,降级,热点,系统,授权等还未完善…可以自己写代码完善…
是什么?
一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
怎么玩?
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。
步骤
修改8401:
-
pom: 加入以下依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
-
yml
spring: cloud: sentinel: datasource: #<---------------------------关注点,添加Nacos数据源配置 ds1: nacos: server-addr: localhost:8848 #数据在那个nacos中 dataId: cloudalibaba-sentinel-service #规则名 groupId: DEFAULT_GROUP #规则在那个分组 data-type: json #数据类型 rule-type: flow #规则类型:流动
-
打开nacos:配置规则:
[{ "resource": "byUrl", "IimitApp": "default", "grade": 1, "count": 1, "strategy": 0, "controlBehavior": 0, "clusterMode": false }]
解释: Data ID: 8401 的 spring.application.name
选择JSON,- resource:资源名称;(
@SentinelResource(value)
)的value - limitApp:来源应用;
- grade:阈值类型,0表示线程数, 1表示QPS;
- count:单机阈值;
- strategy:流控模式,0表示直接,1表示关联,2表示链路;
- controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
- clusterMode:是否集群。
- resource:资源名称;(
-
重启8401,访问:http://localhost:8401/sentinel/rate/byURL快速访问,会发现直接被限流
查看sentinel的控制台: 有一条记录
持久化过程: nacos跟项目配置相同规则 --> 启动项目 --> 项目会从nacos拿配置下来 --> 放入sentinel中 --> 服务停止 --> sentinel会把该服务的所有配置清空 --> 服务再次重启 --> 将会再次从nacos拿下来,配置进sentinel
json示例:
[{
"resource": "byURL",
"IimitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
},{
"resource": "customerBlockHandler",
"IimitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}]
当nacos的规则修改之后会自动推送到sentinel中,并不需要重启任何服务或者sentinel
20. SpringCloud Alibaba Seata处理分布式事务
github: https://github.com/seata/seata
下载地址1: https://seata.io/zh-cn/blog/download.html
下载地址2(荐
): https://github.com/seata/seata/tags
20.1 分布式事务问题由来
分布式前
- 单机单库没这个问题
- 从1:1 -> 1:N -> N:N
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三三 个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
20.2 简介
以下很多代码的东西都能在下载的压缩包的conf目录下的README-zh.md
找到解释,可以去看源码
,或者拿sql
,老版本的会在conf下有,新版本的只能去github上自己拿了
-
能干嘛?
一个典型的分布式事务过程
分布式事务处理过程的
一个ID+三组件模型
:Transaction ID XID 全局唯一的事务ID
三组件概念- TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。 - TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。 - RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
- TC (Transaction Coordinator) - 事务协调者
-
处理过程?
TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
XID在微服务调用链路的上下文中传播;
RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
TM向TC发起针对XID的全局提交或回滚决议;
TC调度XID下管辖的全部分支事务完成提交或回滚请求。
-
使用?
本地 @Transactional (spring的)
全局 @GlobalTransactional (springcloud alibaba的)
只需要使用一个 @GlobalTransactional 注解在业务方法上:
20.3 下载 | 安装 | 配置mysql
-
下载
地址: https://github.com/seata/seata/tags(我自己1.4
)
每个版本的配置可能有差异,详情请看 官网参考文档 -
数据源配置为mysql
seata所有配置1.4.2源文件: https://gitee.com/xyy-kk_admin/springcloud-config/tree/master/seata-conf
修改conf下的 file.conf 文件(提前备份)
store{ mode="db" db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" #这里一定要加时区,否则报错(格林+8) #格林威治时间 ( GMT ),这(UTC)是从英国格林威治零经度线上测得的。 url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&serverTimezone=GMT%2B8" user = "root" password = "root" } } service { #事务服务组映射,后缀(_tx_group) vgroupMapping.my_test_tx_group = "xyy_tx_group" #只支持注册时。Type =file,请不要设置多个地址 default.grouplist = "127.0.0.1:8091" #降级,当前不支持 enableDegrade = false #禁用seata disableGlobalTransaction = false }
-
修改
registry.conf
文件(提前备份)registry { type = "nacos" nacos { # nacos注册服务名 application = "seata-server" #nacos地址 serverAddr = "127.0.0.1:8848" #SEATA_GROUP nacos服务名称 group = "SEATA_GROUP" #命名空间 namespace = "" cluster = "default" username = "nacos" password = "nacos" } }
-
mysql创建数据库:
CREATE DATABASE
seata
-
导入表:
看
20.2 简介
第一行SQL获取地址: https://gitee.com/xyy-kk_admin/springcloud-config/tree/master/seata-sql自行导入…
linux 可以先把文件放到服务器中,在脚本目录中登陆mysql
mysql -uroot -p
,然后执行use seata
选择seata数据库执行导入命令source mysql.sql
-
启动服务:
要先启动nacos再启动seata,别搞错了,因为Seata要注册进入nacos
window:
nacos -->startup.cmd -m standalone
seata --> cmd或双击:seata-server.bat
linux
nacos -->sh startup.sh -m standalone
seata -->sh seata-server.sh
-
查看服务:登陆naocs
20.4 订单/库存/账户业务数据准备
以下需要nacos和seata都启动成功配置好mysql!!!
分布式业务说明:
-
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
-
当用户下单时,会在订单服务中创建一个订单, 然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
-
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单—>扣库存—>减账户(余额)。
数据库 and 表 创建步骤:
完整SQL地址: 完整执行SQL即可不看下面的代码跳到20.5 订单/库存/账户微服务准备
数据库结构(成品):
-
创建数据库:
seata_ order:存储订单的数据库;
seata_ storage:存储库存的数据库;
seata_ account:存储账户信息的数据库。CREATE DATABASE seata_order; CREATE DATABASE seata_storage; CREATE DATABASE seata_account;
-
seata_order库下建 t_order 表
USE `seata_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 DEFAULT CHARSET=utf8;
-
seata_storage 库下建 t_storage 表,并插入一条数据
USE `seata_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 DEFAULT CHARSET=utf8; INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0','100');
-
seata_account 库下建 t_account 表,并插入一条数据
USE `seata_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 DEFAULT CHARSET=utf8; INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
-
回滚日志表
官网地址: https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql
订单(order)-库存(storage)-账户(account)3个库
下都需要键各自的回滚日志表# USE `seata_storage`; # USE `seata_account`; USE `seata_order`; CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT '部门事务 id', `xid` VARCHAR(128) NOT NULL COMMENT '全局事务 id', `context` VARCHAR(128) NOT NULL COMMENT '撤消日志上下文,如序列化', `rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息', `log_status` INT(11) NOT NULL COMMENT '0:正常状态,1:防御状态', `log_created` DATETIME(6) NOT NULL COMMENT '创建时间', `log_modified` DATETIME(6) NOT NULL COMMENT '最新修改时间', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
20.5 订单/库存/账户微服务准备
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
20.5.1 订单Order-Module
项目结构:
-
新建子项目
seata-order-service2001
-
pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud</artifactId> <groupId>com.xyy</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-order-service2001</artifactId> <dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--seata 自带1.0.0,用1.4.2改一下--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.4.2</version> </dependency> <!--openfeign--> <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> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </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> </project>
-
yml
tx-service-group的值请去查看
20.3 配置文件的file.conf 下的service
server: port: 2001 spring: application: name: seata-order-service cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: #这个在 conf文件下的 service.vgroupMapping.my_test_tx_group = "xyy_tx_group" tx-service-group: xyy_tx_group datasource: #数据库8.0的配置方式 type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: root #一定要false吧hystrix服务降级关了,否则如果报错全局事务将无法进行 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: type-aliases-package: com.xyy.springcloudalibaba.entity mapperLocations: classpath:mapper/*.xml
-
conf
跟application.yml同级file.conf
注意更改vgroupMapping.
后面的值
disableGlobalTransaction
一定要等于false才是打开事务,true是关闭事务transport { # tcp, unix-domain-socket type = "TCP" #NIO, NATIVE server = "NIO" #enable heartbeat heartbeat = true # the client batch send request enable enableClientBatchSendRequest = true #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size bossThreadSize = 1 #auto default pin or 8 workerThreadSize = "default" } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #transaction service group mapping vgroupMapping.xyy_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses default.grouplist = "127.0.0.1:8091" #degrade, current not support enableDegrade = false #disable seata disableGlobalTransaction = false } client { rm { asyncCommitBufferLimit = 10000 lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true } reportRetryCount = 5 tableMetaCheckEnable = false tableMetaCheckerInterval = 60000 reportSuccessEnable = false sagaBranchRegisterEnable = false sagaJsonParser = jackson sagaRetryPersistModeUpdate = false sagaCompensatePersistModeUpdate = false tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000 } tm { commitRetryCount = 5 rollbackRetryCount = 5 defaultGlobalTransactionTimeout = 60000 degradeCheck = false degradeCheckPeriod = 2000 degradeCheckAllowTimes = 10 interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000 } undo { dataValidation = true onlyCareUpdateColumns = true logSerialization = "jackson" logTable = "undo_log" compress { enable = true # allow zip, gzip, deflater, 7z, lz4, bzip2, default is zip type = zip # if rollback info size > threshold, then will be compress # allow k m g t threshold = 64k } } loadBalance { type = "RandomLoadBalance" virtualNodes = 10 } } log { exceptionRate = 100 }
registry.conf:
都注册进入nacosregistry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" username = "nacos" password = "nacos" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" aclToken = "" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } custom { name = "" } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig、custom type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seata.properties" } consul { serverAddr = "127.0.0.1:8500" aclToken = "" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" nodePath = "/seata/seata.properties" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } custom { name = "" } }
-
实体类entity:
@Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; //订单状态:0:创建中;1:已完结 }
-
接口 OrderMapper 和OrderMapper.xml
import com.xyy.springcloudalibaba.entity.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface OrderMapper { //1, 新建订单 void create(Order order); //2. 修改订单状态,0-->1 Integer update(@Param("id") Long id); }
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.xyy.springcloudalibaba.mapper.OrderMapper"> <resultMap id="BaseResultMap" type="com.xyy.springcloudalibaba.entity.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" useGeneratedKeys="true" keyProperty="id" parameterType="com.xyy.springcloudalibaba.entity.Order"> 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 id = ${id} </update> </mapper>
-
业务类 service:
OrderService:
import com.xyy.springcloudalibaba.entity.Order; public interface OrderService { Long create(Order order); }
AccountFeignService:
import com.xyy.springcloudalibaba.entity.Output; 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 AccountFeignService { //使用说明方式去访问其他服务的方法 @PostMapping(value = "/account/decrease") Output decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
StorageFeignService:
import com.xyy.springcloudalibaba.entity.Output; 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 StorageFeignService { //使用说明方式去访问其他服务的方法 @PostMapping(value = "/storage/decrease") Output decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
OrderServiceImpl:
import com.xyy.springcloudalibaba.entity.Order; import com.xyy.springcloudalibaba.entity.Output; import com.xyy.springcloudalibaba.mapper.OrderMapper; import com.xyy.springcloudalibaba.service.AccountFeignService; import com.xyy.springcloudalibaba.service.OrderService; import com.xyy.springcloudalibaba.service.StorageFeignService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private StorageFeignService storageFeignService; @Resource private AccountFeignService accountFeignService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说:下订单->扣库存->减余额->改状态 */ @Override //@GlobalTransactional //暂时不要启动这个注解 public Long create(Order order) { log.info("----->开始新建订单"); //1 新建订单 orderMapper.create(order); log.info("----->订单id为:" + order.getId()); //2 扣减库存 log.info("----->订单微服务开始调用库存,做扣减Count"); storageFeignService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,做扣减end"); //3 扣减账户 log.info("----->订单微服务开始调用账户,做扣减Money"); accountFeignService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,做扣减end"); //4 修改订单状态,从零到1,1代表已经完成 log.info("----->修改订单状态开始"); orderMapper.update(order.getId()); log.info("----->修改订单状态结束"); log.info("----->下订单结束了,O(∩_∩)O哈哈~"); return order.getId(); } }
-
业务类controller:
OrderController
import com.xyy.springcloudalibaba.entity.Order; import com.xyy.springcloudalibaba.entity.Output; import com.xyy.springcloudalibaba.service.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping("order") public class OrderController { @Resource private OrderService orderService; //http://localhost:2001/order/create?userId=1&productId=1&cout=10&money=100 @GetMapping("create") public Output create(Order order) { Long orderid = orderService.create(order); return Output.success("成功", "订单id: " + orderid); } }
-
配置类:
MyBatisConfig:
import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.xyy.springcloudalibaba.mapper") public class MyBatisConfig { }
DataSourceProxyConfig:
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; /** * 使用Seata对数据源进行代理 */ @Configuration public class DataSourceProxyConfig { //mybatis.mapperLocations在yml里定义的 mapper.xml路径 @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(); } }
-
启动类:
SeataOrderMain2001
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 //注册nacos启动 @EnableFeignClients //feign启动 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除自带的数据源配置,用自己配置的 public class SeataOrderMain2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMain2001.class, args); } }
启动测试没有问题(不要打开order实现类中的那个注解
)
20.5.2 库存 storage-Module
项目结构:
-
创建子项目
seata-storage-service2002
-
pom跟
seata-order-service2001
的pom相同 -
yml
除了这些不一样以外,其他一模一样server: port: 2002 spring: application: name: seata-storage-service datasource: # 数据库变了`seata_storage` url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
-
file.conf
跟registry.conf
跟seata-order-service2001
的一模一样 -
实体类entity:
Storage
@Data @AllArgsConstructor @NoArgsConstructor public class Storage { private Long id; /** * 产品id */ private Long productId; /** * 总库存 */ private Integer total; /** * 已用库存 */ private Integer used; /** * 剩余库存 */ private Integer residue; }
-
mapper :
@Mapper public interface StorageMapper { //扣减库存 Integer decrease(@Param("productId") Long productId, @Param("count") Integer count); }
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.xyy.springcloudalibaba.mapper.StorageMapper"> <resultMap id="BaseResultMap" type="com.xyy.springcloudalibaba.entity.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>
-
业务类 service:
StorageService:
public interface StorageService { //扣减库存 void decrease(Long productId, Integer count); }
StorageServiceImpl:
@Service @Slf4j public class StorageServiceImpl implements StorageService { @Resource private StorageMapper storageMapper; @Override public void decrease(Long productId, Integer count) { log.info("----->开始减库存"); log.info("--->productId="+productId+" count="+count); Integer result = storageMapper.decrease(productId, count); log.info("---->result="+result); log.info("----->结束减库存"); } }
-
业务类controller:
import com.xyy.springcloudalibaba.entity.Output; import com.xyy.springcloudalibaba.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 @RequestMapping("storage") public class StorageController { @Autowired private StorageService storageService; // 扣减库存 @PostMapping("decrease") public Output decrease(Long productId, Integer count) { storageService.decrease(productId, count); return Output.success("扣减库存成功!",null); } }
-
config : 跟
seata-order-service2001
相同 -
启动类 :
SeataStorageMain2002
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; @EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class SeataStorageMain2002 { public static void main(String[] args) { SpringApplication.run(SeataStorageMain2002.class,args); } }
20.5.2 存款Account-Module
项目结构:
-
创建子项目
seata-account-service2003
-
pom跟
seata-order-service2001
的pom相同 -
yml
除了这些不一样以外,其他一模一样server: port: 2003 spring: application: name: seata-account-service datasource: # 数据库变了`seata_storage` url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
-
file.conf
跟registry.conf
跟seata-order-service2001
的一模一样 -
实体类entity:
Account
@Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; /** * 用户id */ private Long userId; /** * 总额度 */ private BigDecimal total; /** * 已用额度 */ private BigDecimal used; /** * 剩余额度 */ private BigDecimal residue; }
-
mapper :
@Mapper public interface AccountMapper { // 扣减账户余额 Integer decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }
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.xyy.springcloudalibaba.mapper.AccountMapper"> <resultMap id="BaseResultMap" type="com.xyy.springcloudalibaba.entity.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>
-
业务类 service:
AccountService:
public interface AccountService { /** * 扣减账户余额 * @param userId 用户id * @param money 金额 */ void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
AccountServiceImpl:
@Service @Slf4j public class AccountServiceImpl implements AccountService { @Resource private AccountMapper accountMapper; /** * 扣减账户余额 */ @Override public void decrease(Long userId, BigDecimal money) { log.info("------->account-service中扣减账户余额开始"); log.info("--->userId:"+userId+" money:"+money); //模拟超时异常(feign默认一秒,没响应就报错),全局事务回滚 //先注释不搞异常 //try { // Thread.sleep(30000); //} catch (InterruptedException e) { // e.printStackTrace(); //} Integer result = accountMapper.decrease(userId,money); log.info("---->result="+result); log.info("------->account-service中扣减账户余额结束"); } }
-
业务类controller:
@RestController @RequestMapping("/account") public class AccountController { @Resource private AccountService accountService; /** * 扣减账户余额 */ @PostMapping("/decrease") public Output decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){ accountService.decrease(userId,money); return Output.success("扣减账户余额成功!",null); } }
-
config : 跟
seata-storage-service2002
相同 -
启动类 :
SeataAccountMain2003
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; @EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class SeataAccountMain2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMain2003.class,args); } }
20.6 整体完成测试
-
启动服务:
启动 nacos
启动 seata
启动seata-order-service2001
订单服务
启动seata-storage-service2002
库存服务
启动seata-account-service2003
账户服务 -
访问http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
用户1,买商品id为1,买10件,一共100元 -
结果:
页面结果:
2001(订单)控制台:
数据库:
2002(库存)控制台:
数据库:
2003(账户)控制台:
数据库:
可以看到数据完美没有出错
20.7 设置异常测试
- 将2003连接超时
将2003的实现类
AccountServiceImpl
线程休眠打开 - 重启2003,再次访问地址测试
2001(订单)数据库:订单状态还在进行中:
2002(库存)数据库:库存已出
2003(账户)数据库:被扣款(30秒后的结果)
可以看到订单还在创建中,并没有结束,假如2003在扣款前抛出异常的话,订单有了,库存已减,就是亏损了啊
20.8 事务解决异常进行回滚
-
在所有方法最开始调用的地方加上注解:
@GlobalTransactional
也就是在seata-order-service2001
的OrderServiceImpl
类的create
方法上:public class OrderServiceImpl implements OrderService { @GlobalTransactional public Long create(Order order) { ... } }
-
重启
seata-order-service2001
, 这时2001控制台可能会反复一直报错:2021-07-09 14:14:19.682 ERROR 4688 — [ileListener_3_1] io.seata.config.FileConfiguration : fileListener execute error, dataId :service.disableGlobalTransaction
不用管,这个好像是senta 1.4.2
的bug:官网问答中不止我一个人是这样的 -
再次访问: http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
还是500,查看数据库:2001(订单)数据库:
2002(库存)数据库:
2003(账户)数据库:
-
结果:
查看2002控制台和2003控制台发现并不是没有执行访问,而是执行访问之后报错,但是数据库并没有订单和库存被扣除的数据,这就是Seata的全局事务注解
@GlobalTransactional
20.9 深度总结**
seata 简介:
各种模式介绍:https://seata.io/zh-cn/docs/overview/what-is-seata.html
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
TC/TM/RM三大组件:
分布式事务的执行流程
- TM开启分布式事务(TM向TC注册全局事务记录) ;
- 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态) ;
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务) ;
- TC汇总事务信息,决定分布式事务是提交还是回滚;
- TC通知所有RM提交/回滚资源,事务二阶段结束。
AT模式如何做到对业务的无侵入
-
基于支持本地 ACID 事务的关系型数据库。
-
Java 应用,通过 JDBC 访问数据库。
整体机制两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
在一阶段,Seata会拦截“业务SQL”
- 解析SQL语义,找到“业务SQL" 要更新的业务数据,在业务数据被更新前,将其保存成"before image”
- 执行“业务SQL" 更新业务数据,在业务数据更新之后,
- 其保存成"after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
-
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。
-
回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。
-
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。