SpringCloud2024
SpringCloud2024
Cloud之Consul
Consul作为新一代服务注册中心和分布式配置,官网:https://www.consul.io
下载安装
https://developer.hashicorp.com/consul/downloads
下载完成后只有一个consul.exe文件,全路径下查看版本号信息

开发者模式启动
consul agent -dev
访问路径
http://localhost:8500

服务注册与发现
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
yml配置
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
主启动类开启服务发现
@SpringBootApplication
@MapperScan("org.cloud.mapper")
@EnableDiscoveryClient
public class Main8001
{
public static void main(String[] args)
{
SpringApplication.run(Main8001.class,args);
}
}
启动服务并查看consul控制台

经过以上步骤就一个将一个服务注册到consul中进行管理
服务配置与刷新
面临问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。
作用:一次修改,处处生效
需求:
通用全局配置信息,直接注册进consul服务器,从consul获取 既然从consul获取自然要遵守consul的配置规则要求
实践
操作对象:修改cloud-payment-service8001服务
1、导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2、新增配置文件bootstrap.yml
bootstrap.yml是什么呢?:
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的
Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。
Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context
和Application Context
有着不同的约定,所以新增了一个bootstrap.yml
文件,保证Bootstrap Context
和Application Context
配置的分离。application.yml文件改为bootstrap.yml,这是很关键的或者两者共存
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
配置内容
将原来application.yml中consul的一些配置抽到bootstrap.yml中
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
原来的application.yml文件内容
server:
port: 8001
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: '020708'
# Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: org.cloud.entity
configuration:
map-underscore-to-camel-case: true
抽离后的文件
server:
port: 8001
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: '020708'
profiles:
active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: org.cloud.entity
configuration:
map-underscore-to-camel-case: true
3、consul服务器k/v配置填写

为什么要配置k/v配置填写呢,因为规则是人家定的,要遵守规矩配置,步骤如下
1、创建config文件夹,以/结尾
2、config文件夹下分别创建3个文件夹,分别为默认环境:cloud-payment-service;开发环境:cloud-payment-service-dev;生产环境:cloud-payment-service-prod这几个文件后缀名分别对应着application.yml中的profiles: active: dev这个参数。
3、上述3个文件夹下分别创建data内容,data不再是文件夹


data中的内容
cloud:
info: welcometoclouddefaultstudy
4、测试:测试是否可以读取到配置好的内容
@Value("${server.port}")
private String port;
@GetMapping(value = "/pay/get/info")
private String getInfoByConsul(@Value("${cloud.info}") String cloudInfo)
{
return "cloudInfo: "+cloudInfo+"\t"+"port: "+port;
}
可以看到我们已经读取到了内容

5、问题引出
我们将consul服务器起上的dev配置内容修改后,不能及时读取到修改后的值,没有做到及时响应和动态刷新

原因:这是因为没有开启及时刷新注解,还有一个原因就是并不是不会修改,而是官方默认要等待55s修改后的配置内容才会生效

解决方案:
启动类添加刷新注解
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@RefreshScope // 动态刷新
public class Main8001
{
public static void main(String[] args)
{
SpringApplication.run(Main8001.class,args);
}
}
修改bootstrap.yml配置文件:将官方默认的55s修改为1s
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
#实际生产不建议修改
watch:
wait-time: 1
6、持久化问题
以上所有的配置,如果关闭了consul,所有的配置文件立即丢失,需要做持久化配置处理
解决:解决持久化并加入到后台启动
1、在consul同级目录下新建MyData文件夹存放配置文件进行持久化,再放一个脚本实现后台开机自启

2、右键管理员方式启动
启动错误:第一次启动会提示启动失败,服务已经被启动,这时候以管理员身份执行cmd,然后sc delete
consul,最后再执行.bat文件即可

3、后续consul的配置文件就保存在mydata文件夹中了,启动就有
Cloud之OpenFeign
是什么?
OpenFeign是一个声明式的Web服务客户端
能干嘛?
- 可插拔的注解支持,包括Feign注解和JAX-RS注解
- 支持可插拔的HTTP编码器和解码器
- 支持SentineI和它的Fallback
- 支持SrinCloudLoadBalancer的负载均衡
- 支持HTTP请求和响应的压缩
怎么玩?
1、接口 + 注解
架构说明图

2、流程步骤
①、建Model
cloud-consumer-openfeign-order
②、改pom
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud consul discovery-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</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>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
写yml
server:
port: 7999
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
主启动类开启注解@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OpenFeignConsumerApp
{
public static void main( String[] args )
{
SpringApplication.run(OpenFeignConsumerApp.class,args);
}
}
apis包下包含所有向外暴露的接口api

/**
* 支付模块相关接口
* 注意:一定要添加注解@FeignClient,否则无法远程调用服务
* name的值为提供服务类的应用名称
*/
@FeignClient(name = "cloud-payment-service")
public interface PaymentFeignService {
@GetMapping("/pay/add")
ResultData addOrder(PayDto payDTO);
@GetMapping("/pay/query/{orderNo}")
ResultData queryOrder(@PathVariable("orderNo") String orderNo);
}
controller调用服务
@Slf4j
@RestController
@RequestMapping("/feign")
@RequiredArgsConstructor
public class PaymentFeignController {
private final PaymentFeignService paymentFeignService;
/**
* 一般情况下,通过浏览器的地址栏输入url,发送的只能是get请求
* 我们底层调用的是post方法,模拟消费者发送get请求,客户端消费者
* 参数可以不添加@RequestBody
* @param payDTO
* @return
*/
@GetMapping("/pay/add")
public ResultData addOrder(PayDto payDTO){
paymentFeignService.addOrder(payDTO);
return ResultData.success();
}
/**
* @param orderNo
* @return
*/
@GetMapping("/pay/query/{orderNo}")
public ResultData queryOrder(@PathVariable("orderNo") String orderNo){
ResultData result = paymentFeignService.queryOrder(orderNo);
return ResultData.success(result.getData());
}
}
3、小总结
重点说明:OpenFeign自带负载均衡功能,默认是轮询

高级特性
1、超时控制
默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好
yml文件中开启配置:
connectTimeout 连接超时时间
readTimeout 请求处理超时时间

