Spring Cloud Alibaba
简介
Spring Cloud Alibaba 旨在为微服务开发提供一站式解决方案。
Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,参考官网安装 Nacos。
注册中心
Nacos 整合 Spring Cloud 作为注册中心,在启动程序中加上@EnableDiscoveryClient 注解即可,主要配置如下:
服务提供者
新建一个名为cloud-alibaba-provider的 SpringCloud 项目。
@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderDemoApplication.class, args);
}
@RestController
public static class EchoController {
@Value("${server.port}")
private String port;
@GetMapping(value = "/echo/pro/{name}")
public String echo(@PathVariable("name") String name) {
return "Hello Nacos Discovery " + name + ",port:" + port;
}
}
}<!-- nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>server:
port: 9000
spring:
application:
name: cloud-alibaba-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848服务消费者
新建一个名为cloud-alibaba-consumer的 SpringCloud 项目。
在服务消费端,RestTemplate 配合 loadbalance 可以用最原始的一种方式实现负载均衡。启动多个生产者实例并修改其端口,验证负载均衡是否生效。
使用 RestTemplate 需要手动写 URL,非常不爽。当然,这里只做演示,推荐 整合 Openfeign ,只需要创建一个接口即可实现远程调用。
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConsumerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConsumerDemoApplication.class, args);
}
@Bean
@LoadBalanced //使用负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
@RestController
public static class EchoController {
@Autowired
private RestTemplate restTemplate;
@GetMapping(value = "/echo/con/{name}")
public String echo(@PathVariable("name") String name) {
return restTemplate.getForObject(
String.format("http://cloud-alibaba-provider/echo/pro/%s", name), String.class);
}
}
}<!-- nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- loadbalancer 服务负载均衡远程调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>server:
port: 10000
spring:
application:
name: cloud-alibaba-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848配置中心
配置中心能实现服务配置动态变更(在控制器加上 @RefreshScope,可动态读取配置)。
项目初始化时,配置文件 bootstrap 优先级高于 application,对于相同配置项,后加载的配置文件可以覆盖前加载的。
以下示例演示如何从 Nacos 中读取配置信息:
配置准备:在 Nacos 服务端中新建一个名(Data ID)为cloud-alibaba-config-client-dev.yaml的配置文件,约定配置文件命名规则为:应用名-环境.配置文件格式,下文示例项目中的环境为 dev,就能读取到这个配置。
配置中加入如下配置:
config:
info: 恭喜你成功啦!!新建一个 SpringCloud 项目:
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabaConfigApplication {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaConfigApplication.class, args);
}
@RestController
// 在控制器加上 @RefreshScope ,Nacos 服务器端更新可以立即读取
@RefreshScope
public static class TestController {
@Value("${config.info}") // 读取 Nacos 服务端配置
private String info;
@GetMapping(value = "/echo/config-info")
public String echo() {
return "Hello Nacos config info:" + info;
}
}
}<!-- 引入nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入nacos配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- bootstrap.yml配置支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>4.1.0</version>
</dependency>server:
port: 9010
spring:
application:
name: cloud-alibaba-config-client
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml # 指定配置文件格式
# 若遵循约定的配置文件命名,可忽略 extension-configs 配置项;当然也可以自定义配置文件名。
extension-configs:
- data-id: cloud-alibaba-config-client-dev.yaml
- group: xxx # 默认为 DEFAULT_GROUP
server-addr: ${spring.cloud.nacos.discovery.server-addr} # 以 nacos 作为配置中心
profiles:
active: dev
# Nacos 端配置文件( Data Id )格式: 应用名-环境.配置文件格式提示
可以通过 Nacos 服务端的 命名空间+组+配置文件 来细分配置,Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
配置中心优先级
// todo jar 运行时指定配置文件路径
持久化
使用 Nacos 作为配置中心,配置文件默认保存在内存中,重启服务配置就会丢失,官方推荐使用 MySQL 数据库作为数据持久方案,请先在数据库中运行初始化文件mysql-schema.sql。
在 nacos 配置文件中/conf/application.properties添加如下数据即可:
spring.sql.init.platform=mysql
db.num=1
db.url.0=jdbc:mysql://${mysql_host}:${mysql_port}/${nacos_database}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user.0=${mysql_user}
db.password.0=${mysql_password}提示
以上持久化方案只适合单机模式部署,推荐使用集群模式部署 Nacos。
Sentinal
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
资源是 Sentinel 中的核心概念之一,Sentinel 将每一个需要流控的 URL 或者方法称为一个资源,并使用 URL 地址 或 方法签名表示资源名称。
使用 @SentinelResource 注解可自定义资源名称,并且可设置熔断、降级方法。
查看应用程序的流量控制信息,可以安装 Sentinal 控制台。在应用的配置文件中指定控制台地址即可,控制台支持实时监控和规则管理。
新建一个名为cloud-alibaba-sentinel的 SpringCloud 项目。:
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAlibabaSentinelApplication {
public static void main(String[] args) {
SpringApplication.run(CloudAlibabaSentinelApplication.class, args);
}
@RestController
public static class TestController {
@GetMapping(value = "/hello/{name}")
@SentinelResource(value = "hello",blockHandler = "myHandler",fallback = "myFallback")
public String hello(@PathVariable("name") String name) {
if("zs".equals(name)) throw new RuntimeException("走开!");
return "Hello Sentinel: " + name;
}
public String myHandler(String name, BlockException ex) {
return "限流(服务熔断)提示:失败了!" + ex.getMessage() + "方法参数:" + name;
}
public String myFallback(String name, Throwable throwable) {
return "异常(服务降级)提示:失败了!" + throwable.getMessage() + "方法参数:" + name;
}
}
}<!-- 引入nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入sentinel流量治理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>server:
port: 9011
spring:
application:
name: cloud-alibaba-sentinel
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080 # Sentinel 控制台服务地址,给控制台发送本服务流量信息
# Sentinel通过此端口启动一个Http Server,默认8719,由这个Http Server将流量信息发送到控制台
# 若在一台机器上启动多个程序,为避免端口占用,会自动+1,直到找到可用端口
port: 8719
# 上下文不为同一链路,例如 Controller 调 Service,可能有多个资源,避免被认为是同一个
web-context-unify: false运行项目,多次访问 http://localhost:9011/hello,打开 Sentinel 控制台地址,查看监控信息,可了解到 url 的访问情况。
流控规则
Sentinel 控制台流控规则面板:

