降级方法的参数跟原方法的原始值相同
编码五部曲:
好的环境配置比代码更重要
入门篇链接: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
官网: 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。
服务限流降级:默认支持Servlet、Feign、RestTemplate、Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics 监控。
服务注册与发现:适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于Spring Cloud Stream为微服务应用构建消息驱动能力。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker (schedulerx-client)上执行。
官网: https://spring.io/projects/spring-cloud-alibaba#learn
中文网: https://nacos.io/zh-cn/
下载地址: https://github.com/alibaba/nacos/tags 自己寻找版本下载,(我自己2.0.2
)
为什么叫 Nacas?
是什么?
能干嘛?
框架对比
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本闭源) |
Zookeeper | CP | 不支持 | 中 |
consul | CP | 支持 | 高 |
Nacos | AP | 支持 | 高 |
据说Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。
本地 Java8 与 maven 环境 ok !!
下载地址: https://github.com/alibaba/nacos/tags 自己寻找版本下载,(我自己2.0.2
,目前推荐的最稳定的版本)
解压进入bin目录,启动startup.cmd
(直接启动是集群环境会报错…)
打开cmd命令窗口,输入: startup.cmd -m standalone
启动单机模式
注: wdnmd(唯独你没懂),第一次加载时间超长,可以去吃个饭再回来…
spring-cloud alibaba 官网都有的
新建子项目cloudalibaba-provider-payment9001
pom
父pom:(应该已经有了)
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
9001pom:
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-provider-payment9001artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
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:
-DServer.port=9002
没空格有点,我没写错cloudalibaba-consumer-nacos-order80
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-consumer-nacos-order80artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.xyygroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
project>
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();
}
}
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);
}
}
Nacos全景图:
Nacos和CAP
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
新建子项目cloudalibaba-config-nacos-client3377
pom
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-config-nacos-client3377artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
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中匹配)
修改nacos配置中心内容将nacos-config-client-dev.yaml
文件的版本号从1改为2,发布,重新刷新3377页面,会发现直接修改成功
问题 - 多环境多项目管理
实际开发中,通常一个系统会准备?
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
官网: https://nacos.io/zh-cn/docs/deployment.html
我们需要mysql数据库:
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
单机模式支持mysql
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持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,nacos所有写嵌入式数据库的数据都写到了mysql。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生产环境配置:
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
启动nacos
修改cloudalibaba-provider-payment9002
的yml
spring:
cloud:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
server-addr: nginx运行服务器的ip:1234
启动测试
gibhub: https://github.com/alibaba/Sentinel
官网文档1: https://sentinelguard.io/zh-cn/
官网文档2:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
github文档: https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
下载地址: https://github.com/alibaba/Sentinel/tags
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 等应用容器。
下载地址: https://github.com/alibaba/Sentinel/tags
运行java -jar sentinel1.8.2.jar
,默认端口8080(需要java8)
web访问地址: http://localhost:8080
startup.cmd -m standalone
java -jar sentinel1.8.jar
cloudalibaba-sentinel-service8401
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-sentinel-service8401artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.xyygroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
project>
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
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后再查看sentinel的页面,否则是空的,因为sentinel是懒加载
模式,并且如果一段时间不访问方法,则不会显示该方法
基本介绍:
直接:
再次访问http://localhost:8401/sentinel/flow/A,快速刷新,结果500
(因为配置了@SentinelResource
,否则页面显示Blocked by Sentinel (flow limiting)
,我不配置这个直接加载不到sentinel控制台)
不管是500
还是Blocked by Sentinel (flow limiting)
,都不是想要的,这时候需要自定义报错页面
,之后讲…
流控规则为并发线程数: 并发情况下,超过指定数量以外的会报错
testA方法修改:
@RequestMapping("A") public String testA(){ ThreadUtil.sleep(800); return "---A"; }
疯狂刷新http://localhost:8401/sentinel/flow/A,结果也会报错
关联:
快速失败
直接失败: 抛出异常
预热:
公式: 阈值 / 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 的场景。
官网: 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,然后重新统计时间
官网: 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秒
结果: 访问无参,带参数p2,疯狂刷新没有问题,而全参带p1和p2与带p1的请求如果触碰到热点规则
则会熔断
注: 支持基本类型和String类型,参数的值=指定值,限流阈值更改(中文需要URL编码改一下)
将testHotKey
方法抛异常,加入以下代码int age =10/0;
运行测试???
将会直接500,并不会进入兜底方法
注:
@SentinelResource - 处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
RuntimeException int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管
后面会讲fallback
可以配
官网: https://github.com/alibaba/Sentinel/wiki/系统自适应限流
简介:
支持模式:
上述兜底方案面领的问题:
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");
}
}
// blockHandlerClass = 兜底类,blockHandler = 兜底类中的那个方法?
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
@GetMapping("/customerBlockHandler")
public Output byURL1(){
return Output.success("自定义 限流成功",new Payment(2020L,"serial001"));
}
地址: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 函数签名和位置要求:
defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
sentinel 整合 ribbon + openFeign + fallback
两种降级方法
fallback
运行异常降级配置
blockHandler
sentinel控制台异常降级配置
@SentinelResource(value = "test",blockHandler = "blockHandler降级方法",fallback ="fallback降级方法")
准备三个子项目:
cloudalibaba-provider-payment9003
服务提供者
cloudalibaba-provider-payment9004
服务提供者
cloudalibaba-consumer-nacos-order84
服务消费者
服务提供者:
cloudalibaba-provider-payment9003
和 cloudalibaba-provider-payment9004
的集群
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-provider-payment9003artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
dependency>
<dependency>
<groupId>com.xyygroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
dependencies>
project>
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
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
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloudalibaba-consumer-nacos-order84artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.xyygroupId>
<artifactId>cloud-api-commonsartifactId>
<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;
}
}
只加fallback
@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());
}
}
只加 blockHandler
@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());
}
}
1
快速刷新会进入兜底方法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());
}
}
测试:
参数正确: http://localhost:84/consumer/fallback/1快速访问会进入blockHandler
兜底方法(sentinel配置)
错误参数:http://localhost:84/consumer/fallback/4快速访问,会进入blockHandler
兜底方法,而慢速访问则会因为参数不正确抛出异常进入handlerFallback
兜底方法
exceptionsToIgnore
出现什么异常不降级
exceptionsToTrace
出现什么异常降级
@SentinelResource(value="fallback",fallback = "handlerFallback",exceptionsToIgnore/exceptionsToTrace = {IllegalArgumentException.class} )
修改84
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
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");
}
}
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);
}
}
- | Sentinel | Hystrix | resilience4j |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔商/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式匀速器模式、预热排队模式 | 不支持 | 简单的Rate Limiter模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控,机器发观等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
这个持久化要跟alibaba的nacos集成的,而且感觉不完整,因为目前只支持简单的流控,降级,热点,系统,授权等还未完善…可以自己写代码完善…
是什么?
一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
怎么玩?
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。
步骤
修改8401:
pom: 加入以下依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
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 #规则类型:流动
[{
"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:是否集群。
重启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
官网: https://seata.io/zh-cn/
github: https://github.com/seata/seata
下载地址1: https://seata.io/zh-cn/blog/download.html
下载地址2(荐
): https://github.com/seata/seata/tags
分布式前
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三三 个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
以下很多代码的东西都能在下载的压缩包的conf目录下的README-zh.md
找到解释,可以去看源码
,或者拿sql
,老版本的会在conf下有,新版本的只能去github上自己拿了
能干嘛?
一个典型的分布式事务过程
分布式事务处理过程的 一个ID+三组件模型
:
Transaction ID XID 全局唯一的事务ID
三组件概念
处理过程?
TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
XID在微服务调用链路的上下文中传播;
RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
TM向TC发起针对XID的全局提交或回滚决议;
使用?
本地 @Transactional (spring的)
全局 @GlobalTransactional (springcloud alibaba的)
只需要使用一个 @GlobalTransactional 注解在业务方法上:
官网: https://seata.io/zh-cn/
下载
地址: 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 可以先把文件放到服务器中,在脚本目录中登陆mysqlmysql -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
以下需要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';
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
新建子项目seata-order-service2001
pom
<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>cloudartifactId>
<groupId>com.xyygroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>seata-order-service2001artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.4.2version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
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:
都注册进入nacos
registry {
# 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:
output 实体类点我
@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: 按照自己的路径修改
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实现类中的那个注解
)
创建子项目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:
output 实体类点我
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
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);
}
}
创建子项目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:
output 实体类点我
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
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);
}
}
启动服务:
启动 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元
数据库:
可以看到数据完美没有出错
将2003的实现类
AccountServiceImpl
线程休眠打开
订单状态还在进行中:
库存已出
被扣款(30秒后的结果)
可以看到订单还在创建中,并没有结束,假如2003在扣款前抛出异常的话,订单有了,库存已减,就是亏损了啊
在所有方法最开始调用的地方加上注解:@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,查看数据库:
结果:
查看2002控制台和2003控制台发现并不是没有执行访问,而是执行访问之后报错,但是数据库并没有订单和库存被扣除的数据,这就是Seata的全局事务注解@GlobalTransactional
seata 简介:
各种模式介绍:https://seata.io/zh-cn/docs/overview/what-is-seata.html
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
AT模式如何做到对业务的无侵入
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
整体机制
两阶段提交协议的演变:
在一阶段,Seata会拦截“业务SQL”
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。
回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。