如何控制呢?
修改cloud-consumer-openfeign-order 的yml配置文件开启OpenFeign客户端超时控制

开始配置
全局配置
修改yml配置文件,添加以下内容即可
spring: cloud: openfeign: client: config: default: #连接超时时间 connectTimeout: 3000 #读取超时时间 readTimeout: 3000
指定特定服务配置
可以指定为某个服务提供者的超时时间控制,如支付模块
spring: application: name: cloud-consumer-openfeign-order ####Spring Cloud Consul for Service Discovery cloud: consul: host: localhost port: 8500 discovery: prefer-ip-address: true #优先使用服务ip进行注册 service-name: ${spring.application.name} openfeign: client: config: #default: #connectTimeout: 4000 #连接超时时间 #readTimeout: 4000 #读取超时时间 cloud-payment-service: connectTimeout: 8000 #连接超时时间 readTimeout: 8000 #读取超时时间
特别说明:当全局和指定配置都配置时,优先遵循指定配置超时时间控制
2、重试机制
有默认值,但是被官方关闭了

开启重试机制
新增配置类:FeignConfig并修改Retryer配置
/**
* ClassName: FeignConfig
* Package: org.cloud.config
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/11 - 21:48
* @Version: v1.0
*/
/**
* 重试机制配置
*/
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
//return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100,1,3);
}
}
3、默认HttpClient替换
是什么?
OpenFeign中http client如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,
由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。
替换之前,默认使用的

替换为Apache HttpClient 5
修改cloud-consumer-openfeign-order
导入依赖
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
写yaml
spring:
application:
name: cloud-consumer-openfeign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 4000
#读取超时时间
readTimeout: 4000
# Apache HttpClient5 配置开启
httpclient:
hc5:
enabled: true
替换之后使用的HttpClient

4、请求响应 / 压缩
官网说明

是什么?
对请求和响应进行GZIP压缩
Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过下面的两个参数设置,就能开启请求与相应的压缩功能:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
细粒度化设置
对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,
只有超过这个大小的请求才会进行压缩:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型
spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小
修改cloud-consumer-openfeign-order
写yml
spring:
application:
name: cloud-consumer-openfeign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 4000
#读取超时时间
readTimeout: 4000
httpclient:
hc5:
enabled: true
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true
5、日志打印功能
日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
配置日志类bean
/**
* 重试机制配置
*/
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
// return new Retryer.Default(100,1,3);
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
配置yaml开启日志功能
公式(三段):logging.level + 含有@FeignClient注解的完整带包名的接口名+debug
# feign日志以什么级别监控哪个接口
logging:
level:
org:
cloud:
apis:
PaymentFeignService: debug
Cloud之Sleuth
Micrometer+ ZipKin分布式链路追踪
Sleuth简介
Sleuth也进入维护模式,改头换面

Sleuth未来替换方案:Micrometer Tracing
分布式链路追踪概述
为什么会出现,解决什么问题?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。

问题:随着问题的复杂化 + 微服务的增多 + 调用链路的变长,画面不要太美丽

解决方案
在分布式与微服务场景下,我们需要解决如下问题:
在大规模分布式与微服务集群下,如何实时观测系统的整体调用链路情况。
在大规模分布式与微服务集群下,如何快速发现并定位到问题。
在大规模分布式与微服务集群下,如何尽可能精确的判断故障对系统的影响范围与影响程度。
在大规模分布式与微服务集群下,如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
在大规模分布式与微服务集群下,如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
在大规模分布式与微服务集群下,如何尽可能精确的分析系统的存储瓶颈与容量规划。
上述问题就是我们的落地议题答案:
分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
Micrometer+ ZipKin
新一代分布式链路追踪:Micrometer
Micrometer官网:
https://docs.micrometer.io/
ZipKin?
**SpringCloudSleuth(micrometer)**提供了一套完整的分布式链路追踪(DistributedTracing) 解决方案且兼容支持了zipkin展现。

小总结
将一次分布式请求还原成调用链路,进行日志记录和能监控,并将一次分布式请求的调用情况集中Web展示。
其他方案
行业内比较成熟的其它分布式链路追踪技术解决方案。

链路追踪原理
假定3个微服务调用的链路,Service1调用Service2,Service2调用Service3和Service4

追踪过程
一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来

1 | 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。 |
---|---|
2 | 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。 |
3 | 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。 |
4 | 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。 |
5 | 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。 |
6 | 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。 |
7 | 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。 |
8 | 通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。 |
ZipKin
官网:https://zipkin.io/
ZipKin概述
Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。
为什么会出现?单有Sleuth(Micrometer)行不行?

说明:
当没有配置 Sleuth 链路追踪的时候,INFO 信息里面是 [passjava-question,,,],后面跟着三个空字符串。
当配置了 Sleuth 链路追踪的时候,追踪到的信息是 [passjavaquestion,504a5360ca906016,e55ff064b3941956,false] ,第一个是 Trace ID,第二个是 Span ID。只有日志没有图,观看不方便,不美观,so,引入图形化Zipkin链路监控让你好看,O(∩_∩)O
下载 + 安装 + 运行
运行命令:java -jar zipkin-server-3.0.0-rc0-exec.jar

访问运行控制台:http://IocaIhost:9411/zipkin/