流控模式
流控模式 描述 直接 默认流控模式。指定的资源访问达到阈值,限制请求 关联 例如设置资源 A 关联 B, 当 B 达到阈值时,限制外部访问 A 链路 例如资源 A 、B 内部都调用资源 C ,为 C 设置链路流控规则且入口资源为 A,达到阈值只会限制 A,对 B 无影响 流控效果
流控效果 描述 快速失败 达到阈值直接拒绝访问 Warm Up 预热(慢启动)模式,检测到请求量达到 1/3,在预热时长内逐渐放行直至阈值 排队等待 例如设置 QPS 为 1,排队等待超时时间为 10 秒;若有 100 个并发请求过来了,因为 QPS=1,则 10 秒内共处理 10 个请求,其他请求拒绝访问
熔断规则
Sentinel 控制台熔断规则面板:

熔断策略
熔断策略 描述 慢调用比例 请求响应时间大于 RT(最大的响应时间)则统计为慢调用,单位统计时长内,请求数大于最小请求数且慢调用比例大于阈值,请求会自动被熔断。经过熔断时长后熔断器进入半开状态,再逐渐恢复 异常比例 单位统计时长内请求数目大于最小请求数,且异常的比例大于阈值,请求会自动被熔断。经过熔断时长后熔断器进入半开状态,再逐渐恢复 异常数 单位统计时长内请求数目大于最小请求数,且异常的数目大于阈值,请求会自动被熔断。经过熔断时长后熔断器进入半开状态,再逐渐恢
热点规则
热点即经常访问的数据,可对某个访问频次最高的 Top K 数据访的问进行限制。
例如有如下 Restful 下单接口
@GetMapping(value = "/test/{productId}/{userId}") @SentinelResource(value = "order") public String order(@PathVariable("productId") String productId, @PathVariable("userId") String userId) { return "Hello productId: " + productId + ", userId: " + userId; }根据订单历史发现,id 为【遥遥领先】的商品卖的非常火爆,一秒钟卖 5000 个,导致系统处理不过来,系统急需对特定商品做限流处理。
在 Sentinel 控制台热点规则面板输入如下内容:
下图设置当购买普通商品时,QPS 限制为 1500,当购买 id 为【遥遥领先】的商品时,QPS 限制为 3000。

