由 杨柳依 于2020年3月30日创建,最近更新于 2020年4月6日。 参考资料:尚硅谷周阳SpringCloud教程
Spring Cloud Alibaba
服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
Spring Cloud Alibaba Nacos 服务注册和配置中心
1. 简介与安装
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos: Dynamic Naming and Configuration Service
注册中心 + 配置中心,Nacos = Eureka + Config + Bus
下载nacos-server-1.1.4.zip,解压,在进入到解压后的文件夹后,运行命令:
bin\startup.cmd
访问http://localhost:8848/nacos,默认账号和密码是`nacos`
2. 作为服务注册中心
使用之前,需要在父POM中添加依赖:
<dependencyManagement><dependencies><!-- 省略前面已经添加的 --><!--spring cloud alibaba 2.1.0.RELEASE--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
基于Nacos的服务提供者
新建模块cloudalibaba-provider-payment9001,添加依赖:
<dependencies><!--SpringCloud Alibaba Nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- SpringBoot整合Web组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--日常通用jar包配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
新建配置文件application.yml:
server:port: 9001spring:application:name: nacos-payment-providercloud:nacos:discovery:server-addr: localhost:8848 # Nacos地址# 暴露监控端点management:endpoints:web:exposure:include: '*'
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类PaymentMain9001:
@SpringBootApplication@EnableDiscoveryClientpublic class PaymentMain9001 {public static void main(String[] args) {SpringApplication.run(PaymentMain9001.class, args);}}
在包alibaba下新建控制类controller.PaymentController:
@RestControllerpublic class PaymentController {@Value("${server.port}")private String serverPort;@GetMapping(value = "/payment/nacos/{id}")public String getPayment(@PathVariable("id") Integer id) {return "nacos registry, serverPort: " + serverPort + "\t id: " + id;}}
即可开始测试。首先保证Nacos是处于运行状态,启动本模块,访问http://localhost:9001/payment/nacos/1,正确返回结果。
访问在http://localhost:8848/nacos管理界面,可以看到服务成功注册。
为演示负载均衡功能,仿照9001新建模块cloudalibaba-provider-payment9002。
基于Nacos的服务消费者
新建模块cloudalibaba-consumer-nacos-order83,POM依赖与9001一样。
新建配置文件application.yml:
server:port: 83spring:application:name: nacos-order-consumercloud:nacos:discovery:server-addr: localhost:8848#消费者将要去访问的微服务名称(注册成功进Nacos的微服务提供者)service-url:nacos-user-service: http://nacos-payment-provider
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类OrderNacosMain83:
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class, args);
}
}
在包alibaba下新建设置类config.ApplicationContextConfig:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced // 一定要加才能负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
在包alibaba下新建控制类controller.OrderNacosController:
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}") // 之前写到配置文件里了
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id) {
return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
}
}
在前面的基础上启动该模块,多次访问http://localhost:83/consumer/payment/nacos/1,可以看到负载均衡。
注册中心对比
Nacos服务发现实例模型
- 临时实例(AP)
- 客户端上报健康状态
- 摘除不健康实例
- 非持久化
- 持久化实例(CP)
- 服务端探测健康状态
- 保留不健康实例
- 持久化
Nacos与其它注册中心对比
| 功能 | Nacos | Eureka | Consul | CoreDNS | Zookeeper |
|---|---|---|---|---|---|
| 一致性协议 | CP/AP | AP | CP | / | CP |
| 健康检查 | TCP/HTTP/MySQL/Client Beat | Client Beat | TCP/HTTP/gRPC/Cmd | / | Client Beat |
| 负载均衡 | 权重/DSL/metadata/CMDB | Ribbon | Fabio | RR | / |
| 雪崩保护 | √ | 支持 | × | × | × |
| 自动注销实例 | √ | √ | × | × | √ |
| 访问协议 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
| 监听支持 | √ | √ | √ | × | √ |
| 多数据支持 | √ | √ | √ | × | × |
| 跨注册中心 | √ | × | √ | × | × |
| SpringCloud集成 | √ | √ | √ | × | × |
| Dubbo集成 | √ | × | × | × | √ |
| K8s集成 | √ | × | √ | √ | × |
- Zookeeper不支持SpringCloud,是SpringCloud主动适配的Zookeeper。
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"
3. 作为服务配置中心
基础配置
新建模块cloudalibaba-config-nacos-client3377,添加依赖:
<dependencies>
<!-- nacos-config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- web+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 一般基础配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
新建配置文件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 #指定配置文件的格式
新建配置文件application.yml:
spring:
profiles:
active: dev # 表示处于开发环境
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类NacosConfigClientMain3377:
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
在包alibaba下新建控制类controller.ConfigClientController:
@RestController
@RefreshScope // 支持Nacos的动态刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
接下来就是在Nacos中添加配置信息了【重点】
Nacos中的匹配规则:
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
本模块的name为nacos-config-client,环境为dev,配置文件格式为yaml,即nacos-config-client-dev.yml。
在http://localhost:8848/nacos的配置列表中新增配置,Data ID为nacos-config-client-dev.yaml,配置格式选yaml,配置内容填:
config:
info: "config info for dev, version=1"
注意文件名与后缀严格相等,前面写的是yaml,这里文件名应该是yaml。 建议不要使用Firefox,会有bug,使用Chrome访问。
启动本模块,访问http://localhost:3377/config/info,可以看到配置内容。修改配置,再次访问会发现内容会**自动刷新**。
分类配置
问题1:实际开发中,通常一个系统会准备 dev开发环境、test测试环境、prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢? 问题2:一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目仅都会有相应的开发环境、测试环境、预发环境、正式环境等。那怎么对这些微服务配置进行管理呢?
Namespace + Group + Data ID。Namespace用于区分部署环境、Group和Data ID用于逻辑上区分两个目标对象。
不同的Namespace是隔离的,Group可以把不同的微服务划分到同一个分组里面去。
默认:Namespace=public,Group=DEFAULT_GROUP,Cluster=DEFAULT。
Data ID方案
指定spring.profile.active和配置文件的Data ID来读取不同环境下的配置
在Nacos配置列表中新建test环境的配置nacos-config-client-test.yaml,修改3377的配置文件application.yml:
spring:
profiles:
active: test # 表示处于测试环境
重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。
Group方案
在Nacos配置列表中新建dev环境的配置nacos-config-client-info.yaml,Group为DEV_GROUP:
config:
info: "DEV_GROUP, nacos-config-client-info.yaml, version=1"
新建test环境的配置nacos-config-client-info.yaml,Group为TEST_GROUP:
config:
info: "TEST_GROUP, nacos-config-client-info.yaml, version=1"
修改3377的配置文件bootstrap.yml,在spring.cloud.nacos.config下新增配置:
config:
group: TEST_GROUP
修改3377的配置文件application.yml:
spring:
profiles:
active: info
重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。
Namespace方案
在Nacos管理界面新建2个命名空间dev和test,在dev中新建配置nacos-config-client-info.yaml:
config:
info: "namespace: dev, nacos-config-client-info.yaml, version=1"
修改3377的配置文件bootstrap.yml,在spring.cloud.nacos.config下新增配置:
config:
# group: TEST_GROUP # 注释掉分组
namespace: 6c10caf3-ba7c-4c1e-9f3f-10d029e25b57 # 为namespace dev对应的id
重启模块,访问http://localhost:3377/config/info,可以看到修改后的测试环境的配置文件。
4. 集群和持久化配置
持久化到MySQL
默认Nacos使用嵌入式数据库Derby实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Apache Derby是一个完全用java编写的数据库,非常小巧,核心部分derby.jar只有2M,所以既可以做为单独的数据库服务器使用,也可以内嵌在应用程序中使用。
在nacos/conf/下有一个nacos-mysql.sql脚本,在mysql中新建数据库nacos_config,复制脚本内容并执行。
复制以下内容并粘到nacos/conf/application.properties中:
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
重启Nacos,可以发现前面配置的东西都没有了,说明切换数据库成功。在nacos添加一条配置,在数据库his_config_info表内可以看到数据。
Linux Nacos安装
预计需要 1个Nginx + 3个Nacos + 1个MySQL。
下载nacos-server-1.1.4.tar.gz到linux主机/opt/,在该路径下运行命令:
tar -xzvf nacos-server-1.1.4.tar.gz
mkdir /mynacos
cp -r nacos /mynacos
cd /mynacos/nacos/bin
cp startup.sh startup.sh.bk
cd /mynacos/nacos/conf
cp application.properties application.properties.bk
cp cluster.conf.example cluster.conf
进入MySQL,运行命令:
# 创建数据库
create database `nacos_config` character set 'utf8' collate 'utf8_general_ci';
# 选择数据库
use nacos_config;
# 执行sql文件脚本
source /mynacos/nacos/conf/nacos-mysql.sql
# 也可以不进入mysql在命令行执行脚本
mysql -uroot -p nacos_config < /mynacos/nacos/conf/nacos-mysql.sql
编辑conf/路径下的配置文件application.properties,添加内容:
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
修改集群配置文件cluster.conf:
# 这里的ip必须是本地执行 hostname -i 显示出来的ip
127.0.0.1:3333
127.0.0.1:4444
127.0.0.1:5555
改启动脚本startup.sh,使其能够接受不同的启动端口:
# 57行
while getopts ":m:f:s:p:" opt
do
case $opt in
m)
MODE=$OPTARG;;
f)
FUNCTION_MODE=$OPTARG;;
s)
SERVER=$OPTARG;;
p)
PORT=$OPTARG;;
?)
echo "Unknown parameter"
exit 1;;
esac
done
# 134行
nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 &
【TODO】
Spring Cloud Alibaba Sentinel 实现熔断与限流
1. 简介与安装
面向云原生微服务的流量控制、熔断降级组件。Hystrix与Sentinel对比:
| Hystrix | Sentinel |
|---|---|
| 1. 需要手工搭建监控平台 | 1. 单独一个组件,可以独立出来 |
| 2. 没有web界面可以进行细粒度化的配置实现流量控制、速率控制、服务熔断、服务降级 | 2. 界面化的细粒度统一配置 |
Hystrix
Github上下载sentinel-dashboard-1.7.1.jar,在下载路径运行java -jar sentinel-dashboard-1.7.1.jar,默认端口为8080,访问http://localhost:8080,默认用户名和密码是`sentinel`。
2. 使用
新建一个模块cloudalibaba-sentinel-service8401,添加依赖:
<dependencies>
<!--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>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web组件+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加配置application.yml:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
# 配置dashboard地址
dashboard: localhost:8080
# 默认8719端口,端口被占用会依次往后+1扫描到能用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类MainApp8401:
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
在包alibaba下新建控制类controller.FlowLimitController:
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
log.info(Thread.currentThread().getName()+"\t "+ "...testA");
return "-----------testA----------";
}
@GetMapping("/testB")
public String testB() {
return "-----------testB----------";
}
}
先启动nacos、sentinel,再启动本模块,打开sentinel管理界面,发现空空如也,原因是sentinel采用懒加载机制,访问一次服务就会把服务加载上去。
3. 流控规则
基本介绍
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel 可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
- 是否集群:不需要集群
- 流控模式
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)[api级别的针对来源]
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
流控模式
直接(默认)
QPS:对/testA新增流控规则,QPS为1,再访问http://localhost:8401/testA,可以发现每秒一次访问可以成功,狂点则返回默认错误`Blocked by Sentinel (flow limiting)`。后续会改进fallback方法,返回自定义内容。
线程数:修改testA方法,增加sleep。对/testA修改流控规则,线程数为1,再访问http://localhost:8401/testA,可以发现慢速点可以成功,狂点则返回默认错误,原因是前面的线程还没完成访问,会开新的线程,被限流。
@GetMapping("/testA")
public String testA() {
// 暂停
try {
TimeUnit.MILLISECONDS.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "-----------testA----------";
}
关联
比如当与A关联的资源B达到阈值后,就限流A自己。恢复testA方法。对/testA修改流控规则,关联资源为/testB,QPS为1。
用postman模拟并发访问testB,新建get请求http://localhost:8401/testB,保存到Collection中,然后点Run,指定Iterations为20,Delay为300,意思是20个线程每次间隔0.3s访问1次,然后运行。浏览器访问testA,发现A返回默认错误。
链路
TODO
流控效果
快速失败
直接失败,抛异常。源码为com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController,在sentinel-core-1.6.3.jar中。
Warm Up(预热)
公式:阈值除以coldFactor(默认3),经过预热时长后才会达到阈值。对/testA修改流控规则,流控模式为直接,QPS为5,流控效果为Warm Up,预热时长为3,意思是刚开始的QPS阈值为5/3=1,3s后阈值达到5。访问testA,发现刚开始狂点会失败,后面狂点会正常返回结果。
排队等待
阈值类型只能是QPS,对/testA修改流控规则,流控模式为直接,QPS为1,流控效果为排队等待,超时时间为20000。
用postman模拟并发访问testA,保存到Collection中,然后点Run,指定Iterations为10,Delay为100,然后运行。观察打印日志,可以发现都能访问成功,但是会变成1秒1个请求(前面设置的QPS)。
4. 降级规则
基本介绍
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。
Sentinel的断路器没有半开状态。取而代之的是时间窗口。
降级策略:
- RT:平均响应时间,秒级
平均响应时间超出阈值 且 在时间窗口内通过的请求≥5,两个条件同时满足后触发降级。窗口期过后关闭断路器,RT最大值为4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXX才能生效) - 异常比例:秒级
QPS≥5 且 异常比例(秒级统计)超过阈值时,触发降级。时间窗口期结束后关闭降级。 - 异常数:分钟级
异常数(分钟统计)超过阈值时,触发降级。时间窗口期结束后关闭降级。
RT
对FlowLimitController类新增方法testD:
@GetMapping("/testD")
public String testD() {
// 暂停
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"\t "+ "...testD 测试RT");
return "-----------testA----------";
}
在sentinel界面新增降级规则/testD,降级策略为RT,阈值为200,时间窗口为1。意思是每个请求必须要在200ms内完成,时间窗口为1s。使用Postman进行压测,100个请求,delay为100ms。再浏览器访问testD,可以发现服务降级了。
异常比例
阈值范围为[0.0, 1.0]。对FlowLimitController类新增方法testE:
@GetMapping("/testE")
public String testE() {
int a = 10 / 0;
log.info(Thread.currentThread().getName() + "\t " + "...testE 测试异常比例");
return "-----------testE 测试异常比例----------";
}
在sentinel界面新增降级规则/testE,降级策略为异常比例,阈值为0.2,时间窗口为1。单独访问发现每次都报错,使用Jmeter进行压测,每秒10个请求,再浏览器访问testD,可以发现服务降级了。
异常数
此时时间窗口一定要≥60s。对FlowLimitController类新增方法testF:
@GetMapping("/testF")
public String testF() {
int a = 10 / 0;
log.info(Thread.currentThread().getName() + "\t " + "...testF 测试异常数");
return "-----------testE 测试异常数----------";
}
在sentinel界面新增降级规则/testF,降级策略为异常数,阈值为5,时间窗口为70。浏览器多次访问testF, 发现从第6次开始触发服务降级。
5. 热点key限流
基本介绍
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制。比如:
- 商品ID为参数,统计—段时间内最常购买的商品ID并进行限制
- 用户ID为参数,针对一段时间内频繁访问的用户ID进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel Parameter Flow Control Sentinel利用LRU策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
热点参数限流支持集群模式。
配置
使用@SentinelResource可以实现自己的兜底方法。
对FlowLimitController类新增方法:
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p1", required = false) String p2) {
return "---------testHotKey";
}
// 兜底方法
public String deal_testHotKey(String p1, String p2, BlockException exception) {
return "---------deal_testHotKey,兜底方法";
}
接着新增热点规则。资源名为testHotKey,参数索引为0(顺序以Java代码中的索引为准),单击阈值为1,窗口期为1。
- 快速点击http://localhost:8401/testHotKey?p1=a,可以发现返回兜底方法。
- 快速点击http://localhost:8401/testHotKey,可以发现返回正常内容。
- 快速点击http://localhost:8401/testHotKey?p2=b,可以发现返回正常内容。
此外,用了热点规则限流,请一定要实现自定义兜底方法,否则会返回Error Page。
参数例外项
期望当p1的值为某个特殊值时,采用另外的限流规则。
修改testHotKey热点规则,添加参数例外项,参数类型为String,参数值为5,阈值为200。
- 快速点击http://localhost:8401/testHotKey?p1=1,可以发现返回兜底方法。
- 快速点击http://localhost:8401/testHotKey?p1=5,可以发现返回正常内容。
注意,Sentinel主管管理界面设置的配置出错,运行出错照样走异常。
6. 系统规则
Sentinel系统自适应限流从整体维度对应用入口流量进行控制,结合应用的Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如Web服务或Dubbo服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
- Load 自适应(仅对Linux/Unix-like机器生效):系统的load1作为启发指标,进行自适应系统保护。当系统load1超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的maxQps.minRt估算得出。设定参考值一般是CPU cores*2.5。
- CPU usage(1.5.0+版本):当系统CPU使用率超过阈值即触发系统保护(取值范围0.0-1.0),比较灵敏。
- 平均RT:当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口QPS:当单台机器上所有入口流量的QPS达到阈值即触发系统保护。
7. @SentinelResource
cloudalibaba-sentinel-service8401模块添加依赖:
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
增加业务类RateLimitController:
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
@GetMapping("/rateLimit/byURL")
@SentinelResource(value = "byURL", blockHandler = "handleException")
public CommonResult beyURL() {
return new CommonResult(200, "按URL限流测试OK", new Payment(2020L, "serial002"));
}
public CommonResult handleException(BlockException e) {
return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
}
}
启动模块,新建流控规则byResource,阈值QPS=1。快速点击/byResource,返回自定义兜底方法。
新建流控规则/rateLimit/byURL,阈值QPS=1。快速点击/rateLimit/byURL,发现还是返回默认流控信息。
按url限流只会返回默认流控信息。关闭模块,发现限流规则消失,说明限流规则是临时的。
自定义限流处理逻辑
RateLimitController类添加方法:
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客户自定义OK", new Payment(2020L, "serial002"));
}
创建myhandler.CustomerBlockHandler类用于自定义限流处理逻辑:
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(444, "按客户自定义,全局兜底方法---1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(444, "按客户自定义,全局兜底方法---2");
}
}
狂点http://localhost:8401/rateLimit/customerBlockHandler,发现返回自定义兜底方法的内容。
8. 服务熔断—Ribbon
Ribbon
新建模块cloudalibaba-provider-payment9003,添加依赖:
<dependencies>
<!--SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加配置application.yml:
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos地址
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: '*'
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类PaymentMain9003:
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
新建业务类controller.PaymentController:
@RestController
public class PaymentController {
public static HashMap<Long, Payment> hashMap = new HashMap<>();
// 模拟数据库数据
static {
hashMap.put(1L, new Payment(1L, "111111111111111111111111111"));
hashMap.put(2L, new Payment(2L, "222222222222222222222222222"));
hashMap.put(3L, new Payment(3L, "333333333333333333333333333"));
}
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
return new CommonResult<>(200, "from mysql, serverPort:" + serverPort, payment);
}
}
启动9003,访问http://localhost:9003/paymentSQL/{id},分别尝试参数为1/2/3/4,看看是否正常。
仿照9003新建模块cloudalibaba-provider-payment9004,除了配置文件端口和主启动类需要修改,其它不动。
新建消费端模块cloudalibaba-consumer-nacos-order84,添加依赖:
<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>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加配置application.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
新建包com.atguigu.springcloud.alibaba,在包alibaba下新建主启动类OrderNacosMain84:
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
新建配置类config.ApplicationContextConfig:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced // 一定要加才能负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
【核心】新建业务类controller.CircleBreakerController:
@RestController
@Slf4j
public class CircleBreakerController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}") // 之前写到配置文件里了
private String serverURL;
@GetMapping(value = "/consumer/fallback/{id}")
@SentinelResource(value = "fallback") // 没有配置
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
CommonResult<Payment> result = restTemplate.getForObject(serverURL + "/paymentSQL/" + id, CommonResult.class);
if (id == 4) {
throw new IllegalArgumentException("参数非法异常");
} else if (result.getData() == null) {
throw new NullPointerException("ID没有对应记录,空指针异常");
}
return result;
}
}
启动9003、9004、84,访问http://localhost:84/consumer/fallback/1,为轮询。
没有配置
访问http://localhost:84/consumer/fallback/4,会报Error Page,体验不友好。
配置fallback方法
修改fallback的注解,在类中添加方法:
// @SentinelResource(value = "fallback") // 没有配置
@SentinelResource(value = "fallback", fallback = "handleFallback") // fallback只管业务异常
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
// ...
}
// 本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
重启模块,访问http://localhost:84/consumer/fallback/4,会返回友好出错信息。
配置blockHandler方法
修改fallback的注解,在类中添加方法:
@GetMapping(value = "/consumer/fallback/{id}")
// @SentinelResource(value = "fallback") // 没有配置
// @SentinelResource(value = "fallback", fallback = "handlerFallback") // fallback只管业务异常
@SentinelResource(value = "fallback", blockHandler = "blockHandler") // blockHandler只管sentinel控制台违规
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
// ...
}
// 本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
重启模块,访问http://localhost:84/consumer/fallback/4,会报Error Page。在sentinel控制台对资源fallback添加降级规则,异常数为2,时间窗口为1s,狂点/consumer/fallback/4,会有blockHandler方法进行兜底。
2种都配置
修改fallback的注解:
@GetMapping(value = "/consumer/fallback/{id}")
// @SentinelResource(value = "fallback") // 没有配置
// @SentinelResource(value = "fallback",) // fallback只管业务异常
// @SentinelResource(value = "fallback", blockHandler="blockHandler") // blockHandler只管sentinel控制台违规
@SentinelResource(value="fallback", fallback="handlerFallback", blockHandler="blockHandler") // 2种都配
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
// ...
}
重启模块,访问http://localhost:84/consumer/fallback/1,在sentinel控制台对资源`fallback`添加流控规则,QPS为1。
快速点击http://localhost:84/consumer/fallback/4,发现blockHandler是优先于fallback的。
即2个都配的话,出现异常是进入blockHandler处理逻辑的。
忽略异常
@SentinelResource有属性exceptionsToIgnore 用于忽略某些异常,如:
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class, NullPointerException.class})
传入需要忽略的异常类的数组,可以忽略处理这些异常类型。
9. 服务熔断—Feign
修改模块cloudalibaba-consumer-nacos-order84,引入OpenFeign(上面已提前引入)。在配置文件中激活Sentinel对Feign的支持(上面已激活)。在主启动类OrderNacosMain84上添加注解@EnableFeignClients激活OpenFeign。
新建接口service.PaymentService:
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping("/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
新建兜底类service.PaymentFallbackService:
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,PaymentFallbackService");
}
}
在CircleBreakerController中添加成员变量和方法:
// OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
重启模块,访问http://localhost:84/consumer/paymentSQL/1,轮询返回结果。
然后关掉9003和9004,发生服务降级返回。
10. 规则持久化
重启模块之后,规则会消失。
将配置规则持久化进Nacos保存,只要刷新84某个REST地址就能获取到。
cloudalibaba-consumer-nacos-order84添加依赖:
<!-- Sentinel规则持久化进Nacos-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改配置文件application.yml,在sentinel节点下新增:
# sentinel规则持久化进行nacos
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
然后在nacos配置列表界面新增一条配置,Data ID:nacos-order-consumer,Group:DEFAULT_GROUP,配置格式:json,配置内容:
[
{
"resource": "GET:http://nacos-payment-provider/paymentSQL/{id}",
"limitApp": "default",
"grade": 1,
"count": 1,
"stategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
注意,不支持配置规则直接写入到nacos中(除非用Java代码)。
Spring Cloud Alibaba Seata 处理分布式事务
1. 基本介绍
分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
基本概念
Transaction ID XID:全局唯一的事务ID
三组件模型:
- Transaction Coordinator(TC):事务协调者
事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚 - Transaction Manager(TM):事务管理器
控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议 - Resource Manager(RM):资源管理器
控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
2. Seata Server安装
github上下载seata-server-1.0.0.zip,解压。
进入conf/路径下修改配置文件file.conf(拿file.conf.example的内容覆盖拷贝到file.conf中):
- 修改service模块,将
vgroupMapping.my_test_tx_group的值改为"my_tx_group" - 修改store模块,将
mode的值改为db,修改具体的db连接信息。
修改配置文件registry.conf,将type改为nacos,将nacos的serverAddr改为"localhost:8848"。
新建数据库seata,复制https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql上的建表语句运行。
先启动nacos,再运行bin\seata-server.bat。
3. 订单/库存/账户业务数据库准备
业务说明:用户下订单,系统远程调用库存服务减库存,远程调用账户服务扣减用户余额,最后修改订单状态为已完成。
创建数据库和业务表:
seata_order:存储订单的数据库```mysql
t_order表
CREATE TABLE
t_order(idbigint(11) NOT NULL AUTO_INCREMENT,user_idbigint(11) NULL DEFAULT NULL COMMENT ‘用户id’,product_idbigint(11) NULL DEFAULT NULL COMMENT ‘产品id’,countint(11) NULL DEFAULT NULL COMMENT ‘数量’,moneydecimal(11, 0) NULL DEFAULT NULL COMMENT ‘金额’,statusint(1) NULL DEFAULT NULL COMMENT ‘订单状态:0-创建中,1-已完成’, PRIMARY KEY (id) ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8; ```seata_storage:存储库存的数据库```mysql
t_storage表
CREATE TABLE
t_storage(idbigint(11) NOT NULL AUTO_INCREMENT,product_idbigint(11) NULL DEFAULT NULL COMMENT ‘产品id’,totalint(11) NULL DEFAULT NULL COMMENT ‘总库存’,usedint(11) NULL DEFAULT NULL COMMENT ‘已用库存’,residueint(11) NULL DEFAULT NULL COMMENT ‘剩余库存’, PRIMARY KEY (id) ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
插入数据
INSERT INTO t_storage VALUES (1, 1, 100, 0, 100);
- seata_account:存储账户信息的数据库```mysql
# t_account表
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
`total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度',
`used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用额度',
`residue` decimal(10, 0) NULL DEFAULT NULL COMMENT '剩余额度',
PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
# 插入数据
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
3个库都要建各自的回滚日志表undo_log:
CREATE TABLE IF NOT EXISTS `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
4. 订单/库存/账户业务微服务准备
业务需求:下订单 → 减库存 → 扣余额 → 改订单状态
订单微服务
新建订单模块seata-order-service2001,添加依赖:
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--要排除掉,与seata server的版本相匹配-->
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.1.0</version>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
添加配置文件application.yml:
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: my_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 123456
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
将seata/conf下的配置文件file.conf拷贝到本项目resources中,修改service节点的事务组名称:
#修改自定义事务组名称
vgroupMapping.my_tx_group = "default"
将seata/conf下的配置文件registry.conf拷贝到本项目resources中。
新建包com.atguigu.springcloud.alibaba,在包下新建domain用于存放实体。在domain下新建类CommonResult:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
在domain下新建类Order:
@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:已完结
}
在alibaba包下新建dao包用于存放接口,在dao下新建接口OrderDao:
@Mapper
public interface OrderDao {
// 新建订单
void create(Order order);
// 修改订单状态
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
在resources目录下新建mapper文件夹用于存放mapper文件,新建OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
insert into t_order (id, user_id, product_id, count, money, status)
values (null, #{userId}, #{productId}, #{count}, #{money}, 0);
</insert>
<update id="update">
update t_order set status=1 where user_id=#{userId} and status = #{status};
</update>
</mapper>
在alibaba包下新建service包,新建3个接口OrderService、StorageService、AccountService:
public interface OrderService {
void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
新建接口实现类impl.OrderServiceImpl:
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
@Override
public void create(Order order) {
log.info("----> 新建订单");
orderDao.create(order);
log.info("----> 订单微服务开始调用[库存微服务],扣减count [start]");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----> 订单微服务开始调用[库存微服务],扣减count [end]");
log.info("----> 订单微服务开始调用[账户微服务],扣减money [start]");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----> 订单微服务开始调用[账户微服务],扣减money [end]");
log.info("----> 修改订单状态 [start]");
orderDao.update(order.getUserId(), 0);
log.info("----> 修改订单状态 [end]");
}
}
在alibaba包下新建controller包,新建OrderController类:
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "订单创建成功");
}
}
在alibaba包下新建config包用于存放配置,新建MyBatisConfig类:
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
新建DataSourceProxyConfig类:
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().
getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
在alibaba包下新建主启动类SeataOrderMainApp2001:
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动装载
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
库存微服务
新建订单模块seata-storage-service2002,添加依赖同2001。
添加配置文件application.yml同2001,修改server.port为2002,spring.application.name为seata-storage-service,spring.datasource.url为jdbc:mysql://localhost:3306/seata_storage。添加file.conf和registry.conf同2001。
新建包com.atguigu.springcloud.alibaba,新建domain包用于存放实体。在domain下新建类CommonResult同2001,新建实体类Storage:
@Data
public class Storage {
private Long id;
private Long productId; // 产品id
private Integer total; // 总库存
private Integer used; // 已用库存
private Integer residue; // 剩余库存
}
在alibaba包下新建dao包用于存放接口,在dao下新建接口StorageDao:
@Mapper
public interface StorageDao {
// 扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
在resources目录下新建mapper文件夹用于存放mapper文件,新建StorageMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
UPDATE t_storage SET used = used + #{count}, residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
在alibaba包下新建service包,新建接口StorageService:
public interface StorageService {
// 扣减库存
void decrease(Long productId, Integer count);
}
新建接口实现类impl.StorageServiceImpl:
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
// 扣减库存
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageDao.decrease(productId, count);
log.info("------->storage-service中扣减库存结束");
}
}
在alibaba包下新建controller包,新建StorageController类:
@RestController
public class StorageController {
@Resource
private StorageService storageService;
// 扣减库存
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200, "扣减库存成功!");
}
}
在alibaba包下新建config包用于存放配置,新建MyBatisConfig、DataSourceProxyConfig同2001。
新建主启动类SeataStorageMainApp2002:
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动装载
public class SeataStorageMainApp2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMainApp2002.class, args);
}
}
账户微服务
新建订单模块seata-account-service2003,添加依赖同2001。
添加配置文件application.yml同2001,修改server.port为2003,spring.application.name为seata-account-service,spring.datasource.url为jdbc:mysql://localhost:3306/seata_account。添加file.conf和registry.conf同2001。
新建包com.atguigu.springcloud.alibaba,新建domain包用于存放实体。在domain下新建类CommonResult同2001,新建实体类Account:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private Long userId; // 用户id
private BigDecimal total; // 总额度
private BigDecimal used; // 已用额度
private BigDecimal residue; // 剩余额度
}
在alibaba包下新建dao包用于存放接口,在dao下新建接口AccountDao:
@Mapper
public interface AccountDao {
// 扣减账户余额
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
在resources目录下新建mapper文件夹用于存放mapper文件,新建AccountMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE t_account SET residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
在alibaba包下新建service包,新建接口AccountService:
public interface AccountService {
// 扣减账户余额
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
新建接口实现类impl.AccountServiceImpl:
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
// 扣减账户余额
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("------->account-service中扣减账户余额开始");
// 模拟超时异常,全局事务回滚
// 暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
accountDao.decrease(userId,money);
log.info("------->account-service中扣减账户余额结束");
}
}
在alibaba包下新建controller包,新建AccountController类:
@RestController
public class AccountController {
@Resource
private AccountService accountService;
// 扣减账户余额
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult(200, "扣减账户余额成功!");
}
}
在alibaba包下新建config包用于存放配置,新建MyBatisConfig、DataSourceProxyConfig同2001。
新建主启动类SeataAccountMainApp2003:
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动装载
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
先启动nacos,seata,2001,2002,再启动本模块。
5.使用@GlobalTransactional
在2001的OrderServiceImpl类的create()方法上加上注解@GlobalTransactional,重启2001,再访问刚才的链接。发现订单不会创建,库存和余额也不会有变化。
注意,Nacos默认是AP的(临时注册),一旦调用微服务失败就会移除。
6. Seata原理
概念:
- TC——事务协调者,管理整个事务的生命周期,类似于班主任管理整个班级
- TM——事务管理器,事务发起方,类似于老师,上课之前先让班主任通知到位
- RM——资源管理器,事务参与者,类似于每个学生
基本步骤:
- TM开启分布式事务(TM向TC注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向TC汇报资源准备状态);
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务);
- TC汇总事务信息,决定分布式事务是提交还是回滚;
- TC通知所有RM提交/回滚资源,事务二阶段结束。
一阶段加载:
在一阶段,Seata会拦截“业务SQL”
- 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成”before image”
- 执行“业务SQL”更新业务数据,在业务数据更新之后,
- 其保存成”after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。
回滚方式便是用”before image”还原业务数据;但在还原前要首先要校验脏写,对比”数据库当前业务数据”和”after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不-致就说明有脏写,出现脏写就需要转人工处理。
二阶段提交:
二阶段如是顺利提交的话,因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