案例实战
分工说明
- Micometer
- 数据采样
- ZipKin
- 图像展示
步骤
1、总体父工程
新增pom依赖
<properties>
<-- 分布式链路追踪依赖 --></-->
<micrometer-tracing.version>1.2.0</micrometer-tracing.version>
<micrometer-observation.version>1.12.0</micrometer-observation.version>
<feign-micrometer.version>12.5</feign-micrometer.version>
<zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
</properties>
<dependencyManagement>
<dependencies>
<!--micrometer-tracing-bom导入链路追踪版本中心 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--micrometer-tracing指标追踪 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-observation 4-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>${micrometer-observation.version}</version>
</dependency>
<!--feign-micrometer 5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<version>${feign-micrometer.version}</version>
</dependency>
<!--zipkin-reporter-brave 6-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
<version>${zipkin-reporter-brave.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
依赖说明
由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:
1 | micrometer-tracing-bom | 导入链路追踪版本中心,体系化说明 |
---|---|---|
2 | micrometer-tracing | 指标追踪 |
3 | micrometer-tracing-bridge-brave | 一个Micrometer模块,用于与分布式跟踪工具 Brave 集成,以收集应用程序的分布式跟踪数据。Brave是一个开源的分布式跟踪工具,它可以帮助用户在分布式系统中跟踪请求的流转,它使用一种称为"跟踪上下文"的机制,将请求的跟踪信息存储在请求的头部,然后将请求传递给下一个服务。在整个请求链中,Brave会将每个服务处理请求的时间和其他信息存储到跟踪数据中,以便用户可以了解整个请求的路径和性能。 |
4 | micrometer-observation | 一个基于度量库 Micrometer的观测模块,用于收集应用程序的度量数据。 |
5 | feign-micrometer | 一个Feign HTTP客户端的Micrometer模块,用于收集客户端请求的度量数据。 |
6 | zipkin-reporter-brave | 一个用于将 Brave 跟踪数据报告到Zipkin 跟踪系统的库。 |
补充包:spring-boot-starter-actuator SpringBoot框架的一个模块用于监视和管理应用程序
2、服务提供者8001
cloud-provider-payment8001
改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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cloud</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-provider-payment8001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
<!--自定义-->
<dependency>
<groupId>org.cloud</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--consul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--SpringBoot通用依赖模块-->
<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>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis-plus和springboot整合-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写yaml
server:
port: 8001
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: '020708'
profiles:
active: dev
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: org.cloud.entity
configuration:
map-underscore-to-camel-case: true
# ========================zipkin===================
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
新建业务类
/**
* ClassName: PayMicrometerController
* Package: org.cloud.controller
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/16 - 13:53
* @Version: v1.0
*/
@RestController
public class PayMicrometerController
{
/**
* Micrometer(Sleuth)进行链路监控的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id)
{
return "Hello, 欢迎到来myMicrometer inputId: "+id+" \t 服务返回:" + IdUtil.simpleUUID();
}
}
3、远程调用openfeign
@FeignClient(name = "cloud-payment-service")
public interface PaymentFeignService {
/**
* Micrometer(Sleuth)进行链路监控的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id);
}
4、服务消费者
cloud-consumer-payment80
改pom
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
写yml
# zipkin图形展现地址和采样率设置
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
新建业务类
@RestController
@Slf4j
public class OrderMicrometerController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id)
{
return payFeignApi.myMicrometer(id);
}
}
测试
本次案例默认已启动ZipKin
将微服务模块注册进consul注册中心
测试地址:http://localhost/feign/micrometer/1
查看ZipKin控制台
会出现以下界面
3148792fe52d9c35185a02dfaec129d9.png 点击【SHOW】按钮查看
查看依赖关系

Cloud之Gateway
官网总述
- 路由:网关的基本构建块。 它由 ID、目标 URI、谓词集合和筛选器集合定义。如果聚合谓词为 true,则匹配路由。
- 谓词:这是一个 Java 8 函数谓词。输入类型是 Spring Framework
ServerWebExchange
。 这使您可以匹配 HTTP 请求中的任何内容,例如标头或参数。 - 筛选器:这些是使用特定工厂构建的
GatewayFilter
实例。 在这里,您可以在发送下游请求之前或之后修改请求和响应。
三大核心
- Route:路由是构建网关的基本模块,它由ID,目标URL,一系列的断言和过滤器组成,如果断言为true则匹配该路由
- Predicate(断言):参考的是Java8的java.util.function.predicate 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
- Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

总结:
web前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。predicate就是我们的匹配条件;filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
工作流程

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
核心逻辑
路由转发 + 断言判断 + 执行过滤器链
创建gateway项目
创建
- 建Model
cloud-gateway9527
- 改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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cloud</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-gateway9527</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 写Yaml
server:
port: 9527
spring:
application:
name: cloud-gateway9527 #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
- 主启动类
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class GatewayApp
{
public static void main(String[] args)
{
SpringApplication.run(GatewayApp.class,args);
}
}
添加网关前测试
在cloud-payment-service服务提供者中新增测试网关的两个接口
/**
* 支付新增接口
* @param payDto
* @return
*/
@PostMapping("/gateway/add")
@Operation(summary = "新增",description = "支付接口")
public ResultData gateWayAddPayment(@RequestBody PayDto payDto){
payService.addPayment(payDto);
return ResultData.success();
}
/**
* 查询支付接口
* @param orderNo
* @return
*/
@GetMapping("/gateway/query/{orderNo}")
@Operation(summary = "查询",description = "查询支付接口")
public ResultData gateWayQueryOrder(@PathVariable("orderNo") String orderNo){
List<PayDto> payDto = payService.queryOrder(orderNo);
return ResultData.success(payDto);
}
进行测试,查看是否可以请求到数据
自测通过,不过这是没有添加网关之前的请求,因为本身已经知道了内部请求地址,所以能通过很正常。
添加网关后测试
- cloud-gateway9527的yaml配置修改
server:
port: 9527
spring:
application:
name: cloud-gateway9527 #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/query/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/add/** # 断言,路径相匹配的进行路由
进行测试,查看是否可以请求到数据
自测通过,说明网关配置没有问题。
引出问题
看看网关9527的Yml配置,映射写死问题

高级特性
Route动态获取服务URL
以微服务名称,动态获取服务URL,解决URL地址写死问题
- 修改9527网关的yaml配置
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #以微服务名称动态获取URL
predicates:
- Path=/pay/gateway/query/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #以微服务名称动态获取URL
predicates:
- Path=/pay/gateway/add/** # 断言,路径相匹配的进行路由
测试2通过:即使访问提供者的应用端口8001变更也可以请求到数据
Predicate断言配置
是什么?

架构概述

内置断言

常用内置断言
配置语法总体概述:有两种
Shortcut Configuration(常用)
Fully Expanded Arguments
示例
常用断言api包括以上内置的十几种,这里以After断言举例,其他的配置举一反三
作用:配置了After断言意味着只有在这个时间段后才可以通过访问请求

我们的问题是:上述这个After好懂,这个时间串串???对应的格式如何获得?
获得ZonedDateTime
/**
* @auther wenxueshi
* @create 2024-4-12 17:37
*/
public class ZonedDateTimeDemo
{
public static void main(String[] args)
{
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj);
}
}
Yaml配置
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- After=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai]
自定义断言
官方内置的断言不够用时,可以看视频解决自定义断言。
总结
Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
Filter过滤器链
常用的内置过滤器讲解
1、请求头(RequestHeader)相关组
具体讲解,其他的举一反三
网关服务添加yaml
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 # 请求头kv,若一头含有多参则重写一行设置
- AddRequestHeader=X-Request-atguigu2,atguiguValue2
2、请求参数(RequestParameter)相关组
自定义过滤器
全局过滤器
案例分析:统计接口调用耗时情况,通过自定义全局过滤器解决上述要求
步骤
新建类MyGlobalFilter并实现GlobalFilter和Ordered两个接口
code
package org.cloud.config; /** * ClassName: MyGlobalFilter * Package: org.cloud.config * Description: * * @Author: @weixueshi * @Create: 2024/4/12 - 15:50 * @Version: v1.0 */ import jakarta.validation.constraints.Size; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 自定义全局网关过滤器 */ @Slf4j @Component public class MyGlobalFilter implements GlobalFilter, Ordered { /** * 开始访问时间 */ private static final String BEGIN_VISIT_TIME = "begin_visit_time"; /** *第2版,各种统计 * @param exchange * @param chain * @return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //先记录下访问接口的开始时间 exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis()); return chain.filter(exchange).then(Mono.fromRunnable(()->{ Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME); if (beginVisitTime != null){ log.info("访问接口主机: " + exchange.getRequest().getURI().getHost()); log.info("访问接口端口: " + exchange.getRequest().getURI().getPort()); log.info("访问接口URL: " + exchange.getRequest().getURI().getPath()); log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery()); log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms"); log.info("我是美丽分割线: ###################################################"); System.out.println(); } })); } /** * 过滤器执行优先级,数字越小优先级越大 * @return */ @Override public int getOrder() { return 0; } }
单一内置过滤器
是什么?
**区别:**以上的全局过滤器是对网关中的所有请求路径下的所有方法都有效,单一内置过滤器是针对某一个请求路径下的所有方法有效。范围比全局过滤器要小。
案例分析:我们内置的这个过滤器的作用是进行请求时需要携带一个statusTest参数并且值为任意才可以通过过滤器
定义步骤
1、新建类名xxx需要以GatewayFilterFactory结尾并继承AbstractGatewayFilterFact0ry类
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
}
2、新建xxxGatewayFilterFactory.Config内部类
public static class Config {
@Setter @Getter
private String status;
}
3、重写apply方法
@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config)
{
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
System.out.println("进入自定义网关过滤器MyGatewayFilterFactory,status===="+config.getStatus());
if(request.getQueryParams().containsKey("statusTest")) {
return chain.filter(exchange);
}else {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
}
};
}
4、重写shortcutFieldOrder
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("status");
}
5、空参构造方法,内部调用super()
public MyGatewayFilterFactory() {
super(MyGatewayFilterFactory.Config.class);
}
如何在网关配置
先看出厂配置
screenshot-1712911648995 自己定制My配置
screenshot-1712911965900
测试
由上图可知,我们只配置了id为pay_routh3下的请求路径
请求路径:
localhost:9527/pay/gateway/get/info
- 请求失败,因为不符合过滤要求
localhost:9527/pay/gateway/get/info?statusTest=test
- 请求成功,符合过滤要求
CloudAlibaba之Nacos
架构介绍