热点规则面板
授权规则
通过控制访问来源限制资源是否通过。
设置应用名
@Component public class MyRequestOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { return "app-shop"; } }设置授权规则
设置只让应用名称为
app-shop的应用访问指定资源(多个应用可以用英文逗号隔开)。
授权规则面板
Sentinel 持久化
在前面几个小节中,通过使用 Sentinel 控制台创建规则,默认被保存在客户端内存中,一旦重启微服务,所有规则配置项都会丢失。
Sentinel 支持多种规则持久化方式(Nacos、ZK、Apollo、Redis 等),这里以 Nacos 为例:
<!-- 采用 Nacos 作为规则配置数据源 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency># 以下为数据源示例配置
spring:
cloud:
sentinel:
datasource:
# 流控数据源
my-datasource-1:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: sentinel-flow
group-id: DEFAULT_GROUP
data-type: json
# 规则类型:flow(流控)、 degrade(熔断降级)、authority(授权)
# system(系统)、param-flow(热点)、 gw-flow(网关限流)、 gw-api-group(网关组)
rule-type: flow
# 熔断降级数据源
my-datasource-2:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: sentinel-degrade
group-id: DEFAULT_GROUP
data-type: json
rule-type: degrade流控规则持久化
打开 Nacos 控制台,新建一个 Data ID 为sentinel-flow、Group 为DEFAULT_GROUP、配置格式为JSON的配置文件,输入如下内容(注意去掉注释,属性列表可参考官网):
[
{
"resource": "hello", // 资源名
"limitApp": "default", // 针对来源,若为 default 则不区分调用来源
"grade": 1, // 限流阈值类型(1:QPS; 0:并发线程数)
"count": 3, // 单机阈值
"clusterMode": false, // 是否是集群模式
"strategy": 0, // 流控模式(0:直接; 1:关联; 2:链路)
"controlBehavior": 0, // 流控效果 (0:快速失败,1:Warm Up(预热模式),2:排队等待)
"warmUpPeriodSec": 10, // 预热时间(秒,预热模式需要此参数)
"maxQueueingTimeMs": 500, // 超时时间(排队等待模式需要此参数)
"refResource": "rrr" // 关联资源、入口资源(关联、链路模式需要此参数)
}
]JSON 配置和 Sentinel Dashboard 是一一对应的:

提示
Sentinel Dashboard 主要读取配置和监控流量信息,无法通过它直接修改 Nacos 中的配置,对于应用程序而言,不使用 Sentinel Dashboard 也没有任何影响。
如果直接在 Sentinel Dashboard 中修改规则,只会存在于客户端内存中,在 Nacos 中修改配置才是正确的做法。
熔断规则持久化
和流控规则类似,只提供 JSON 配置供参考:
[
{
"resource": "hello", // 资源名
"grade": 1, // 熔断策略 (0:慢调用比例/;1:异常比例;2:异常数)
"count": 3, // 最大 RT (毫秒) 或 异常数
"slowRatioThreshold": 0.3, // 比例阈值
"timeWindow": 5, // 熔断时长,单位为 s
"minRequestAmount": 10, // 最小请求数
"statIntervalMs": 1000 // 统计时长(单位为 ms)
}
]热点规则持久化
[
{
"resource": "order", // 资源名
"paramIdx": 0, // 参数索引
"count": 1500, // 单机阈值
"durationInSec": 1, // 统计窗口时长(单位秒)
"clusterMode": false, // 是否是集群模式
"paramFlowItemList": [
{ "object": "遥遥领先", "classType": "java.lang.String", "count": 3000 }
] // 参数例外项
}
]提示
其他持久化规则自行参考官网,太无聊懒得写了
Sentinel 整合 Openfeign
注意
由于 Openfeign 是第三方组件,SpringCloud 官方在 2022 年宣布只会改改它的小问题,不会有重大更新。SpringCloud 官方建议使用自产的 Spring Interface Clients 来替代它,它使用 Webclient,支持反应式 Api,和 Openfeign 类似也能像调用本地方法一样远程调用其他服务。
使用@SentinelResource注解就能轻松的实现 Sentinel 附带的强大功能,在微服务系统中,使用 Openfeign 远程调用的过程中,如何利用 Sentinel 来实现熔断和降级呢?
Sentinel 与 OpenFeign 组件兼容,整合 Openfeign 如下:
服务提供者
对于服务提供者
cloud-alibaba-provider来说,仅需在 Controller 中添加@SentinelResource注解,指定资源名称为test01,在 Sentinel 控制台中为这个资源设置访问规则,测试服务熔断是否可用。Openfeign 对服务提供者是没有任何代码侵入的,在消费者端它只会通过注册中心找到对应的服务。
启动类@SpringBootApplication @EnableDiscoveryClient public class NacosProviderDemoApplication { public static void main(String[] args) { SpringApplication.run(NacosProviderDemoApplication.class, args); } @RestController public static class EchoController { @Value("${server.port}") private String port; @GetMapping(value = "/echo/pro/{name}") @SentinelResource(value = "test01", blockHandler = "myHandler") public String echo(@PathVariable("name") String name) { return "Hello Nacos provider " + name + ",port:" + port; } public String myHandler(String name, BlockException ex) { return "限流(服务熔断)提示:失败了!" + ex.getMessage() + "方法参数:" + name; } } }pom.xml<!-- 引入 sentinel 流量治理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>统一暴露接口
新建
cloud-alibaba-provider-interface项目,创建一个接口,接口中的方法名、方法参数、访问 url 需要和服务提供方 Controller 一致,且实现了接口的方法可用于服务降级。接口@FeignClient(value = "cloud-alibaba-provider",fallback = MyFallback.class) public interface MyOpenfeignClientApi { @GetMapping(value = "/echo/pro/{name}") String echo(@PathVariable("name") String name); }接口实现@Component public class MyFallback implements MyOpenfeignClientApi { public String echo(String name) { return "异常(服务降级)提示:失败了!方法参数:" + name; } }pom.xml<!-- 引入 openfeign 服务负载均衡远程调用 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>服务消费者
对于消费者而言,只需在启动类中加入
@EnableFeignClients注解,引入暴露接口,即可实现对提供者的调用。新建
cloud-alibaba-consumer-openfeign项目。启动类@EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class NacosConsumerOpenFeignDemoApplication { public static void main(String[] args) { SpringApplication.run(NacosConsumerOpenFeignDemoApplication.class, args); } @RestController public class EchoController { @Autowired private MyOpenfeignClientApi clientApi; // 注入暴露接口 @GetMapping(value = "/echo/con/{name}") public String echo(@PathVariable("name") String name) { return clientApi.echo("通过Feign调用服务提供者成功!name: " + name); } } }pom.xml<!-- 引入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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- 引入服务提供方暴露的接口 --> <dependency> <groupId>com.zjx</groupId> <artifactId>cloud-alibaba-provider-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency>application.ymlserver: port: 10001 spring: application: name: cloud-alibaba-consumer-openfeign cloud: nacos: discovery: server-addr: 127.0.0.1:8848
整合 GateWay
使用Spring Cloud Gateway非常简单,可参考之前笔记。 Sentinel 整合 Gateway 能实现网关流控功能。网关层无业务代码,通常将它作为一个单独的微服务,只负责路由转发和接口鉴权等操作。
网关流控中,主要配置一下网关规则GatewayFlowRule即可,它可以根据路由 Id 统一限流,也支持自定义为某些 API 分组限流,以下是配置类代码(只抽取部分,
新建cloud-alibaba-consumer-gateway项目。
// 自定义API分组
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api01 = new ApiDefinition("my-group-01")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// url前缀匹配
add(new ApiPathPredicateItem().setPattern("/my-app-2/test/echo2/**")
// 匹配策略【前缀、正则、精确(默认)】
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
// 精确指定url
add(new ApiPathPredicateItem().setPattern("/my-app-2/test/echo3/hi"));
}});
definitions.add(api01);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 限制每秒1次请求 -- 根据 Gateway 配置文件的 route id
rules.add(new GatewayFlowRule("cloud-alibaba-provider")
.setCount(1)
.setIntervalSec(1)
);
// 限制每秒3次请求 -- 根据自定义API分组
rules.add(new GatewayFlowRule("my-group-01")
// 自定义,需要指定模式
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(3)
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
// 触发限流,设置处理器处理
GatewayCallbackManager.setBlockHandler((exchange, throwable) -> {
Map<String, Object> map = new HashMap<>();
map.put("errCode", throwable.getMessage());
map.put("errMsg", "请求太过频繁!");
map.put("请求地址",exchange.getRequest().getURI());
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(map);
});
}@SpringBootApplication
@EnableDiscoveryClient
public class NacosGatewayDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosGatewayDemoApplication.class, args);
}
}spring:
application:
name: cloud-alibaba-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080 # Sentinel 控制台服务地址,给控制台发送本服务流量信息
# Sentinel通过此端口启动一个Http Server,默认8719,由这个Http Server将流量信息发送到控制台
# 若在一台机器上启动多个程序,为避免端口占用,会自动+1,直到找到可用端口
port: 8719
gateway:
routes:
- id: cloud-alibaba-provider # 路由id必须唯一,建议使用服务名
uri: lb://cloud-alibaba-provider # 负载均衡到服务提供者
predicates:
# 配置请求路径
- Path=/my-app-1/**
- id: cloud-alibaba-provider-1 # 路由id必须唯一,建议使用服务名
uri: lb://cloud-alibaba-provider # 负载均衡到服务提供者
predicates:
# 配置请求路径
- Path=/my-app-2/**
logging:
level:
com.zjx.cloud.alibaba: DEBUG<!-- 引入 sentinel 流量治理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入 gateway 服务网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--负载均衡,gateway中必须加上这个才能实现用服务名通信,否则只能用ip -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--整合sentinel实现网关流控功能-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>在cloud-alibaba-provider服务中新建几个简单的测试方法如下:
@RestController
public class TestController {
@Value("${server.port}")
private String port;
@GetMapping(value = "/my-app-1/test/echo1/hi")
public String echoTest01() {
return "测试sentinel网关流控,根据【路由id】限流!qps=1 ,port:" + port ;
}
@GetMapping(value = "/my-app-2/test/echo2/hi")
public String echoTest02() {
return "测试sentinel网关流控,根据【url前缀】限流!qps=3 ,port:" + port ;
}
@GetMapping(value = "/my-app-2/test/echo3/hi")
public String echoTest03() {
return "测试sentinel网关流控,【精确url】限流! qps=3 ,port:" + port ;
}
}提示
注意,根据路由 Id 统一限流的地址中,若包含自定义 API 分组中的 URL,则后者会被覆盖!
分布式事务之 Seata
注意
对于 MySQL 而言目前不支持分布式事务,但由于数据库的飞速发展,某些数据库如 TiDB 等自带分布式事务能力,Seata 的作用在将来可能不再那么重要。但目前,Seata 还是有必要了解的!
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 中主要有三个核心概念:
- TC(Transaction Coordinator),事务协调者,可理解为 Seata 本身
- TM(Transaction Manager),事务管理器,标注了@GlobalTransactional 注解的方法,方法中涉及多个数据库的调用。
- RM(Resource Manager),资源管理器,可理解为多个 MySQL 分支,与 TC 交谈并汇报事务状态。
Seata 中处理分布式事务的流程:
各事务模式
AT 模式是较主流的模式,其他模式可根据具体业务需求选择。
Seata AT 模式
Seata 独有的一种事务模式,要求在每个数据库中额外新增一张 undo_log 表,记录增删改操作前、后的镜像数据,如下:
# 业务SQL update user set phone = '110' where phone = '888'; # Seata 中会自动解析这条SQL,并将前后镜像数据插入undo_log 表,大致如下: select * from user where phone = '888'; # 执行业务SQL前,保存镜像,得到id定位 select * from user where id = 1; # 执行业务SQL后,保存镜像由于每个数据库在自己的事务中,都保留了事务执行前后的数据,若某个事务执行失败,TC 会通知其他参与者全部回滚。无需担心 undo_log 表的数据量过大问题,因为每完成一个事务,相关数据会被删除。
AT 模式性能较高,无代码侵入。它存在数据短时不一致问题,如订单库事务提交,库存库事务还未及时处理。
TCC 模式
有人称之为反人类模式, 是一种侵入式的分布式事务解决方案,需要自行实现 Try,Confirm,Cancel 三个操作,对业务系统有着非常大的入侵性,但是它性能很高。
适用场景:对性能有很高要求的系统。
Saga 模式
对于接入第三方系统而言,无权修改数据库结构,AT 模式不再适用,需要手动编辑回滚逻辑。
例如:调用支付宝支付 -> 出库失败 -> 调用支付宝退款接口
适用场景:有第三方系统参与分布式事务,业务流程长、业务流程多。
XA 模式
一种最简单除暴的模式,每个事务参与者开启事务,并执行业务 SQL,但是都不提交事务,最终由 TC 来完成事务统一提交或回滚操作,在事务未提交之前每个参与者都是阻塞状态,性能较差。
适用场景:要求数据的强一致性的系统或一些基于 XA 协议的老应用。
准备
下载 Seata,因为 AT 模式适合大部分应用,之后的内容都会围绕它展开。
启动 Seata 服务端
解压下载的 Seata,新建一个
seata_test数据库,运行解压目录/script/server/db中的 sql 文件,创建 4 张表。然后打开conf目录,参考其中的application.example.yml,修改application.yml文件,application.yml 部分内容如下:server: port: 7091 spring: application: name: seata-server seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP # 服务注册地址 registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP cluster: my-test-cluster # tc集群名称,和客户端程序中的配置对应 # seata 数据保存方式 store: mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata_test?rewriteBatchedStatements=true user: root password: root # 下面的 4张数据库表 global-table: global_table # 全局事务表名 branch-table: branch_table # 分支事务表名 lock-table: lock_table # 全局锁表名 distributed-lock-table: distributed_lock # 多 Sever下用到的锁,保证只有一个处理运行
bin目录中的 seata-setup 脚本,成功访问 http://localhost:7091 即可。准备客户端
新建两个业务库(storage、order),在每个数据库中新建 undo_log 表和自己的业务表。
undo_log 表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`);业务表/** 库存表,商品编码和数量 **/ CREATE TABLE `storage_tb` ( `id` int(11) NOT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO storage_tb VALUES(1,"0001",100) /** 订单表,商品编码和数量 **/ CREATE TABLE `order_tb` ( `id` int(11) NOT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在项目中测试分布式事务
注意
示例项目中使用多数据源来模拟分布式场景,并使 setae 实现分布式事务(单纯为了方便,新建几个服务并通过 feign 调用写着太无聊了)。
实际架构选型中,不推荐在项目中使用 【多数据源+分布式事务】,最好拆分为微服务架构。
新建一个名为
cloud-alibaba-seata的微服务。启动类@SpringBootApplication @EnableDiscoveryClient @MapperScan("com.zjx.cloud.alibaba.mapper") public class NacosSeataDemoApplication { public static void main(String[] args) { SpringApplication.run(NacosSeataDemoApplication.class, args); } }pom.xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <!-- mysql连接驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!-- 多数据源 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <version>4.3.0</version> </dependency> <!-- 引入p6spy 打印完整SQL--> <dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>application.ymlserver: port: 9013 spring: application: name: cloud-alibaba-seata cloud: nacos: discovery: server-addr: 127.0.0.1:8848 datasource: dynamic: p6spy: true # 默认false,建议线上关闭 seata: true # 开启seata代理 【不建议在多数据源中使用Seata,这里只是为了模拟微服务场景】 # 全局hikariCP参数 hikari: connection-timeout: 20000 # 连接超时时间(单位毫秒,默认30秒) idle-timeout: 600000 # 池中空闲连接超时时间 (单位毫秒,默认10分钟) max-pool-size: 10 # 最大的连接数,默认值是 10 min-idle: 10 # 最小连接池数量,默认值与 max-pool-size 相同 primary: db1 # 默认数据源名称为 master datasource: # 命名规则:支持负载均衡,命名为"前缀_xx",使用 @DS("前缀") ,默认轮询策略 db1: url: jdbc:mysql://127.0.0.1:3306/seata_order username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 针对每个库可以重新设置hikari参数 hikari: connection-timeout: 200000 db2: url: jdbc:mysql://127.0.0.1:3306/seata_storage username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver seata: # 自动代理 这个配置是为了满足对多数据源的支持,本项目涉及的【订单、库存】不是真正的微服务 # 实际架构选型中,不推荐在单体项目中使用 【多数据源+分布式事务】 enable-auto-data-source-proxy: false # 正真的微服务架构不要配置此项 registry: type: nacos nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} group: SEATA_GROUP # 需要和Seata 服务端配置的Nacos分组一致 application: seata-server # Seata 服务端的应用id # 事务分组,他和下面的 vgroup-mapping.* 对应,可定位到具体使用的TC集群,设计成这样是为了方便故障转移 tx-service-group: my_tx_group_1 service: vgroup-mapping: my_tx_group_1: my-test-cluster # TC集群名称,他的值和 Seata 服务端配置的cluster值对应 my_tx_group_2: other-cluster # 其他某个独立的 TC 备用集群 data-source-proxy-mode: AT # 默认为 AT logging: level: com.baomidou.dynamic: debug io: seata: debug业务代码
订单和库存使用不同的数据库,业务事务要么同时成功,要么同时失败。
控制器@RestController public class TestSeataController { IOrderService orderService; @Autowired public TestSeataController(IOrderService orderService) { this.orderService = orderService; } @GetMapping("/test/add/{tag}") public void testSeata(@PathVariable("tag") String tag) { orderService.addOrder(tag); } }order@DS("db1") @Service @Slf4j public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService { private final IStorageService storageService; private final OrderMapper orderMapper; @Autowired public OrderServiceImpl(IStorageService storageService, OrderMapper orderMapper) { this.storageService = storageService; this.orderMapper = orderMapper; } @GlobalTransactional(name = "test",rollbackFor = Exception.class) @Override public void addOrder(String tag) { log.info("当前 XID: {}", RootContext.getXID()); Order order = new Order(); order.setCommodityCode("0001"); order.setCount(1); // 加订单 orderMapper.insert(order); if(tag.equals("00")) throw new RuntimeException("模拟业务异常!!!!"); // 减库存 storageService.decrease(order.getCommodityCode(),order.getCount()); } }storage@Service @DS("db2") public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService { private StorageMapper storageMapper; @Autowired public StorageServiceImpl(StorageMapper storageMapper) { this.storageMapper = storageMapper; } @Override public void decrease(String commodityCode, Integer count) { Storage storage = storageMapper.selectOne(new LambdaQueryWrapper<Storage>() .eq(Storage::getCommodityCode, commodityCode)); storage.setCount(storage.getCount() - count); storageMapper.updateById(storage); } }访问 http://localhost:9013/test/add/00测试分布式事务是否生效。