Nacos简介
是什么?
官网:Nacos官网 | Nacos 官方社区 | Nacos 下载 | Nacos一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
一句话:Nacos就是注册中心 + 配置中心的组合

各种注册中心比较

下载安装
官网下载安装nacos,解压安装包,运行bin目录下控制台执行命令startup.cmd -m standalone开启nacos,运行成功后直接访问http://localhost:8848/nacos
作为注册中心
将其他服务注册进nacos
添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
添加yaml
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
enabled: true
主启动类添加
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaOpenFeignApp
{
public static void main( String[] args )
{
SpringApplication.run(AlibabaOpenFeignApp.class,args);
}
}
与gateway集成
注意:虽然nacos与consul作用相似,但是集成gateway和openfeign时有区别
nacos与gateway网关集成
添加依赖:注意一定要添加loadbalancer依赖,否则配置网关时无法通过服务名称去请求,会一直报503错误,与consul与gateway集成就不需要添加loadbalancer依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
完整yaml配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: pay_routh1
uri: lb://cloud-alibaba-openfeign-consumer
predicates:
- Path=/feign/pay/query/**
nacos与openfeign网关集成
添加依赖:注意一定要添加loadbalancer依赖,否则配置网关时无法通过服务名称去请求,会一直报503错误,与consul与gateway集成就不需要添加loadbalancer依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
完整yaml配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
enabled: true
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 20000
#读取超时时间
readTimeout: 20000
httpclient:
hc5:
enabled: true
CloudAlibaba之Sentinel
简介
官网:
介绍 · alibaba/Sentinel Wiki (github.com)
home | Sentinel (sentinelguard.io)
是什么?
轻量级的流量控制,熔断降级的java库
去哪下
Releases · alibaba/Sentinel (github.com)
能干嘛?
从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性
怎么玩?
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务降级
服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。
例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。
服务熔断
在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务熔断一般情况下会有三种状态:闭合、开启和半熔断;
闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
服务限流
服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
服务隔离
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。
互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。
服务超时
整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。
形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。
安装Sentinel
Sentinel由两部分组成

安装步骤
下载
Releases · alibaba/Sentinel (github.com)
下载到本地sentinel-dashboard-1.8.7.jar
运行命令
- 前提
- java环境ok
- 8080端口不能被占用
- 命令
- java -jar sentinel-dashboard-1.8.7.jar
访问sentinel管理界面
http://localhost:8080
账号密码均为sentinel
微服务8401整合案例
新建微服务8401
新建微服务cloudAlibaba-sentinel-service8401:意味着8401微服务将被哨兵纳入监控
改pom
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>org.cloud</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<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>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
写yaml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelApplication
{
public static void main( String[] args )
{
SpringApplication.run(SentinelApplication.class,args);
}
}
业务类
@RestController
public class FlowLimitController
{
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
启动8401服务查看Sentinel控制台
空空如也,啥也没有
Sentinel采用的懒加载说明
- 注意:想使用Sentinel对某个接口进行限流和降级等操作,一定要先访问下接口,使Sentinel检测到相应的接口
- 执行一次访问即可
- http://localhost:8401/testA
- http://localhost:8401/testB
- 效果
24f33ae69639b534d40c60c71fb2b893.png
流控规则
基本介绍
Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。参数见最下方:

1资源名 | 资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。 |
---|---|
2针对来源 | 具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。 |
3阈值类型 | QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。 |
4单机阈值 | 与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。 |
5是否集群 | 选中则表示集群环境,不选中则表示非集群环境。 |
流控模式
直接
- 默认的流控模式,当接口达到限流条件时,直接开启限流功能。
- 配置及说明
- 表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误
- 测试
- 快速点击访问http://localhost:8401/testA
- 结果:Blocked by Sentinel (flow limiting)
- 思考?直接调用默认报错信息,技术方面OK,是否应该有我们自己的后续处理?类似有个fallback的兜底方法?
关联
- 是什么?
- 当关联的资源达到阈值时,就限流自己 当与A关联的资源B达到阀值后,就限流A自己 B惹事,A挂了
- 配置A
- 设置效果:当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名,B惹事,A挂了
- 测试:大批量线程高并发访问B,导致A失效,访问http://localhost:8401/testA,结果:Blocked by Sentinel (flow limiting)
- 是什么?
链路
是什么?
- 来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施, 比如C请求来访问就限流,D请求来访问就是OK
修改微服务cloudAlibaba-sentinel-service8401
改yaml
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service #8401微服务提供者后续将会被纳入阿里巴巴sentinel监管 cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址 port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
业务类
新建FlowLimitService
@Service public class FlowLimitService { @SentinelResource(value = "common") public void common() { System.out.println("------FlowLimitService come in"); } }
修改FlowLimitController
@RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } /**流控-链路演示demo * C和D两个请求都访问flowLimitService.common()方法,阈值到达后对C限流,对D不管 */ @Resource private FlowLimitService flowLimitService; @GetMapping("/testC") public String testC() { flowLimitService.common(); return "------testC"; } @GetMapping("/testD") public String testD() { flowLimitService.common(); return "------testD"; } }
Sentinel配置
- 说明:C和D两个请求都访问flowLimitService.common()方法,对C限流,对D不管
测试
- 访问http://localhost:8401/testC
- C链路
- D链路ok
流控效果

熔断规则
介绍
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
熔断规则实战
慢调用比例
异常比例
异常数
@SentinelResource注解
是什么?
@SentinelResource是一个流量防卫防护组件注解,用于指定防护资源,对配置的资源进行流量控制,熔断降级等功能。
实战
默认限流返回
安装rest地址限流 + 默认限流返回
通过访问的rest地址来限流,会返回sentinel自带默认的限流处理信息
新建业务类RateLimitController
@RestController @Slf4j public class RateLimitController { @GetMapping("/rateLimit/byUrl") public String byUrl() { return "按rest地址限流测试OK"; } }
访问一次:http://localhost:8401/rateLimit/byUrl 没啥事
Sentinel控制台配置
测试:疯狂点击http://localhost:8401/rateLimit/byUrl
- 结果:
自定义限流返回
不想要默认自带的限流提示Blocked by Sentinel (flow limiting),想返回自定义的限流提示
修改业务类RateLimitController
@RestController @Slf4j public class RateLimitController { @GetMapping("/rateLimit/byUrl") public String byUrl() { return "按rest地址限流测试OK"; } @GetMapping("/rateLimit/byResource") @SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException") public String byResource() { return "按资源名称SentinelResource限流测试OK"; } public String handleException(BlockException exception) { return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o"; } }
测试地址:http://localhost:8401/rateLimit/byResource
Sentinel控制台配置
疯狂点击:http://localhost:8401/rateLimit/byResource
返回自定义限流提示
f1dec737ef7e9a49b4d14a981ac8f7e0.png
自定义限流 + 服务降级处理
按照SentinelResource配置,点击超过限流配置返回自定义限流提示 + 程序异常返回fallbakc服务降级处理
修改业务类RateLimitController
@RestController @Slf4j public class RateLimitController { @GetMapping("/rateLimit/byUrl") public String byUrl() { return "按rest地址限流测试OK"; } @GetMapping("/rateLimit/byResource") @SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException") public String byResource() { return "按资源名称SentinelResource限流测试OK"; } public String handleException(BlockException exception) { return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o"; } @GetMapping("/rateLimit/doAction/{p1}") @SentinelResource(value = "doActionSentinelResource", blockHandler = "doActionBlockHandler", fallback = "doActionFallback") public String doAction(@PathVariable("p1") Integer p1) { if (p1 == 0){ throw new RuntimeException("p1等于零直接异常"); } return "doAction"; } public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){ log.error("sentinel配置自定义限流了:{}", e); return "sentinel配置自定义限流了"; } public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){ log.error("程序逻辑异常了:{}", e); return "程序逻辑异常了"+"\t"+e.getMessage(); } }
访问地址:http://localhost:8401/rateLimit/doAction/2
Sentinel控制台配置
图形配置和代码关系
测试:
疯狂访问:http://localhost:8401/rateLimit/doAction/2,返回了自定义限流提示
1f6a8b13f25126deef6df63658f3238e.png p1参数为0,访问:http://localhost:8401/rateLimit/doAction/0,异常发生,返回了自定义的fallback服务降级处理
603fb91522eb078909ca8a287480cc8e.png
小结
blockHandler:主要针对sentinel配置后出现的违规情况处理
fallback:程序出现了异常,JVM抛出的异常服务降级
两者可以同时共存
热点规则
授权规则
规则持久化
Sentinel集成openfeign
作用?
实现fallback服务降级
需求说明
cloudalibaba-consumer-nacos-order83 通过OpenFeign调用 cloudalibaba-provider-payment9001
1 、83 通过OpenFeign调用 9001微服务,正常访问OK
2 、83 通过OpenFeign调用 9001微服务,异常访问error
访问者要有fallback服务降级的情况,不要持续访问9001加大微服务负担,但是通过feign接口调用的又方法各自不同,
如果每个不同方法都加一个fallback配对方法,会导致代码膨胀不好管理,工程埋雷....../(ㄒoㄒ)/~~
3 、public @interface FeignClient
通过fallback属性进行统一配置,feign接口里面定义的全部方法都走统一的服务降级,一个搞定即可。
4、 9001微服务自身还带着sentinel内部配置的流控规则,如果满足也会被触发,也即本例有2个Case
4.1 、OpenFeign接口的统一fallback服务降级处理
4.2 、Sentinel访问触发了自定义的限流配置,在注解@SentinelResource里面配置的blockHandler方法。
编码步骤
修改服务提供方
1、修改微服务cloudAlibaba-provider-payment9001
- 改pom:新增sentinel依赖即可,目的就是让服务提供方9001被哨兵sentinel监控
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 写yaml:添加sentinel配置即可
spring:
application:
name: cloud-payment-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
enabled: true
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
添加一个被sentinel哨兵监控的业务类用来测试
/**
* ClassName: PaySentinelController
* Package: org.cloud.controller
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/14 - 15:20
* @Version: v1.0
*/
@RestController
public class PaySentinelController {
@GetMapping("/pay/sentinel/get/{orderNo}")
@SentinelResource(value = "getPayByOrderNo",blockHandler = "handlerBlockHandler")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
{
//模拟从数据库查询出数据并赋值给DTO
PayDto payDto = new PayDto();
payDto.setOrderNo(orderNo);
payDto.setPayNo("pay:"+ IdUtil.fastUUID());
payDto.setUserId(1);
return ResultData.success("查询返回值:"+payDto);
}
public ResultData handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception)
{
return ResultData.fail(ResponseCodeEnum.BAD_REQUEST.getCode(),"getPayByOrderNo服务不可用," +
"触发sentinel流控配置规则"+"\t"+"o(╥﹏╥)o");
}
/*
fallback服务降级方法纳入到Feign接口统一处理,全局一个
public ResultData myFallBack(@PathVariable("orderNo") String orderNo,Throwable throwable)
{
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"异常情况:"+throwable.getMessage());
}
*/
}
修改openfeign服务
改pom:添加sentinel依赖
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
修改远程调用接口,并且添加fallback处理类
package org.cloud.apis;
import io.swagger.v3.oas.annotations.Operation;
import org.cloud.apis.fallback.PayFeignSentinelApiFallBack;
import org.cloud.model.dto.PayDto;
import org.cloud.response.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* ClassName: AliPaymentFeign
* Package: org.cloud.apis
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/13 - 14:57
* @Version: v1.0
*/
@FeignClient(name = "cloud-payment-service",fallback = PayFeignSentinelApiFallBack.class)
public interface AliPaymentFeign {
/**
* sentinel哨兵监控测试
* @param orderNo
* @return
*/
@GetMapping("/pay/sentinel/get/{orderNo}")
ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
新建PayFeignSentinelApiFallBack.class处理fallback逻辑,这就是全局唯一的服务降级处理
/**
* ClassName: PayFeignSentinelApiFallBack
* Package: org.cloud.apis.fallback
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/14 - 16:06
* @Version: v1.0
*/
@Component
public class PayFeignSentinelApiFallBack implements AliPaymentFeign {
@Override
public ResultData getPayByOrderNo(String orderNo)
{
return ResultData.fail(ResponseCodeEnum.BAD_REQUEST.getCode(),"对方服务宕机或不可用,FallBack服务降级o(╥﹏╥)o");
}
}
新建服务消费方
改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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cloud</groupId>
<artifactId>cloud2024</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudAlibaba-consumer-payment83</artifactId>
<dependencies>
<!--自定义openfeign服务-->
<dependency>
<groupId>org.cloud</groupId>
<artifactId>cloudAlibaba-openfeign-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--自定义-->
<dependency>
<groupId>org.cloud</groupId>
<artifactId>cloud-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</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>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写yaml
server:
port: 83
spring:
application:
name: sentinel-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
main:
allow-bean-definition-overriding: true
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
添加启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerPayment83App
{
public static void main( String[] args )
{
SpringApplication.run(ConsumerPayment83App.class,args);
}
}
添加业务类
@RestController
@RequiredArgsConstructor
public class ConsumerPaySentinelController {
private final AliPaymentFeign aliPaymentFeign;
@GetMapping("/consumer/pay/sentinel/get/{orderNo}")
public ResultData getPayment(@PathVariable("orderNo") String orderNo){
return aliPaymentFeign.getPayByOrderNo(orderNo);
}
}
启动服务消费方时报错:

解决方案

测试
以上三个服务全部启动成功后,测试访问地址为(这个时候sentinel还未做任何的配置)
http://localhost:83/consumer/pay/sentinel/get/147
测试:即使疯狂点击进行请求handlerBlockHandler和fallback的逻辑也不会生效
配置sentinel


测试handlerBlockHandler是否生效:疯狂点击http://localhost:83/consumer/pay/sentinel/get/147
结果触发了handlerBlockHandler方法,并输出自定义限流提示
测试fallback是否生效:将访问提供者9001宕机
结果触发了PayFeignSentinelApiFallBack类中的方法,并输出自定义服务降级提示
Sentinel与openfeign集成完毕
Sentinel集成gateway
需求说明
由Sentinel集成gateway通过限流方式来保护服务提供者cloud-Alibaba-provider-payment9001
集成步骤
建Model
cloudAlibaba-sentinel-gateway9528
改pom
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
写yaml
server:
port: 9528
spring:
application:
name: cloudAlibaba-sentinel-gateway9528 #以微服务注册进consul
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: pay_routh1
uri: lb://sentinel-order-consumer
predicates:
- Path=/consumer/**
配置类(重要)
参考官网配置说明案例改写
/**
* ClassName: GatewayConfiguration
* Package: org.cloud.config
* Description:
*
* @Author: @weixueshi
* @Create: 2024/4/14 - 22:17
* @Version: v1.0
*/
/**
* 使用时只需注入对应的 SentinelGatewayFilter
* 实例以及 SentinelGatewayBlockExceptionHandler 实例即可
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer)
{
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* javax.annotation.PostConstruct
*/
@PostConstruct
public void doInit() {
initBlockHandler();
}
//处理/自定义返回的例外信息
private void initBlockHandler() {
Set<GatewayFlowRule> rules = new HashSet<>();
/**
* 参数1:网关唯一id
* 参数2、3:1秒钟2个请求,超过触发限流
*/
rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String,String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "系统繁忙,稍后重试!");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}
}
测试
原生url(不加网关)
- http://localhost:9001/pay/sentinel/get/333
加网关
http://localhost:9528/consumer/pay/sentinel/get/888
Sentinel + gateway:加快点击频率,出现限流错误
825f519931b94f2414d0d8c748f2684d.png
CloudAlibaba之Seata
问题引出
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题,但是,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

结论
迫切希望提供一种分布式事务框架,解决微服务架构下的分布式事务问题
简介
是什么?
Simple Extensible Autonomous Transaction Architecture:Seata
Apache Seata(incubating) 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
能干嘛?
解决分布式事务
去哪下?
官网:Apache Seata
怎么玩?
本地@Transactional
全局@GlobalTransactional
Seata的分布式交易解决方案
3c515c833d82540654e36ad43b6aeb93.png
工作流程简介
1、纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。

2、seata对分布式事务的协调和控制就是1+3
一个XID
XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
官网版3个概念(TC—>TM—>RM)
个别版3个概念
- TC (Transaction Coordinator) - 事务协调器
- 就是seata,负责维护全局事务和分支事务的状态。驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器
- 标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
- RM (Resource Manager) - 资源管理器
- 就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事,汇报分支事务状态,驱动分支事务的提交或回滚。
- TC (Transaction Coordinator) - 事务协调器
3、分布式事务的执行流程———小总结
三个组件相互协作,TC以Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行,
流程如下:

4、各事务模式

我们以AT模式进行学习
Seata-Server2.0.0安装
下载地址
配置参数参考
https://seata.apache.org/zh-cn/docs/user/configuration
新手部署指南

步骤
建库
1、新建数据库Seata
CREATE DATABASE seata;
USE seata;
SQL建表地址
https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
SQL脚本
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
结果

改配置
修改seata-server-2.O.O\conf\appIication.yml配置,记得先备份

application.yml定稿版(一般使用里面的配置足矣)
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Seata控制台访问端口
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${log.home:${user.home}/logs/seata}
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
# Seata控制台登录用户密码
console:
user:
username: seata
password: seata
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
namespace:
cluster: default
username: nacos
password: nacos
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: root
password: '020708'
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
启动
先启动Nacos
再启动seata-server.bat
访问http://127.0.0.1:7091

再看Nacos,可以看到Seata已经注册到Nacos中了

案例实战
案例实战说明:
即将准备三个微服务模块:订单模块、库存模块、账户余额模块
主要说明演示:使用分布式事务Seata前后的区别说明:不使用分布式事务有什么影响,使用分布式事务可以解决哪些痛点。
数据库和表准备
注意:以下演示Nacos和Seata都要先保证启动ok
案例业务说明:
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。-------最下面还有笔记
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
再通过远程调用账户服务来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单 → 减库存 → 扣余额 → 改(订单)状态

建库
针对3个微服务建各自的数据库

SQL脚本
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
建表
针对这3个库,各自需要建对应的undo_log回滚日志表
SQL脚本
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
结果

针对这3个库,各自需要建对应的业务表
seata_order库建t_order表
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
seata_account建t_account
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
seata_storage建t_storage
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
最终效果

编码落地实现
业务需求:下订单——>减库存——>扣余额——>改订单状态
创建openfeign需要远程调用的一些接口方法
seata-account-service远程接口
/**
* @Author: @weixueshi
* @Create: 2024/4/15 - 13:07
* @Version: v1.0
*/
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
/**
* 扣减账户余额
*/
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
seata-storage-service远程接口
/**
* @Author: @weixueshi
* @Create: 2024/4/15 - 13:07
* @Version: v1.0
*/
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
/**
* 扣减库存
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
新建订单Order微服务
建Model
- seata-order-service2001
改pom
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--自定义--> <dependency> <groupId>org.cloud</groupId> <artifactId>cloud-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--自定义openfeign服务--> <dependency> <groupId>org.cloud</groupId> <artifactId>cloudAlibaba-openfeign-consumer</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--alibaba-seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--loadbalancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringBoot通用依赖模块--> <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> <!--SpringBoot集成druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mybatis-plus和springboot整合--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> </dependency> <!--Mysql数据库驱动8 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!-- fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
写yml
server: port: 2001 spring: application: name: seata-order-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 # ==========applicationName + druid-mysql8 driver=================== datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: '020708' # ========================mybatis=================== mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: org.cloud.entity configuration: map-underscore-to-camel-case: true # ========================seata=================== seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称 service: vgroup-mapping: # 点击源码分析 default_tx_group: default # 事务组与TC服务集群的映射关系 data-source-proxy-mode: AT logging: level: io: seata: info
启动类
@SpringBootApplication @MapperScan("org.cloud.mapper") @EnableDiscoveryClient //服务注册和发现 @EnableFeignClients public class SeataOrderServiceApp { public static void main( String[] args ) { SpringApplication.run(SeataOrderServiceApp.class,args); } }
业务类
这里省略了mapper等哪些创建
OrderController
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private IOrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
OrderServiceImpl
/**
* @author author
* @since 2024-04-15
* 下订单->减库存->扣余额->改(订单)状态
*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Resource
private OrderMapper orderMapper;
/**
* 订单微服务通过OpenFeign去调用库存微服务
*/
@Resource
private StorageFeignApi storageFeignApi;
/**
* 订单微服务通过OpenFeign去调用账户微服务
*/
@Resource
private AccountFeignApi accountFeignApi;
@Override
// @GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class)
public void create(Order order) {
//xid检查
String xid = RootContext.getXID();
//1. 新建订单
log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
//订单状态status:0:创建中;1:已完结
order.setStatus(0);
int result = orderMapper.insert(order);
//插入订单成功后获得插入mysql的实体对象
Order orderFromDB = null;
if(result > 0)
{
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("status", order.getStatus());
orderFromDB = orderMapper.selectOne(wrapper);
//orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
System.out.println();
//3. 扣减账号余额
log.info("-------> 订单微服务开始调用Account账号,做扣减money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
System.out.println();
//4. 修改订单状态
//订单状态status:0:创建中;1:已完结
log.info("-------> 修改订单状态");
orderFromDB.setStatus(1);
int updateResult = baseMapper.updateById(orderFromDB);
log.info("-------> 修改订单状态完成"+"\t"+updateResult);
log.info("-------> orderFromDB info: "+orderFromDB);
}
System.out.println();
log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
}
}
新建库存storage微服务
建Model
- seata-storage-service2002
改pom
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--自定义--> <dependency> <groupId>org.cloud</groupId> <artifactId>cloud-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--loadbalancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!--alibaba-seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringBoot通用依赖模块--> <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> <!--SpringBoot集成druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mybatis-plus和springboot整合--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> </dependency> <!--Mysql数据库驱动8 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!-- fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
写yml
server: port: 2002 spring: application: name: seata-storage-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 # ==========applicationName + druid-mysql8 driver=================== datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: '020708' # ========================mybatis=================== mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: org.cloud.entity configuration: map-underscore-to-camel-case: true # ========================seata=================== seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称 service: vgroup-mapping: default_tx_group: default # 事务组与TC服务集群的映射关系 data-source-proxy-mode: AT logging: level: io: seata: info
启动类
@SpringBootApplication @MapperScan("org.cloud.mapper") @EnableDiscoveryClient @EnableFeignClients public class SeataStorageServiceapp { public static void main( String[] args ) { System.out.println( "Hello World!" ); } }
业务类
StorageController
@RestController
@RequestMapping("/storage")
public class StorageController {
@Resource
private IStorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/decrease")
public ResultData decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return ResultData.success("扣减库存成功!");
}
}
StorageServiceImpl
@Slf4j
@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService {
@Resource
private StorageMapper storageMapper;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageMapper.decrease(productId,count);
log.info("------->storage-service中扣减库存结束");
}
}
StorageMapper
<?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="org.cloud.mapper.StorageMapper">
<resultMap id="BaseResultMap" type="org.cloud.model.entity.Storage">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="total" jdbcType="INTEGER" property="total" />
<result column="used" jdbcType="INTEGER" property="used" />
<result column="residue" jdbcType="INTEGER" property="residue" />
</resultMap>
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
新建账户Account微服务
建Model
- seata-account-service2003
改pom
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--自定义--> <dependency> <groupId>org.cloud</groupId> <artifactId>cloud-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--loadbalancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!--alibaba-seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringBoot通用依赖模块--> <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> <!--SpringBoot集成druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mybatis-plus和springboot整合--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> </dependency> <!--Mysql数据库驱动8 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!-- fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
写yml
server: port: 2003 spring: application: name: seata-account-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 # ==========applicationName + druid-mysql8 driver=================== datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: '020708' # ========================mybatis=================== mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: org.cloud.entity configuration: map-underscore-to-camel-case: true # ========================seata=================== seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称 service: vgroup-mapping: default_tx_group: default # 事务组与TC服务集群的映射关系 data-source-proxy-mode: AT logging: level: io: seata: info
启动类
@SpringBootApplication @MapperScan("org.cloud.mapper") @EnableDiscoveryClient @EnableFeignClients public class SeataAccountServiceApp { public static void main( String[] args ) { SpringApplication.run(SeataAccountServiceApp.class,args); } }
业务类
AccountController
@RestController
@RequestMapping("/account")
public class AccountController {
@Resource
private IAccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/decrease")
public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
accountService.decrease(userId,money);
return ResultData.success("扣减账户余额成功!");
}
}
AccountServiceImpl
@Slf4j
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {
@Resource
AccountMapper accountMapper;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣减账户余额开始");
accountMapper.decrease(userId,money);
//myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣减账户余额结束");
}
/**
* 模拟超时异常,全局事务回滚
*/
private static void myTimeOut()
{
try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
AccountMapper
<?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="org.cloud.mapper.AccountMapper">
<resultMap id="BaseResultMap" type="org.cloud.model.entity.Account">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="total" jdbcType="DECIMAL" property="total" />
<result column="used" jdbcType="DECIMAL" property="used" />
<result column="residue" jdbcType="DECIMAL" property="residue" />
</resultMap>
<!--
money 本次消费金额
t_account数据库表
total总额度 = 累计已消费金额(used) + 剩余可用额度(residue)
-->
<update id="decrease">
UPDATE
t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
案例实战测试
启动服务
- 启动Nacos
- 启动Seata
- 启动openfeign服务
- 启动订单微服务2001
- 启动库存微服务2002
- 启动账户微服务2003
数据库初始情况

正常下单
此时我们没有在订单模块添加==@GlobalTransactional==
结果:没有任何问题
超时异常出错
此时我们没有在订单模块添加==@GlobalTransactional==
故障情况:
当库存和账户全额扣减后订单状态并没有设置为已经完成。没有从零改为1
数据库情况

超时异常解决
此时我们在订单模块添加==@GlobalTransactional==
@Override
@GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class) //AT
public void create(Order order)
{
..........
}
此时,订单模块就是TM,也是其中一个RM

查看Seata后台
http://IocaIhost:7091/#/Iogin
全局事务ID
全局锁
下单后数据库并没有任何变化,被回滚了
- 业务中

按照正常逻辑,本该有新记录入库,等待最后完成提交。
回滚后
Order记录根本就插入不进来
5172cdaac923752a846fbe48f780eaf5.png
上一步准备新增的记录被彻底回滚了,保证的一致性。
全部回退
总结
AT模式如何做到对业务的无侵入
是什么?

一阶段加载
在一阶段,Seata 会拦截“业务 SQL”,
1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
2 执行“业务 SQL”更新业务数据,在业务数据更新之后,
3 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

二阶段分2种情况
正常提交
二阶段如是顺利提交的话,
因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

异常回滚
二阶段回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
