Spring Cloud 本身并不是一个拿来即可用的框架,它是一套微服务规范,这套规范共有两代实现。
2018 年 12 月12 日,Netflix 公司宣布 Spring Cloud Netflix 系列大部分组件都进入维护模式,不再添加新特性。这严重地限制了 Spring Cloud 的高速发展,于是各大互联网公司和组织开始把目光转向 Spring Cloud 的第二代实现:Spring Cloud Alibaba。
Spring Cloud Alibaba 是阿里巴巴结合自身丰富的微服务实践而推出的微服务开发的一站式解决方案,是 Spring Cloud 第二代实现的主要组成部分。
Spring Cloud Alibaba 吸收了 Spring Cloud Netflix 的核心架构思想,并进行了高性能改进。自 Spring Cloud Netflix 进入停更维护后,Spring Cloud Alibaba 逐渐代替它成为主流的微服务框架。
Spring Cloud Alibaba 是国内首个进入 Spring 社区的开源项目。2018 年 7 月,Spring Cloud Alibaba 正式开源,并进入 Spring Cloud 孵化器中孵化;2019 年 7 月,Spring Cloud 官方宣布 Spring Cloud Alibaba 毕业,并将仓库迁移到 Alibaba Github OSS 下。
虽然 Spring Cloud Alibaba 诞生时间不久,但俗话说的好“大树底下好乘凉”,依赖于阿里巴巴强大的技术影响力,Spring Cloud Alibaba 在业界得到了广泛的使用,成功案例也越来越多。
Spring Cloud Alibaba 包含了多种开发分布式微服务系统的必需组件
通过 Spring Cloud Alibaba 的这些组件,我们只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
Spring Cloud Alibaba 的应用场景如下:
下表展示了 Spring Cloud 两代实现的组件对比情况。
Spring Cloud 第一代实现(Netflix) | 状态 | Spring Cloud 第二代实现(Alibaba) | 状态 |
---|---|---|---|
Ereka | 2.0 孵化失败 | Nacos Discovery 性能更好,感知力更强 | |
Ribbon | 停更进维 | Spring Cloud Loadbalancer Spring Cloud 原生组件,用于代替 Ribbon | |
Hystrix | 停更进维 | Sentinel 可视化配置,上手简单 | |
Zuul | 停更进维 | Spring Cloud Gateway 性能为 Zuul 的 1.6 倍 | |
Spring Cloud Config | 搭建过程复杂,约定过多,无可视化界面,上手难点大 | Nacos Config | 搭建过程简单,有可视化界面,配置管理更简单,容易上手 |
Spring Cloud、Spring Cloud Alibaba 以及 Spring Boot 之间版本依赖关系如下。
Spring Cloud 版本 | Spring Cloud Alibaba 版本 | Spring Boot 版本 |
---|---|---|
Spring Cloud 2020.0.1 | 2021.1 | 2.4.2 |
Spring Cloud Hoxton.SR12 | 2.2.7.RELEASE | 2.3.12.RELEASE |
Spring Cloud Hoxton.SR9 | 2.2.6.RELEASE | 2.3.2.RELEASE |
Spring Cloud Greenwich.SR6 | 2.1.4.RELEASE | 2.1.13.RELEASE |
Spring Cloud Hoxton.SR3 | 2.2.1.RELEASE | 2.2.5.RELEASE |
Spring Cloud Hoxton.RELEASE | 2.2.0.RELEASE | 2.2.X.RELEASE |
Spring Cloud Greenwich | 2.1.2.RELEASE | 2.1.X.RELEASE |
Spring Cloud Finchley | 2.0.4.RELEASE(停止维护,建议升级) | 2.0.X.RELEASE |
Spring Cloud Edgware | 1.5.1.RELEASE(停止维护,建议升级) | 1.5.X.RELEASE |
Spring Cloud Alibaba 下各组件版本关系如下表。
Spring Cloud Alibaba 版本 | Sentinel 版本 | Nacos 版本 | RocketMQ 版本 | Dubbo 版本 | Seata 版本 |
---|---|---|---|---|---|
2.2.7.RELEASE | 1.8.1 | 2.0.3 | 4.6.1 | 2.7.13 | 1.3.0 |
2.2.6.RELEASE | 1.8.1 | 1.4.2 | 4.4.0 | 2.7.8 | 1.3.0 |
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE | 1.8.0 | 1.4.1 | 4.4.0 | 2.7.8 | 1.3.0 |
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE | 1.8.0 | 1.3.3 | 4.4.0 | 2.7.8 | 1.3.0 |
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE | 1.7.1 | 1.2.1 | 4.4.0 | 2.7.6 | 1.2.0 |
2.2.0.RELEASE | 1.7.1 | 1.1.4 | 4.4.0 | 2.7.4.1 | 1.0.0 |
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE | 1.7.0 | 1.1.4 | 4.4.0 | 2.7.3 | 0.9.0 |
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE | 1.6.3 | 1.1.1 | 4.4.0 | 2.7.3 | 0.7.1 |
Nacos 英文全称为 Dynamic Naming and Configuration Service,是一个由阿里巴巴团队使用 Java 语言开发的开源项目。
Nacos 是一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台(参考自 Nacos 官网)。
Nacos 的命名是由 3 部分组成:
组成部分 | 全称 | 描述 |
---|---|---|
Na | naming/nameServer | 即服务注册中心,与 Spring Cloud Eureka 的功能类似。 |
co | configuration | 即配置中心,与 Spring Cloud Config+Spring Cloud Bus 的功能类似。 |
s | service | 即服务,表示 Nacos 实现的服务注册中心和配置中心都是以服务为核心的。 |
我们可以将 Nacos 理解成服务注册中心和配置中心的组合体,它可以替换 Eureka 作为服务注册中心,实现服务的注册与发现;还可以替换 Spring Cloud Config 作为配置中心,实现配置的动态刷新。
Nacos 作为服务注册中心经历了十年“双十一”的洪峰考验,具有简单易用、稳定可靠、性能卓越等优点,可以帮助用户更敏捷、容易地构建和管理微服务应用。
Nacos 支持几乎所有主流类型“服务”的发现、配置和管理:
Nacos 提供了一系列简单易用的特性,能够帮助我们快速地实现动态服务发现、服务配置等功能。
Nacos 支持基于 DNS 和 RPC 的服务发现。当服务提供者使用原生 SDK、OpenAPI 或一个独立的 Agent TODO 向 Nacos 注册服务后,服务消费者可以在 Nacos 上通过 DNS TODO 或 HTTP&API 查找、发现服务。
Nacos 提供对服务的实时健康检查,能够阻止请求发送到不健康主机或服务实例上。Nacos 还提供了一个健康检查仪表盘,能够帮助我们根据健康状态管理服务的可用性及流量。
动态配置服务可以让我们以中心化、外部化和动态化的方式,管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效、敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的 UI 帮助我们管理所有服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助我们更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
Nacos 提供了动态 DNS 服务,能够让我们更容易地实现负载均衡、流量控制以及数据中心内网的简单 DNS 解析服务。
Nacos 提供了一些简单的 DNS APIs TODO,可以帮助我们管理服务的关联域名和可用的 IP:PORT 列表。
Nacos 能让我们从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及 metrics 统计数据。
与 Eureka 类似,Nacos 也采用 CS(Client/Server,客户端/服务器)架构,它包含两大组件,如下表。
组件 | 描述 | 功能 |
---|---|---|
Nacos Server | Nacos 服务端,与 Eureka Server 不同,Nacos Server 由阿里巴巴团队使用 Java 语言编写并将 Nacos Server 的下载地址给用户,用户只需要直接下载并运行即可。 | Nacos Server 可以作为服务注册中心,帮助 Nacos Client 实现服务的注册与发现。Nacos Server 可以作为配置中心,帮助 Nacos Client 在不重启的情况下,实现配置的动态刷新。 |
Nacos Client | Nacos 客户端,通常指的是微服务架构中的各个服务,由用户自己搭建,可以使用多种语言编写。 | Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-discovery,在服务注册中心(Nacos Server)中实现服务的注册与发现。 |
Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-config,在配置中心(Nacos Server)中实现配置的动态刷新。 |
Nacos 作为服务注册中心可以实现服务的注册与发现,流程如下图。
图1:Nacos 服务注册与发现
在图 1 中共涉及到以下 3 个角色:
Nacos 实现服务注册与发现的流程如下:
下面我们以 Nacos 2.0.3 为例,演示下如何安装和运行 Nacos Server,步骤如下。
图2:Nacos Server 下载
图3:Nacos Server 目录结构
Nacos Server 下各目录说明如下:
startup.cmd -m standalone
"nacos is starting with standalone",--.,--.'|,--,: : | Nacos 2.0.3
,`--.'`| ' : ,---. Running in stand alone mode, All function modules
| : : | | ' ,'\ .--.--. Port: 8848
: | \ | : ,--.--. ,---. / / | / / ' Pid: 27512
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.3.138:8848/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'2021-11-08 16:16:38,877 INFO Bean 'org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler@5ab9b447' of type [org.springframework.security.access.expression.method
.DefaultMethodSecurityExpressionHandler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-11-08 16:16:38,884 INFO Bean 'methodSecurityMetadataSource' of type [org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource] is not eligible for getting processed by al
l BeanPostProcessors (for example: not eligible for auto-proxying)
2021-11-08 16:16:40,001 INFO Tomcat initialized with port(s): 8848 (http)
2021-11-08 16:16:40,713 INFO Root WebApplicationContext: initialization completed in 14868 ms
2021-11-08 16:16:52,351 INFO Initializing ExecutorService 'applicationTaskExecutor'
2021-11-08 16:16:52,560 INFO Adding welcome page: class path resource [static/index.html]
2021-11-08 16:16:54,239 INFO Creating filter chain: Ant [pattern='/**'], []
2021-11-08 16:16:54,344 INFO Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7dd611c8, org.springframework.security.web.con
text.SecurityContextPersistenceFilter@5c7668ba, org.springframework.security.web.header.HeaderWriterFilter@fb713e7, org.springframework.security.web.csrf.CsrfFilter@6ec7bce0, org.springframework.secur
ity.web.authentication.logout.LogoutFilter@7d9ba6c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@158f4cfe, org.springframework.security.web.servletapi.SecurityContextHolderAwa
reRequestFilter@6c6333cd, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5d425813, org.springframework.security.web.session.SessionManagementFilter@13741d5a, org.springf
ramework.security.web.access.ExceptionTranslationFilter@3727f0ee]
2021-11-08 16:16:54,948 INFO Initializing ExecutorService 'taskScheduler'
2021-11-08 16:16:54,977 INFO Exposing 16 endpoint(s) beneath base path '/actuator'
2021-11-08 16:16:55,309 INFO Tomcat started on port(s): 8848 (http) with context path '/nacos'
2021-11-08 16:16:55,319 INFO Nacos started successfully in stand alone mode. use embedded storage
图4:Nacos Server 登陆页面
图5:Nacos Server 控制台
自此,我们就完成了 Nacos Server 的下载、安装和运行工作。
接下来,我们来搭建一个服务提供者,步骤如下。
4.0.0 pom org.springframework.boot spring-boot-starter-parent 2.5.6 net.demo.c spring-cloud-alibaba-demo 1.0-SNAPSHOT 8 8 UTF-8 1.8 1.8 4.12 1.2.17 1.16.18 2020.0.4 com.alibaba.cloud spring-cloud-alibaba-dependencies 2021.1 pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import
在该工程的 pom.xml 中,我们通过 dependencyManagement 对 Spring Cloud Alibaba 的版本信息进行管理,该工程下的各个子模块在引入 Spring Cloud Alibaba 的各个组件时就不要单独指定版本号了。
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-provider-8001 0.0.1-SNAPSHOT spring-cloud-alibaba-provider-8001 Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
#端口号
server.port=8001
#服务名
spring.application.name=spring-cloud-alibaba-provider
#Nacos Server 的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
management.endpoints.web.exposure.include=*
package net.demo.c.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class DeptController {@Value("${server.port}")private String serverPort;@GetMapping(value = "/dept/nacos/{id}")public String getPayment(@PathVariable("id") Integer id) {return "提醒您,服务访问成功!
服务名:spring-cloud-alibaba-provider
端口号: " + serverPort + "
传入的参数:" + id;}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //开启服务发现功能
public class SpringCloudAlibabaProvider8001Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaProvider8001Application.class, args);}
}
启动 spring-cloud-alibaba-provider-8001,使用浏览器访问“http://localhost:8001/dept/nacos/1”。
使用浏览器访问“http://localhost:8848/nacos”,查看“服务管理”下的“服务列表”,如下图。
图7:服务注册列表
从图 7 可以看到,我们搭建的服务提供者 spring-cloud-alibaba-provider-8001 所提供的服务已经被注册到了 Nacos Server 上了。
下面,我们就来搭建一个服务消费者来消费 spring-cloud-alibaba-provider-8001 提供的服务,步骤如下。
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-consumer-nacos-8081 0.0.1-SNAPSHOT spring-cloud-alibaba-consumer-nacos-8081 Demo project for Spring Boot 1.8 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-loadbalancer org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
注意:由于 Netflix Ribbon 已经进入停更维护状态,Nacos Discovery 已经停了对 Ribbon 的支持,因此我们需要在服务消费者的 pom.xml 中引入 spring-cloud-loadbalancer 才能调用服务提供者提供的服务。
server:port: 8801 #端口号
spring:application:name: spring-cloud-alibaba-consumer #服务名cloud:nacos:discovery:server-addr: localhost:8848 #Nacos server 的地址
#以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码服务提供者的服务名
service-url:nacos-user-service: http://spring-cloud-alibaba-provider #服务提供者的服务名
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 开启服务注册与发现功能
public class SpringCloudAlibabaConsumerNacos8801Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaConsumerNacos8081Application.class, args);}
}
package net.demo.c.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextBean {@Bean@LoadBalanced //与 Ribbon 集成,并开启负载均衡功能public RestTemplate getRestTemplate() {return new RestTemplate();}
}
package net.demo.c.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class DeptController_Consumer {@Resourceprivate RestTemplate restTemplate;@Value("${service-url.nacos-user-service}")private String serverURL; //服务提供者的服务名@GetMapping("/consumer/dept/nacos/{id}")public String paymentInfo(@PathVariable("id") Long id) {return restTemplate.getForObject(serverURL + "/dept/nacos/" + id, String.class);}
}
图8:服务消费者
Nacos Server 还可以作为配置中心,对 Spring Cloud 应用的外部配置进行统一地集中化管理。而我们只需要在应用的 POM 文件中引入 spring-cloud-starter-alibaba-nacos-config 即可实现配置的获取与动态刷新。
从配置管理的角度看,Nacos 可以说是 Spring Cloud Config 的替代方案,但相比后者 Nacos 的使用更简单,操作步骤也更少。
接下来,我们通过一个实例来演示下 Nacos 是如何实现配置的统一管理和动态刷新的。
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-config-client-3377 0.0.1-SNAPSHOT spring-cloud-alibaba-nacos-config-client-3377 Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-bootstrap org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
注意:我们使用的 Spring Cloud 2020 版本默认不启用 bootstrap,若想要在应用启动时加载 bootstrap 配置(例如 bootstrap.yml 或 bootstrap.properties),就需要我们在 pom.xml 中显式的引入 spring-cloud-starter-bootstrap 依赖。
server:port: 3377 #端口号
spring:application:name: config-client #服务名cloud:nacos:discovery:server-addr: 127.0.0.1:8848 #Nacos服务注册中心地址config:server-addr: 127.0.0.1:8848 #Nacos作为配置中心地址file-extension: yaml #指定yaml格式的配置
spring:profiles:active: dev #激活 dev 的配置
package net.demo.c.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope
public class ConfigClientController {@Value("${config.info}")private String ConfigInfo;@GetMapping("/config/info")public String getConfigInfo(){return ConfigInfo;}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaNacosConfigClient3377Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaNacosConfigClient3377Application.class, args);}
}
Data ID: config-client-dev.yamlGroup : DEFAULT_GROUP配置格式: YAML配置内容: config:info: c.demo.net
在 Nacos Server 中,配置的 dataId(即 Data ID)的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
dataId 格式中各参数说明如下:
图10:Nacos Config
config:info: this is c.demo.net
图11:Nacos Cofig
在实际的项目开发中,一个微服务系统往往由十几,几十个甚至几百个微服务组成。 这些服务若全部注册到同一台 Nacos Server,就极有可能导致 Nacos Server 因为不堪重负而崩溃,最终导致整个微服务系统瘫痪。解决这个问题最直接的办法就是使用 Nacos Server 集群。
Nacos Server 的集群化部署有一个十分明显的优点,那就是可以保障系统的高可用性。在集群化部署中,只要不是所有的 Nacos Server 都停止工作,Nacos Client 就还可以从集群中正常的 Nacos Server 上获取服务信息及配置,而不会导致系统的整体瘫痪,这就是 Nacos Server 集群化部署的高可用性。
下图展示了 Nacos Server 集群化部署的基本架构。
图12:Nacos Server 集群架构
下面我们以 Windows 系统为例,演示如何部署 Nacos Server 集群。
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(255) DEFAULT NULL,`content` longtext NOT NULL COMMENT 'content',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',`app_name` varchar(128) DEFAULT NULL,`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',`c_desc` varchar(256) DEFAULT NULL,`c_use` varchar(64) DEFAULT NULL,`effect` varchar(64) DEFAULT NULL,`type` varchar(64) DEFAULT NULL,`c_schema` text,PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(255) NOT NULL COMMENT 'group_id',`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',`content` longtext NOT NULL COMMENT '内容',`gmt_modified` datetime NOT NULL COMMENT '修改时间',`app_name` varchar(128) DEFAULT NULL,`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL COMMENT 'content',`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL COMMENT 'content',`md5` varchar(32) DEFAULT NULL COMMENT 'md5',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`src_user` text COMMENT 'source user',`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',PRIMARY KEY (`id`),UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (`id` bigint(20) NOT NULL COMMENT 'id',`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',`data_id` varchar(255) NOT NULL COMMENT 'data_id',`group_id` varchar(128) NOT NULL COMMENT 'group_id',`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',`nid` bigint(20) NOT NULL AUTO_INCREMENT,PRIMARY KEY (`nid`),UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (`id` bigint(64) unsigned NOT NULL,`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`data_id` varchar(255) NOT NULL,`group_id` varchar(128) NOT NULL,`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',`content` longtext NOT NULL,`md5` varchar(32) DEFAULT NULL,`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,`src_user` text,`src_ip` varchar(50) DEFAULT NULL,`op_type` char(10) DEFAULT NULL,`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',PRIMARY KEY (`nid`),KEY `idx_gmt_create` (`gmt_create`),KEY `idx_gmt_modified` (`gmt_modified`),KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';CREATE TABLE `tenant_info` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',`kp` varchar(128) NOT NULL COMMENT 'kp',`tenant_id` varchar(128) default '' COMMENT 'tenant_id',`tenant_name` varchar(128) default '' COMMENT 'tenant_name',`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);CREATE TABLE `permissions` (`role` varchar(50) NOT NULL,`resource` varchar(255) NOT NULL,`action` varchar(8) NOT NULL,UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
192.168.3.138:3333
192.168.3.138:4444
192.168.3.138:5555
配置说明如下:
192.168.138 为本地电脑主机的 IP 地址,这里最好不要写成 localhost 或 127.0.0.1,否则 Nacos Server 集群可能会搭建失败!
本次搭建的 Nacos Server 集群的端口分别为:3333、4444、5555。
server.port=3333
################ MySQL 数据库配置##################
spring.datasource.platform=mysqldb.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
db.user=root
db.password=root
4. 将该 Nacos Server 目录复制到另外两台机器上,并将它们的端口号分别修改为: 4444 和 5555。
#user nobody;
worker_processes 1;#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;#pid logs/nginx.pid;events {worker_connections 1024;
}http {include mime.types;default_type application/octet-stream;sendfile on;keepalive_timeout 65;upstream cluster{server 127.0.0.1:3333;server 127.0.0.1:4444;server 127.0.0.1:5555;}server {listen 1111;server_name localhost;#charset koi8-r;#access_log logs/host.access.log main;location / {#root html;#index index.html index.htm;proxy_pass http://cluster;}}
}
"nacos is starting with cluster",--.,--.'|,--,: : | Nacos 2.0.3
,`--.'`| ' : ,---. Running in cluster mode, All function modules
| : : | | ' ,'\ .--.--. Port: ****
: | \ | : ,--.--. ,---. / / | / / ' Pid: 21592
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.3.138:3333/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'2021-11-09 16:25:00,993 INFO The server IP list of Nacos is [192.168.3.138:3333, 192.168.3.138:4444, 192.168.3.138:5555]2021-11-09 16:27:07,318 INFO Nacos is starting...2021-11-09 16:27:08,325 INFO Nacos is starting...2021-11-09 16:27:09,340 INFO Nacos is starting...2021-11-09 16:27:10,343 INFO Nacos is starting...2021-11-09 16:27:10,742 INFO Nacos started successfully in cluster mode. use external storage
图13:Nginx 启动脚本
图14:Nacos 集群
server:port: 8801 #端口号
spring:application:name: spring-cloud-alibaba-consumer #服务名cloud:nacos:discovery:#server-addr: localhost:8848 #单机版 Nacos Server 的地址server-addr: localhost:1111 #集群版 Nacos Server 的地址
#以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码服务提供者的服务名
service-url:nacos-user-service: http://spring-cloud-alibaba-provider #服务提供者的服务名
图15:服务注册到 Nacos Server 集群上
Sentinel 是由阿里巴巴中间件团队开发的开源项目,是一种面向分布式微服务架构的轻量级高可用流量控制组件。
Sentinel 主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度帮助用户保护服务的稳定性。
Sentinel 具有以下优势:
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它可以在 ClassPath 路径下的 META-INF/services 文件夹查找文件,并自动加载文件中定义的类。
从功能上来说,Sentinel 与 Spring Cloud Netfilx Hystrix 类似,但 Sentinel 要比 Hystrix 更加强大,例如 Sentinel 提供了流量控制功能、比 Hystrix 更加完善的实时监控功能等等。
Sentinel 主要由以下两个部分组成:
Sentinel 核心库不依赖 Sentinel Dashboard,但两者结合使用可以有效的提高效率,让 Sentinel 发挥它最大的作用。
Sentinel 的基本概念有两个,它们分别是:资源和规则。
基本概念 | 描述 |
---|---|
资源 | 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。我们可以通过 Sentinel 提供的 API 来定义一个资源,使其能够被 Sentinel 保护起来。通常情况下,我们可以使用方法名、URL 甚至是服务名来作为资源名来描述某个资源。 |
规则 | 围绕资源而设定的规则。Sentinel 支持流量控制、熔断降级、系统保护、来源访问控制和热点参数等多种规则,所有这些规则都可以动态实时调整。 |
@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表。
属性 | 说明 | 必填与否 | 使用要求 |
---|---|---|---|
value | 用于指定资源的名称 | 必填 | - |
entryType | entry 类型 | 可选项(默认为 EntryType.OUT) | - |
blockHandler | 服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException 异常的。简单点说,该属性用于指定服务限流后的后续处理逻辑。 | 可选项 | blockHandler 函数访问范围需要是 public;返回类型需要与原方法相匹配;参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
blockHandlerClass | 若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 | 不能单独使用,必须与 blockHandler 属性配合使用;该属性指定的类中的 blockHandler 函数必须为 static 函数,否则无法解析。 |
fallback | 用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 | 返回值类型必须与原函数返回值类型一致;方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
fallbackClass | 若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 | 不能单独使用,必须与 fallback 或 defaultFallback 属性配合使用;该属性指定的类中的 fallback 函数必须为 static 函数,否则无法解析。 |
defaultFallback | 默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 | 返回值类型必须与原函数返回值类型一致;方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;defaultFallback |
exceptionsToIgnore | 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 | 可选项 | - |
注:在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。
Sentinel 提供了一个轻量级的开源控制台 Sentinel Dashboard,它提供了机器发现与健康情况管理、监控(单机和集群)、规则管理与推送等多种功能。
Sentinel 控制台提供的功能如下:
Sentinel Dashboard 是我们配置和管理规则(例如流控规则、熔断降级规则等)的重要入口之一。通过它,我们不仅可以对规则进行配置和管理,还能实时查看规则的效果。
下面我们就来演示下,如何下载和安装 Sentinel 控制台,具体步骤如下。
图1:Sentinel 控制台下载
java -jar sentinel-dashboard-1.8.2.jar
图2:Sentinel 控制台登录页
图3:Sentinel 控制台主页
Sentinel 的开发流程如下:
为了减少开发的复杂程度,Sentinel 对大部分的主流框架都进行了适配,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux 和 Reactor 等。以 Spring Cloud 为例,我们只需要引入 spring-cloud-starter-alibaba-sentinel 的依赖,就可以方便地将 Sentinel 整合到项目中。
下面我们就通过一个简单的实例,演示如何将 Sentinel 整合到 Spring Cloud 项目中,步骤如下。
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-sentinel-service-8401 0.0.1-SNAPSHOT spring-cloud-alibaba-sentinel-service-8401 Demo project for Spring Boot 1.8 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.csp sentinel-datasource-nacos org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
server:port: 8401 #端口
spring:application:name: sentinel-service #服务名cloud:nacos:discovery:#Nacos服务注册中心(集群)地址server-addr: localhost:1111sentinel:transport:#配置 Sentinel dashboard 地址dashboard: localhost:8080#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口port: 8719
management:endpoints:web:exposure:include: '*'
package net.demo.c.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class SentinelFlowLimitController {@Value("${server.port}")private String serverPort;@GetMapping("/testA")public String testA() {return "提醒您,服务访问成功------testA";}@GetMapping("/testB")public String testB() {return "提醒您,服务访问成功------testB"}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaSentinelService8401Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSentinelService8401Application.class, args);}
}
依次启动 Nacos Server 集群、 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”.
使用浏览器访问 Sentinel 控制台主页,我们发现在“首页”下方新增了一个“sentinel-servcie”的菜单,而这正是 spring-cloud-alibaba-sentinel-service-8401 的服务名(spring.application.name),说明 Sentinel 已经监控到这个服务,如下图。
图5:Sentinel 控制台主页
图6:Sentinel 实时监控
资源是 Sentinel 中的核心概念之一。在项目开发时,我们只需要考虑这个服务、方法或代码是否需要保护,如果需要保护,就可以将它定义为一个资源。
Sentinel 为我们提供了多种定义资源的方式:
Sentinel 对大部分的主流框架都进行了适配,我们只要引入相关的适配模块(例如 spring-cloud-starter-alibaba-sentinel),Snetinel 就会自动将项目中的服务(包括调用端和服务端)定义为资源,资源名就是服务的请求路径。此时,我们只要再定义一些规则,这些资源就可以享受到 Sentinel 的保护。
我们可以在 Sentinel 控制台的“簇点链路”中,直接查看被 Sentinel 监控的资源,如下图。
图7:Sentinel 控制台-簇点链路
通过 SphU 手动定义资源
Sentinel 提供了一个名为 SphU 的类,它包含的 try-catch 风格的 API ,可以帮助我们手动定义资源。
下面我们就通过一个实例,来演示下如何通过 SphU 定义资源。
package net.demo.c.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphO;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@Slf4j
public class SentinelFlowLimitController {@Value("${server.port}")private String serverPort;@GetMapping("/testA")public String testA() {return testAbySphU();}@GetMapping("/testB")public String testB() {return "提醒您,服务访问成功------testB";}/*** 通过 SphU 手动定义资源* @return*/public String testAbySphU() {Entry entry = null;try {entry = SphU.entry("testAbySphU");//您的业务逻辑 - 开始log.info("提醒您,服务访问成功------testA:"+serverPort);return "提醒您,服务访问成功------testA:"+serverPort;//您的业务逻辑 - 结束} catch (BlockException e1) {//流控逻辑处理 - 开始log.info("提醒您,testA 服务被限流");return "提醒您,testA 服务被限流";//流控逻辑处理 - 结束} finally {if (entry != null) {entry.exit();}}}
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”,结果如下。
提醒您,服务访问成功------testA:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
图8:Sentinel 通过 SphU 定义资源
通过 SphO 手动定义资源
Sentinel 还提供了一个名为 SphO 的类,它包含了 if-else 风格的 API,能帮助我们手动定义资源。通过这种方式定义的资源,发生了限流之后会返回 false,此时我们可以根据返回值,进行限流之后的逻辑处理。
下面我们就通过一个实例,来演示下如何通过 SphO 定义资源。
package net.demo.c.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphO;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@Slf4j
public class SentinelFlowLimitController {@Value("${server.port}")private String serverPort;@GetMapping("/testA")public String testA() {return testAbySphU();}@GetMapping("/testB")public String testB() {return testBbySphO();}/*** 通过 SphU 手动定义资源** @return*/public String testAbySphU() {Entry entry = null;try {entry = SphU.entry("testAbySphU");//您的业务逻辑 - 开始log.info("提醒您,服务访问成功------testA:" + serverPort);return "提醒您,服务访问成功------testA:" + serverPort;//您的业务逻辑 - 结束} catch (BlockException e1) {//流控逻辑处理 - 开始log.info("提醒您,testA 服务被限流");return "提醒您,testA 服务被限流";//流控逻辑处理 - 结束} finally {if (entry != null) {entry.exit();}}}/*** 通过 SphO 手动定义资源** @return*/public String testBbySphO() {if (SphO.entry("testBbySphO")) {// 务必保证finally会被执行try {log.info("提醒您,服务访问成功------testB:" + serverPort);return "提醒您,服务访问成功------testB:" + serverPort;} finally {SphO.exit();}} else {// 资源访问阻止,被限流或被降级//流控逻辑处理 - 开始log.info("提醒您,testB 服务被限流");return "提醒您,testB 服务被限流";//流控逻辑处理 - 结束}}
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testB”,结果如下。
提醒您,服务访问成功------testB:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
Sentinel SphO
图9:Sentinel 通过 SphO 定义资源
注解方式定义资源(推荐)
除了以上两种方式外,我们还可以通过 Sentinel 提供的 @SentinelResource 注解定义资源。
下面我们就通过一个实例,来演示下如何通过 @SentinelResource 注解定义资源。
@GetMapping("/testC")
@SentinelResource(value = "testCbyAnnotation") //通过注解定义资源
public String testC() {log.info("提醒您,服务访问成功------testC:" + serverPort);return "提醒您,服务访问成功------testC:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testC”,结果如下。
服务访问成功------testC:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
图10:Sentinel 注解方式定义资源
Sentinel 流量控制
任何系统处理请求的能力都是有限的,但任意时间内到达系统的请求量往往是随机且不可控的,如果在某一个瞬时时刻请求量急剧增,那么系统就很有可能被瞬时的流量高峰冲垮。为了避免此类情况发生,我们都需要根据系统的处理能力对请求流量进行控制,这就是我们常说的“流量控制”,简称“流控”。
Sentinel 作为一种轻量级高可用流量控制组件,流量控制是它最主要的工作之一。
我们可以针对资源定义流控规则,Sentinel 会根据这些规则对流量相关的各项指标进行监控。当这些指标当达到或超过流控规则规定的阈值时,Sentinel 会对请求的流量进行限制(即“限流”),以避免系统被瞬时的流量高峰冲垮,保障系统的高可用性。
一条流量规则主要由下表中的属性组成,我们可以通过组合这些属性来实现不同的限流效果。
属性 | 说明 | 默认值 |
---|---|---|
资源名 | 的作用对象。 | - |
阈值 | 值。 | - |
阈值类型 | 流控阈值的类型,包括 QPS 或并发线程数。 | QPS |
针对来源 | 流控针对的调用来源。 | default,表示不区分调用来源 |
流控模式 | 调用关系限流策略,包括直接、链路和关联。 | 直接 |
流控效果 | 流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流。 | 直接拒绝 |
注:QPS 表示并发请求数,换句话说就是,每秒钟最多通过的请求数。
同一个资源可以创建多条流控规则,Sentinel 会遍历这些规则,直到有规则触发限流或者所有规则遍历完毕为止。
Sentinel 触发限流时,资源会抛出 BlockException 异常,此时我们可以捕捉 BlockException 来自定义被限流之后的处理逻辑。
注意:这里我们主要讲解 Sentinel 流控规则的定义与使用,至于详细的流控规则配置,请参考 Sentinel 官方流控文档。
通过 Sentinel 控制台定义流控规则
我们可以通过 Sentinel 控制台,直接对资源定义流控规则,操作步骤如下。
/*** 通过 Sentinel 控制台定义流控规则** @return*/
@GetMapping("/testD")
public String testD() {log.info("提醒您,服务访问成功------testD:" + serverPort);return "提醒您,服务访问成功------testD:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testD”,结果如下。
提醒您,服务访问成功------testD:8401
使用浏览器访问“http://localhost:8080”,登陆 Sentinel 控制台主页,点击 sentinel-sevice 下的“簇点链路”,结果如下图。
图11:Sentinel 控制台定义流控规则
图12:Sentinel 控制台定义流控规则
图13:Sentinel 控制台流控规则列表
若页面中出现以上信息,则说明该服务已被限流,但这种提示是 Sentinel 系统自动生成的,用户体验不好。
/*** 通过 Sentinel 控制台定义流控规则**/
@GetMapping("/testD")
@SentinelResource(value = "testD-resource", blockHandler = "blockHandlerTestD") //通过注解定义资源
public String testD() {log.info("提醒您,服务访问成功------testD:" + serverPort);return "提醒您,服务访问成功------testD:" + serverPort;
}
/*** 限流之后的逻辑* @param exception* @return*/
public String blockHandlerTestD(BlockException exception) {log.info(Thread.currentThread().getName() + "提醒您,TestD服务访问失败! 您已被限流,请稍后重试");return "提醒您,TestD服务访问失败! 您已被限流,请稍后重试";
}
在以上代码中,我们通过 @SentinelResource 注解的 blockHandler 属性指定了一个 blockHandler 函数,进行限流之后的后续处理。
使用 @SentinelResource 注解的 blockHandler 属性时,需要注意以下事项:
blockHandler 函数访问范围需要是 public;
返回类型需要与原方法相匹配;
参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;
blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
请务必添加 blockHandler 属性来指定自定义的限流处理方法,若不指定,则会跳转到错误页(用户体验不好)。
图14:Sentinel 控制台-簇点链路
图15:Sentinel 控制台新增流控规则
提醒您,TestD服务访问失败! 您已被限流,请稍后重试
通过代码定义流控规则
我们还可以在服务代码中,调用 FlowRuleManager 类的 loadRules() 方法来定义流控规则,该方法需要一个 FlowRule 类型的 List 集合作为其参数,示例代码如下。
public static void loadRules(List rules) {currentProperty.updateValue(rules);
}
FlowRule 可以通过以下属性定义流控规则,如下表。
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,即流控规则的作用对象 | - |
count | 限流的阈值。 | - |
grade | 流控阈值的类型,包括 QPS 或并发线程数 | QPS |
limitApp | 流控针对的调用来源 | default,表示不区分调用来源 |
strategy | 调用关系限流策略,包括直接、链路和关联 | 直接 |
controlBehavior | 流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流 | 直接拒绝 |
下面我们就通过一个简单的实例,来演示下如何通过代码定义流控规则,步骤如下。
/*** 通过代码定义流量控制规则*/
private static void initFlowRules() {List rules = new ArrayList<>();//定义一个限流规则对象FlowRule rule = new FlowRule();//资源名称rule.setResource("testD-resource");//限流阈值的类型rule.setGrade(RuleConstant.FLOW_GRADE_QPS);// 设置 QPS 的阈值为 2rule.setCount(2);rules.add(rule);//定义限流规则FlowRuleManager.loadRules(rules);
}
@GetMapping("/testD")
@SentinelResource(value = "testD-resource", blockHandler = "blockHandlerTestD") //通过注解定义资源
public String testD() {initFlowRules(); //调用初始化流控规则的方法log.info("提醒您,服务访问成功------testD:" + serverPort);return "提醒您,服务访问成功------testD:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,并使用浏览器访问“http://localhost:8401/testD”,结果如下。
提醒您,服务访问成功------testD:8401
快速连续(频率大于每秒钟 2 次)访问“http://localhost:8401/testD”,结果如下。
提醒您,TestD服务访问失败! 您已被限流,请稍后重试
打开命令行窗口,执行以下命令查看资源的实时统计信息。
curl http://localhost:8719/cnode?id=testD-resource
控制台输出内容如下。
idx id thread pass blocked success total aRt 1m-pass 1m-block 1m-all exceptio
2 testD-resource 0 0.0 0.0 0.0 0.0 0.0 10 16 26 0.0
实时统计信息各列名说明如下:
除了流量控制以外,对调用链路中不稳定资源的熔断降级,也是保障服务高可用的重要措施之一。
在分布式微服务架构中,一个系统往往由多个服务组成,不同服务之间相互调用,组成复杂的调用链路。如果链路上的某一个服务出现故障,那么故障就会沿着调用链路在系统中蔓延,最终导致整个系统瘫痪。Sentinel 提供了熔断降级机制就可以解决这个问题。
Sentinel 的熔断将机制会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),暂时切断对这个资源的调用,以避免局部不稳定因素导致整个系统的雪崩。
熔断降级作为服务保护自身的手段,通常在客户端(调用端)进行配置,资源被熔断降级最直接的表现就是抛出 DegradeException 异常。
Sentinel 熔断策略
Sentinel 提供了 3 种熔断策略,如下表所示。
熔断策略 | 说明 |
---|---|
慢调用比例(SLOW_REQUEST_RATIO) | 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大响应时间),若请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则再次被熔断。 |
异常比例 (ERROR_RATIO) | 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目且异常的比例大于阈值,则在接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。 |
异常数 (ERROR_COUNT) | 当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 |
注意:Sentinel 1.8.0 版本对熔断降级特性进行了全新的改进升级,以上熔断策略针对的是 Sentinel 1.8.0 及以上版本。
Sentinel 熔断降级中共涉及 3 种状态,熔断状态的之间的转换过程如下图。
图16:Sentinel 熔断状态转换
Sentinel 熔断降级中共涉及 3 种状态,如下表。
状态 | 说明 | 触发条件 |
---|---|---|
熔断关闭状态(CLOSED) | 处于关闭状态时,请求可以正常调用资源。 满足以下任意条件,Sentinel 熔断器进入熔断关闭状态:全部请求访问成功。单位统计时长(statIntervalMs)内请求数目小于设置的最小请求数目。未达到熔断标准,例如服务超时比例、异常数、异常比例未达到阈值。处于探测恢复状态时,下一个请求访问成功。 | |
熔断开启状态(OPEN) | 处于熔断开启状态时,熔断器会一定的时间(规定的熔断时长)内,暂时切断所有请求对该资源的调用,并调用相应的降级逻辑使请求快速失败避免系统崩溃。 满足以下任意条件,Sentinel 熔断器进入熔断开启状态:单位统计时长内请求数目大于设置的最小请求数目,且已达到熔断标准,例如请求超时比例、异常数、异常比例达到阈值。处于探测恢复状态时,下一个请求访问失败。 | |
探测恢复状态(HALF-OPEN) | 处于探测恢复状态时,Sentinel 熔断器会允许一个请求调用资源。则若接下来的一个请求成功完成(没有错误)则结束熔断,熔断器进入熔断关闭(CLOSED)状态;否则会再次被熔断,熔断器进入熔断开启(OPEN)状态。 在熔断开启一段时间(降级窗口时间或熔断时长,单位为 s)后,Sentinel 熔断器自动会进入探测恢复状态。 |
Sentinel 熔断降级规则包含多个重要属性,如下表所示。
属性 | 说明 | 默认值 | 使用范围 |
---|---|---|---|
资源名 | 规则的作用对象。 | - | 所有熔断策略 |
熔断策略 | Sentinel 支持3 中熔断策略:慢调用比例、异常比例、异常数策略。 | 慢调用比例 | 所有熔断策略 |
最大 RT | 请求的最大相应时间,请求的响应时间大于该值则统计为慢调用。 | - | 慢调用比例 |
熔断时长 | 熔断开启状态持续的时间,超过该时间熔断器会切换为探测恢复状态(HALF-OPEN),单位为 s。 | - | 所有熔断策略 |
最小请求数 | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入)。 | 5 | 所有熔断策略 |
统计时长 | 熔断触发需要统计的时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入)。 | 1000 ms | 所有熔断策略 |
比例阈值 | 分为慢调用比例阈值和异常比例阈值,即慢调用或异常调用占所有请求的百分比,取值范围 [0.0,1.0]。 | - | 慢调用比例 、异常比例 |
异常数 | 请求或调用发生的异常的数量。 | - | 异常数 |
Sentinel 实现熔断降级的步骤如下:
我们可以通过 Sentinel 控制台直接对资源定义熔断降级规则。
下面我们通过一个实例,来演示如何通过 Sentinel 控制台,对资源定义降级规则。
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (`dept_no` int NOT NULL AUTO_INCREMENT,`dept_name` varchar(255) DEFAULT NULL,`db_source` varchar(255) DEFAULT NULL,PRIMARY KEY (`dept_no`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;INSERT INTO `dept` VALUES ('1', '开发部', 'demobang_jdbc');
INSERT INTO `dept` VALUES ('2', '人事部', 'demobang_jdbc');
INSERT INTO `dept` VALUES ('3', '财务部', 'demobang_jdbc');
INSERT INTO `dept` VALUES ('4', '市场部', 'demobang_jdbc');
INSERT INTO `dept` VALUES ('5', '运维部', 'demobang_jdbc');
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-provider-mysql-8003 0.0.1-SNAPSHOT spring-cloud-alibaba-provider-mysql-8003 Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery net.demo.c spring-cloud-alibaba-api ${project.version} junit junit 4.12 mysql mysql-connector-java 5.1.49 ch.qos.logback logback-core org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
server:port: 8003 #端口
spring:application:name: spring-cloud-alibaba-provider-mysqlcloud:nacos:discovery:server-addr: localhost:1111######################### 数据库连接 #################################datasource:username: root #数据库登陆用户名password: root #数据库登陆密码url: jdbc:mysql://127.0.0.1:3306/spring_cloud_db2 #数据库urldriver-class-name: com.mysql.jdbc.Driver
management:endpoints:web:exposure:include: "*" # * 在yaml 文件属于关键字,所以需要加引号
###################################### MyBatis 配置 ######################################
mybatis:# 指定 mapper.xml 的位置mapper-locations: classpath:mybatis/mapper/*.xml#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名type-aliases-package: net.demo.c.entityconfiguration:#默认开启驼峰命名法,可以不用设置该属性map-underscore-to-camel-case: true
package net.demo.c.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor //无参构造函数
@Data // 提供类的get、set、equals、hashCode、canEqual、toString 方法
@Accessors(chain = true)
public class Dept implements Serializable {private Integer deptNo;private String deptName;private String dbSource;
}
package net.demo.c.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult {private Integer code;private String message;private T data;public CommonResult(Integer code, String message) {this(code, message, null);}
}
package net.demo.c.mapper;
import net.demo.c.entity.Dept;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DeptMapper {//根据主键获取数据Dept selectByPrimaryKey(Integer deptNo);//获取表中的全部数据List GetAll();
}
dept_no, dept_name, db_source
package net.demo.c.service;
import net.demo.c.entity.Dept;
import java.util.List;
public interface DeptService {Dept get(Integer deptNo);List selectAll();
}
package net.demo.c.service.impl;
import net.demo.c.entity.Dept;
import net.demo.c.mapper.DeptMapper;
import net.demo.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("deptService")
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Overridepublic Dept get(Integer deptNo) {return deptMapper.selectByPrimaryKey(deptNo);}@Overridepublic List selectAll() {return deptMapper.GetAll();}
}
package net.demo.c.controller;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.CommonResult;
import net.demo.c.entity.Dept;
import net.demo.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class DeptController {@Autowiredprivate DeptService deptService;@Value("${server.port}")private String serverPort;@RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)public CommonResult get(@PathVariable("id") int id) {log.info("端口:" + serverPort + "\t+ dept/get/");try {TimeUnit.SECONDS.sleep(1);log.info("休眠 1秒");} catch (InterruptedException e) {e.printStackTrace();}Dept dept = deptService.get(id);CommonResult result = new CommonResult(200, "from mysql,serverPort: " + serverPort, dept);return result;}@RequestMapping(value = "/dept/list", method = RequestMethod.GET)public CommonResult> list() {log.info("端口:" + serverPort + "\t+ dept/list/");List depts = deptService.selectAll();CommonResult> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, depts);return result;}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaProviderMysql8003Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaProviderMysql8003Application.class, args);}
}
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-consumer-mysql-8803 0.0.1-SNAPSHOT spring-cloud-alibaba-consumer-mysql-8803 Demo project for Spring Boot 1.8 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba.cloud spring-cloud-starter-alibaba-sentinel net.demo.c spring-cloud-alibaba-api ${project.version} org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
server:port: 8803
spring:application:name: spring-cloud-alibaba-consumer-mysql-feigncloud:nacos:discovery:server-addr: localhost:1111sentinel:transport:dashboard: localhost:8080port: 8719
# 以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码 服务提供者的服务名
service-url:nacos-user-service: http://spring-cloud-alibaba-provider-mysql #消费者要方位的微服务名称
# 激活Sentinel对Feign的支持
feign:sentinel:enabled: true
package net.demo.c.service;
import net.demo.c.entity.CommonResult;
import net.demo.c.entity.Dept;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
@Component
@FeignClient(value = "spring-cloud-alibaba-provider-mysql", fallback = DeptFallbackService.class)
public interface DeptFeignService {@RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)public CommonResult get(@PathVariable("id") int id);@RequestMapping(value = "/dept/list", method = RequestMethod.GET)public CommonResult> list();
}
package net.demo.c.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.EventObserverRegistry;
import com.alibaba.csp.sentinel.util.TimeUtil;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.CommonResult;
import net.demo.c.entity.Dept;
import net.demo.c.service.DeptFeignService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@RestController
@Slf4j
public class DeptFeignController {@ResourceDeptFeignService deptFeignService;@RequestMapping(value = "consumer/feign/dept/get/{id}", method = RequestMethod.GET)@SentinelResource(value = "fallback", fallback = "handlerFallback")public CommonResult get(@PathVariable("id") int id) {monitor();System.out.println("--------->>>>主业务逻辑");CommonResult result = deptFeignService.get(id);if (id == 6) {System.err.println("--------->>>>主业务逻辑,抛出非法参数异常");throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");//如果查到的记录也是 null 也控制正异常} else if (result.getData() == null) {System.err.println("--------->>>>主业务逻辑,抛出空指针异常");throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");}return result;}@RequestMapping(value = "consumer/feign/dept/list", method = RequestMethod.GET)public CommonResult> list() {return deptFeignService.list();}//处理异常的回退方法(服务降级)public CommonResult handlerFallback(@PathVariable int id, Throwable e) {System.err.println("--------->>>>服务降级逻辑");Dept dept = new Dept(id, "null", "null");return new CommonResult(444, "提醒您,服务被降级!异常信息为:" + e.getMessage(), dept);}/*** 自定义事件监听器,监听熔断器状态转换*/public void monitor() {EventObserverRegistry.getInstance().addStateChangeObserver("logging",(prevState, newState, rule, snapshotValue) -> {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");if (newState == CircuitBreaker.State.OPEN) {// 变换至 OPEN state 时会携带触发时的值System.err.println(String.format("%s -> OPEN at %s, 发送请求次数=%.2f", prevState.name(),format.format(new Date(TimeUtil.currentTimeMillis())), snapshotValue));} else {System.err.println(String.format("%s -> %s at %s", prevState.name(), newState.name(),format.format(new Date(TimeUtil.currentTimeMillis()))));}});}
}
在以上代码中,我们通过 @SentinelResource 注解的 fallback 属性指定了一个 fallback 函数,进行熔断降级的后续处理。
使用 @SentinelResource 注解的 blockHandler 属性时,需要注意以下事项:
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringCloudAlibabaConsumerMysql8803Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaConsumerMysql8803Application.class, args);}
}
{"code":200,"message":"from mysql,serverPort: 8003","data":{"deptNo":3,"deptName":"财务部","dbSource":"spring_cloud_db2"}}
{"code":444,"message":"提醒您,服务被降级!异常信息为:NullPointerException,该ID没有对应记录,空指针异常","data":{"deptNo":7,"deptName":"null","dbSource":"null"}}
--------->>>>主业务逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务熔断降级逻辑
图17:Sentinel 熔断降级
图18:Sentinel 控制台定义熔断规则
在上图中,熔断规则各属性说明如下:
我们为 fallback 资源定义的熔断规则为:当 1 秒钟内请求数大于 2 个,且请求异常数大于 1 时,服务被熔断,熔断的时长为 5 秒钟。
{"code":444,"message":"提醒您,服务被降级!异常信息为:null","data":{"deptNo":7,"deptName":"null","dbSource":"null"}}
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
CLOSED -> OPEN at 2021-11-17 14:06:47, 发送请求次数=2.00
从控制台输出可以看出,在 14点 06 分 47 秒时,熔断器从熔断关闭状态(CLOSED)切换到熔断开启状态(OPEN)。
在熔断开启开启状态下,使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/4”,结果页面输出如下。
{“code”:444,“message”:“提醒您,服务被降级!异常信息为:null”,“data”:{“deptNo”:4,“deptName”:“null”,“dbSource”:“null”}}
控制台输出如下。
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
CLOSED -> OPEN at 2021-11-17 14:09:19, 发送请求次数=2.00
--------->>>>服务降级逻辑
--------->>>>服务降级逻辑
--------->>>>服务降级逻辑
从控制台输出可知,当熔断器处于熔断开启状态时,所有的请求都直接交给降级逻辑处理。
{"code":200,"message":"from mysql,serverPort: 8003","data":{"deptNo":4,"deptName":"市场部","dbSource":"spring_cloud_db2"}}
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
CLOSED -> OPEN at 2021-11-17 14:09:19, 发送请求次数=2.00
--——--->>>>服务降级逻辑
--------->>>>服务降级逻辑
--------->>>>服务降级逻辑
OPEN -> HALF_OPEN at 2021-11-17 14:09:24
--——--->>>>主业务逻辑
HALF_OPEN -> CLOSED at 2021-11-17 14:09:24
--------->>>>主业务逻辑
--------->>>>主业务逻辑
从以上控制台输出可知,熔断器在经历了 5 秒的熔断时长后,自动切换到了探测恢复状态(HALF-OPEN),并在下一个请求成功的情况下,结束了熔断开启状态,切换到了熔断关闭状态(CLOSED)。
通过代码定义熔断规则
Sentinel 核心库中提供了的一个名为 DegradeRuleManager 类,我们可以通过调用它的 loadRules() 方法来定义熔断降级规则,该方法需要一个 DegradeRule 类型的 List 参数。
public static void loadRules(List rules) {try {currentProperty.updateValue(rules);} catch (Throwable var2) {RecordLog.error("[DegradeRuleManager] Unexpected error when loading degrade rules", var2);}
}
DegradeRule 类可以用来定义一条熔断规则,它包含多个与熔断规则相关的属性,如下表。
下面我们就通过一个实例,演示下如何通过代码定义熔断规则,步骤如下。
/*** 初始化熔断策略*/
private static void initDegradeRule() {List rules = new ArrayList<>();DegradeRule rule = new DegradeRule("fallback");//熔断策略为异常比例rule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());//异常比例阈值rule.setCount(0.7);//最小请求数rule.setMinRequestAmount(100);//统计市场,单位毫秒rule.setStatIntervalMs(30000);//熔断市场,单位秒rule.setTimeWindow(10);rules.add(rule);DegradeRuleManager.loadRules(rules);
}
@RequestMapping(value = "consumer/feign/dept/get/{id}", method = RequestMethod.GET)
@SentinelResource(value = "fallback", fallback = "handlerFallback")
public CommonResult get(@PathVariable("id") int id) {initDegradeRule();monitor();System.out.println("--------->>>>主业务逻辑");CommonResult result = deptFeignService.get(id);if (id == 6) {System.err.println("--------->>>>主业务逻辑,抛出非法参数异常");throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");//如果查到的记录也是 null 也控制正异常} else if (result.getData() == null) {System.err.println("--------->>>>主业务逻辑,抛出空指针异常");throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");}return result;
}
重启 spring-cloud-alibaba-consumer-mysql-8803,使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/1”,结果如下。
“code”:200,“message”:“from mysql,serverPort: 8003”,“data”:{“deptNo”:1,“deptName”:“开发部”,“dbSource”:“spring_cloud_db2”}}
使用浏览器访问 Sentinel 控制主页,点击“熔断规则”查看熔断规则列表,结果如下图。
图19:Sentinel 代码定义熔断规则
从上图我们看到,通过代码也能够为资源定义熔断规则。
随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。
例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:
为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。
Seata,读音 [si:t], 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。
阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。
阿里巴巴对于分布式事务问题先后发布了以下解决方案:
分布式事务主要涉及以下概念:
我们可以将分布式事务理解成一个包含了若干个分支事务的全局事务。全局事务的职责是协调其管辖的各个分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足 ACID 特性的本地事务。
Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。
XID 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
Seata 定义了 3 个核心组件:
以上三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。
图1:Sentinel 的工作流程
Seata 的整体工作流程如下:
1.TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
2.XID 通过服务的调用链传递到其他服务;
3.RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
4.TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
5.TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。
在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:
此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。不同数据库在创建 UNDO_LOG 表时会略有不同,以 MySQL 为例,其 UNDO_LOG 表的创表语句如下:
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。
假设某数据库中存在一张名为 webset 的表,表结构如下。
列名 | 类型 | 主键 |
---|---|---|
id | bigint(20) | √ |
name | varchar(255) | |
url | varchar(255) |
在某次分支事务中,我们需要在 webset 表中执行以下操作。
update webset set url = 'c.demo.net' where name = 'XX测试';
Seata AT 模式一阶段的工作流程如下图所示。
图2:Seata AT 模式一阶段
Seata AT 模式一阶段工作流程如下。
获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(webset)、判断条件(where name = ‘XX测试’)等相关信息。
查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。
select id,name,url from webset where name='XX测试';
执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
id | name | url |
---|---|---|
1 | XX测试 | xxx.net |
执行业务 SQL(update webset set url = ‘c.demo.net’ where name = ‘XX测试’;),将这条记录的 url 修改为 c.demo.net。
查询后镜像:根据“前镜像数据”的主键(id : 1),生成“后镜像查询语句”。
select id,name,url from webset where id= 1;
执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。
id | name | url |
---|---|---|
1 | XX测试 | c.xxx.net |
{"@class": "io.seata.rm.datasource.undo.BranchUndoLog","xid": "172.26.54.1:8091:5962967415319516023","branchId": 5962967415319516027,"sqlUndoLogs": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.undo.SQLUndoLog","sqlType": "UPDATE","tableName": "webset","beforeImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords","tableName": "webset","rows": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Row","fields": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "id","keyType": "PRIMARY_KEY","type": -5,"value": ["java.lang.Long",1]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "url","keyType": "NULL","type": 12,"value": "demo.net"}]]}]]},"afterImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords","tableName": "webset","rows": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Row","fields": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "id","keyType": "PRIMARY_KEY","type": -5,"value": ["java.lang.Long",1]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "url","keyType": "NULL","type": 12,"value": "c.demo.net"}]]}]]}}]]
}
以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。
本地事务提交:将业务数据的更新和前面生成的 UNDO_LOG 一并提交。
上报执行结果:将本地事务提交的结果上报给 TC。
当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。
若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。
若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。
查找 UNDO_LOG 记录:通过 XID 和分支事务 ID(Branch ID) 查找所有的 UNDO_LOG 记录。
数据校验:将 UNDO_LOG 中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。
生成回滚语句:根据 UNDO_LOG 中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:
update webset set url= 'demo.net' where id = 1;
还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。
提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
图3:Seata 服务器下载页面
图4:Seata Server 目录结构
Seata Server 目录中包含以下子目录:
所谓“配置中心”,就像是一个“大衣柜”,内部存放着各种各样的配置文件,我们可以根据自己的需要从其中获取指定的配置文件,加载到对应的客户端中。
Seata 支持多种配置中心:
对于 Seata 来说,Nacos 是一种重要的配置中心实现。
Seata 整合 Nacos 配置中心的操作步骤十分简单,大致步骤如下.
我们需要将 nacos-client 的 Maven 依赖添加到项目的 pom.xml 文件中:
io.seata seata-spring-boot-starter 最新版
com.alibaba.nacos nacos-client 1.2.0及以上版本
在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。
com.alibaba.cloud spring-cloud-starter-alibaba-seata
在 Seata Server 安装目录下的 config/registry.conf 中,将配置方式(config.type)修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,示例配置如下。
config {# Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心#配置方式修改为 nacostype = "nacos"nacos {#修改为使用的 nacos 服务器地址serverAddr = "127.0.0.1:1111"#配置中心的命名空间namespace = ""#配置中心所在的分组group = "SEATA_GROUP"#Nacos 配置中心的用户名username = "nacos"#Nacos 配置中心的密码password = "nacos"}
}
我们可以在 Seata Client(即微服务架构中的服务)中,通过 application.yml 等配置文件对 Nacos 配置中心进行配置,示例代码如下。
seata:config:type: nacosnacos:server-addr: 127.0.0.1:1111 # Nacos 配置中心的地址group : "SEATA_GROUP" #分组namespace: ""username: "nacos" #Nacos 配置中心的用于名password: "nacos" #Nacos 配置中心的密码
在完成了 Seata 服务端和客户端的相关配置后,接下来,我们就可以将配置上传的 Nacos 配置中心了,操作步骤如下。
我们可以通过 Seata Server 源码/script/config-center 目录中获取 config.txt,然后根据自己需要修改其中的配置,如下图。
图5:config.txt
在 seata-1.4.2\script\config-center\nacos 目录下,右键鼠标选择 Git Bush Here,并在弹出的 Git 命令窗口中执行以下命令,将 config.txt 中的配置上传到 Nacos 配置中心。
sh nacos-config.sh -h 127.0.0.1 -p 1111 -g SEATA_GROUP -u nacos -w nacos
Git 命令各参数说明如下:
在以上所有步骤完成后,启动 Nacos Server,登陆 Nacos 控制台查看配置列表,结果如下图。
图6:Seata Nacos 配置中心
所谓“注册中心”,可以说是微服务架构中的“通讯录”,它记录了服务与服务地址的映射关系。
在分布式微服务架构中,各个微服务都可以将自己注册到注册中心,当其他服务需要调用某个服务时,就可以从这里找到它的服务地址进行调用,常见的服务注册中心有 Nacos、Eureka、zookeeper 等。
Seata 支持多种服务注册中心:
Seata 通过这些服务注册中心,我们可以获取 Seata Sever 的服务地址,进行调用。
对于 Seata 来说,Nacos 是一种重要的注册中心实现。
Seata 整合 Nacos 注册中心的步骤十分简单,步骤如下。
将 nacos-client 的 Maven 依赖添加到项目的 pom.xml 文件中:
io.seata seata-spring-boot-starter 最新版
com.alibaba.nacos nacos-client 1.2.0及以上版本
在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。
com.alibaba.cloud spring-cloud-starter-alibaba-seata
在 Seata Server 安装目录下的 config/registry.conf 中,将注册方式(registry.type)修改为 Nacos,并对 Nacos 注册中心的相关信息进行配置,示例配置如下。
registry {# Seata 支持 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 作为其注册中心# 将注册方式修改为 nacostype = "nacos"nacos {application = "seata-server"# 修改 nacos 注册中心的地址serverAddr = "127.0.0.1:1111"group = "SEATA_GROUP"namespace = ""cluster = "default"username = ""password = ""}
}
我们可以在 Seata Client 的 application.yml 中,对 Nacos 注册中心进行配置,示例配置如下。
seata:registry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:1111 # Nacos 注册中心的地址group : "SEATA_GROUP" #分组namespace: ""username: "nacos" #Nacos 注册中心的用户名password: "nacos" # Nacos 注册中心的密码
在以上所有步骤完成后,先启动 Nacos Server 再启动 Seata Server,登录 Nacos 控制台查看服务列表,结果如下图。
从图 9 可以看出,seata-server 服务已经注册到了 Nacos 注册中心。
事务分组是 Seata 提供的一种 TC(Seata Server) 服务查找机制。
Seata 通过事务分组获取 TC 服务,流程如下:
在应用中配置事务分组。
下面我们以 Nacos 服务注册中心为例,介绍 Seata 事务的使用。
在 Seata Server 的 config/registry.conf 中,进行如下配置。
registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos" #使用 Nacos作为注册中心nacos {serverAddr = "127.0.0.1:1111" # Nacos 注册中心的地址namespace = "" # Nacos 命名空间id,"" 为 Nacos 保留 public 空间控件,用户勿配置 namespace = "public"cluster = "c.demo.net" # seata-server在 Nacos 的集群名}
}
config {# file、nacos 、apollo、zk、consul、etcd3type = "nacos" # 使用nacos作为配置中心nacos {serverAddr = "localhost"namespace = ""cluster = "default"}
}
Seata Client 中 application.yml 的配置如下。
spring:alibaba:seata:tx-service-group: service-order-group #事务分组名
seata:registry:type: nacos #从 Nacos 获取 TC 服务nacos:server-addr: 127.0.0.1:1111config:type: nacos #使用 Nacos 作为配置中心nacos:server-addr: 127.0.0.1:1111name
在以上配置中,我们通过 spring.cloud.alibaba.seata.tx-service-group 来配置 Seata 事务分组名,其默认取值为:服务名-fescar-service-group。
将以下配置上传到 Nacos 配置中心。
service.vgroupMapping.service-order-group=c.demo.net
在以上配置中,
service-order-group:为事务分组的名称;
c.demo.net:为 TC 集群的名称。
先启动 Nacos,再启动 Seata Server,最后再启动 Seata Client。
Seata Client 在启动时,会从 application.yml 的配置中,根据 spring.cloud.alibaba.seata.tx-service-group 获取事务分组的名称:service-order-group。
使用事务分组名“service-order-group”拼接成“service.vgroupMapping.service-order-group”,并从 Nacos 配置中心获取该配置的取值,这个值就是 TC 集群的名称:“c.demo.net”。
根据 TC 集群名、Nacos 注册中心的地址(server-addr)以及命名空间(namespace),在 Nacos 注册中心找到真实的 TC 服务列表。
通过事务分组获取服务名,共需要以下 3 步:
1.服务启动时,从配置文件中获取服务分组的名称;
2.从配置中心,通过事务分组名获取 TC 集群名;
3.根据 TC 群组名以及其他信息构建服务名,获取真实的 TC 服务列表。
模式 | 说明 | 准备工作 |
---|---|---|
file | 文件存储模式,默认存储模式;该模式为单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 root.data,性能较高 | - |
db | 数据库存储模式;该模式为高可用模式,全局事务会话信息通过数据库共享,性能较低。 | 建数据库表 |
redis | 缓存处处模式;Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险, | 配置 redis 持久化配置 |
在 db 模式下,我们需要针对全局事务的会话信息创建以下 3 张数据库表。
在 MySQL 中,创建一个名为 seata 的数据库实例,并在该数据库内执行以下 SQL。
global_table 的建表 SQL 如下。
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
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_gmt_modified_status` (`gmt_modified`, `status`),KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8;
branch_table 的建表 SQL 如下。
-- 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 = InnoDBDEFAULT CHARSET = utf8;
lock_table 的建表 SQL 如下。
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key` VARCHAR(128) NOT NULL,`xid` VARCHAR(96),`transaction_id` BIGINT,`branch_id` BIGINT NOT NULL,`resource_id` VARCHAR(256),`table_name` VARCHAR(32),`pk` VARCHAR(36),`gmt_create` DATETIME,`gmt_modified` DATETIME,PRIMARY KEY (`row_key`),KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8;
registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa# 将注册方式修改为 nacostype = "nacos"nacos {application = "seata-server"# 修改 nacos 的地址serverAddr = "127.0.0.1:1111"group = "SEATA_GROUP"namespace = ""cluster = "default"username = ""password = ""}
}
config {# file、nacos 、apollo、zk、consul、etcd3#配置方式修改为 nacostype = "nacos"nacos {#修改为使用的 nacos 服务器地址serverAddr = "127.0.0.1:1111"namespace = ""group = "SEATA_GROUP"username = "nacos"password = "nacos"#不使用 seataServer.properties 方式配置#dataId = "seataServer.properties"}
}
1) 下载并解压 Seata Server 的源码 seata-1.4.2.zip,然后修改 seata-1.4.2/script/config-center 目录下的 config.txt,修改内容如下。```xml
#将 Seata Server 的存储模式修改为 db
store.mode=db
# 数据库驱动
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库 url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC
# 数据库的用户名
store.db.user=root
# 数据库的密码
store.db.password=root
# 自定义事务分组
service.vgroupMapping.service-order-gro
sh nacos-config.sh -h 127.0.0.1 -p 1111 -g SEATA_GROUP -u nacos -w nacos
=========================================================================
Complete initialization parameters, total-count:87 , failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.
图8:Seata 配置上传到 Nacos
注意:在使用 Git 命令将配置上传到 Nacos 前,应该先确保 Nacos 服务器已启动。
图9:Seata Server 启动脚本
Seata Server 启动日志如下。
16:52:48,549 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
16:52:48,549 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
16:52:48,550 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/C:/Users/79330/Desktop/seata-server-1.4.2/conf/logback.xml]
16:52:48,551 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
16:52:48,551 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/C:/Users/79330/Desktop/seata-server-1.4.2/lib/seata-server-1.4.2.jar!/logback.xml]
……
SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
SLF4J: See also http://www.slf4j.org/codes.html#replay
16:52:49.003 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry
16:52:49.008 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is C:\Users\79330\Desktop\seata-server-1.4.2\conf\registry.conf
16:52:51.063 INFO --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
16:52:51.981 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
接下来,我们就以电商系统为例,来演示下业务系统是如何集成 Seata 的。
在电商系统中,用户下单购买一件商品,需要以下 3 个服务提供支持:
这三个微服务分别使用三个不同的数据库,架构图如下所示。
图10:电商系统架构图
当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` bigint DEFAULT NULL COMMENT '用户id',`product_id` bigint DEFAULT NULL COMMENT '产品id',`count` int DEFAULT NULL COMMENT '数量',`money` decimal(11,0) DEFAULT NULL COMMENT '金额',`status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8 ;
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-seata-order-8005 0.0.1-SNAPSHOT spring-cloud-alibaba-seata-order-8005 Demo project for Spring Boot 1.8 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer com.alibaba.cloud spring-cloud-starter-alibaba-seata org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test net.demo.c spring-cloud-alibaba-api ${project.version} junit junit 4.12 mysql mysql-connector-java 8.0.19 ch.qos.logback logback-core org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 org.springframework.boot spring-boot-starter-actuator com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.mybatis.generator mybatis-generator-maven-plugin 1.4.0 src/main/resources/mybatis-generator/generatorConfig.xml true true mysql mysql-connector-java 8.0.19 org.mybatis.generator mybatis-generator-core 1.4.0 org.springframework.boot spring-boot-maven-plugin
spring:cloud:## Nacos认证信息nacos:config:username: nacospassword: nacoscontext-path: /nacosserver-addr: 127.0.0.1:1111 # 设置配置中心服务端地址namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
spring:application:name: spring-cloud-alibaba-seata-order-8005 #服务名#数据源配置datasource:driver-class-name: com.mysql.jdbc.Driver #数据库驱动name: defaultDataSourceurl: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC #数据库连接地址username: root #数据库的用户名password: root #数据库密码cloud:nacos:discovery:server-addr: 127.0.0.1:1111 #nacos 服务器地址namespace: public #nacos 命名空间username:password:sentinel:transport:dashboard: 127.0.0.1:8080 #Sentinel 控制台地址port: 8719alibaba:seata:#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同tx-service-group: service-order-group
server:port: 8005 #端口
seata:application-id: ${spring.application.name}#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同tx-service-group: service-order-groupservice:grouplist:#Seata 服务器地址seata-server: 127.0.0.1:8091# Seata 的注册方式为 nacosregistry:type: nacosnacos:server-addr: 127.0.0.1:1111# Seata 的配置中心为 nacosconfig:type: nacosnacos:server-addr: 127.0.0.1:1111
feign:sentinel:enabled: true #开启 OpenFeign 功能
management:endpoints:web:exposure:include: "*"
###################################### MyBatis 配置 ######################################
mybatis:# 指定 mapper.xml 的位置mapper-locations: classpath:mybatis/mapper/*.xml#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名type-aliases-package: net.demo.c.entityconfiguration:#默认开启驼峰命名法,可以不用设置该属性map-underscore-to-camel-case: true
package net.demo.c.entity;
import java.math.BigDecimal;
public class Order {private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;private Integer status;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}public Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Integer getCount() {return count;}public void setCount(Integer count) {this.count = count;}public BigDecimal getMoney() {return money;}public void setMoney(BigDecimal money) {this.money = money;}public Integer getStatus() {return status;}public void setStatus(Integer status) {this.status = status;}
}
package net.demo.c.mapper;
import net.demo.c.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderMapper {int deleteByPrimaryKey(Long id);int insert(Order record);int create(Order order);int insertSelective(Order record);//2 修改订单状态,从零改为1void update(@Param("userId") Long userId, @Param("status") Integer status);Order selectByPrimaryKey(Long id);int updateByPrimaryKeySelective(Order record);int updateByPrimaryKey(Order record);
}
id, user_id, product_id, count, money, status deletefrom t_orderwhere id = #{id,jdbcType=BIGINT} insert into t_order (id, user_id, product_id,count, money, status)values (#{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},#{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER}) insert into t_orderid, user_id, product_id, count, money, status, #{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT}, #{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER}, insert into t_order (user_id, product_id,count, money, status)values (#{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},#{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER}) update t_orderuser_id = #{userId,jdbcType=BIGINT}, product_id = #{productId,jdbcType=BIGINT}, count = #{count,jdbcType=INTEGER}, money = #{money,jdbcType=DECIMAL}, status = #{status,jdbcType=INTEGER}, where id = #{id,jdbcType=BIGINT} update t_orderset user_id = #{userId,jdbcType=BIGINT},product_id = #{productId,jdbcType=BIGINT},count = #{count,jdbcType=INTEGER},money = #{money,jdbcType=DECIMAL},status = #{status,jdbcType=INTEGER}where id = #{id,jdbcType=BIGINT} update t_orderset status = 1where user_id = #{userId}and status = #{status};
package net.demo.c.service;
import net.demo.c.entity.Order;
public interface OrderService {/*** 创建订单数据* @param order*/CommonResult create(Order order);
}
package net.demo.c.service;
import net.demo.c.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "spring-cloud-alibaba-seata-storage-8006")
public interface StorageService {@PostMapping(value = "/storage/decrease")CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package net.demo.c.service;
import net.demo.c.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "spring-cloud-alibaba-seata-account-8007")
public interface AccountService {@PostMapping(value = "/account/decrease")CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package net.demo.c.service.impl;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.Order;
import net.demo.c.mapper.OrderMapper;
import net.demo.c.service.AccountService;
import net.demo.c.service.OrderService;
import net.demo.c.service.StorageService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StorageService storageService;@Resourceprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:下订单->扣库存->减余额->改订单状态*/@Override@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)public CommonResult create(Order order) {log.info("----->开始新建订单");//1 新建订单order.setUserId(new Long(1));order.setStatus(0);orderMapper.create(order);//2 扣减库存log.info("----->订单服务开始调用库存服务,开始扣减库存");storageService.decrease(order.getProductId(), order.getCount());log.info("----->订单微服务开始调用库存,扣减库存结束");//3 扣减账户log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额");accountService.decrease(order.getUserId(), order.getMoney());log.info("----->订单微服务开始调用账户,账户扣减商品金额结束");//4 修改订单状态,从零到1,1代表已经完成log.info("----->修改订单状态开始");orderMapper.update(order.getUserId(), 0);log.info("----->修改订单状态结束");log.info("----->下订单结束了------->");return new CommonResult(200, "订单创建成功");}
}
package net.demo.c.controller;
import net.demo.c.entity.CommonResult;
import net.demo.c.entity.Order;
import net.demo.c.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class OrderController {@Autowiredprivate OrderService orderService;@GetMapping("/order/create/{productId}/{count}/{money}")public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count, @PathVariable("money") BigDecimal money) {Order order = new Order();order.setProductId(Integer.valueOf(productId).longValue());order.setCount(count);order.setMoney(money);return orderService.create(order);}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "net.demo")
public class SpringCloudAlibabaSeataOrder8005Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataOrder8005Application.class, args);}
}
-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (`id` bigint NOT NULL AUTO_INCREMENT,`product_id` bigint DEFAULT NULL COMMENT '产品id',`total` int DEFAULT NULL COMMENT '总库存',`used` int DEFAULT NULL COMMENT '已用库存',`residue` int DEFAULT NULL COMMENT '剩余库存',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100');-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-seata-storage-8006 0.0.1-SNAPSHOT spring-cloud-alibaba-seata-storage-8006 Demo project for Spring Boot 1.8 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer com.alibaba.cloud spring-cloud-starter-alibaba-seata org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test net.demo.c spring-cloud-alibaba-api ${project.version} junit junit 4.12 mysql mysql-connector-java 8.0.19 ch.qos.logback logback-core org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 org.springframework.boot spring-boot-starter-actuator com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.mybatis.generator mybatis-generator-maven-plugin 1.4.0 src/main/resources/mybatis-generator/generatorConfig.xml true true mysql mysql-connector-java 8.0.19 org.mybatis.generator mybatis-generator-core 1.4.0 org.springframework.boot spring-boot-maven-plugin
spring:cloud:## Nacos认证信息nacos:config:username: nacospassword: nacoscontext-path: /nacosserver-addr: 127.0.0.1:1111 # 设置配置中心服务端地址namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
spring:application:name: spring-cloud-alibaba-seata-storage-8006datasource:driver-class-name: com.mysql.jdbc.Drivername: defaultDataSourceurl: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=UTCusername: rootpassword: rootcloud:nacos:discovery:server-addr: 127.0.0.1:1111namespace: publicusername:password:sentinel:transport:dashboard: 127.0.0.1:8080port: 8719alibaba:seata:tx-service-group: service-storage-group
server:port: 8006
seata:application-id: ${spring.application.name}tx-service-group: service-storage-groupservice:grouplist:seata-server: 127.0.0.1:8091registry:type: nacosnacos:server-addr: 127.0.0.1:1111config:type: nacosnacos:server-addr: 127.0.0.1:1111
feign:sentinel:enabled: true
management:endpoints:web:exposure:include: "*"
###################################### MyBatis 配置 ######################################
mybatis:# 指定 mapper.xml 的位置mapper-locations: classpath:mybatis/mapper/*.xml#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名type-aliases-package: net.demo.c.entityconfiguration:#默认开启驼峰命名法,可以不用设置该属性map-underscore-to-camel-case: true
package net.demo.c.entity;
public class Storage {private Long id;private Long productId;private Integer total;private Integer used;private Integer residue;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Integer getTotal() {return total;}public void setTotal(Integer total) {this.total = total;}public Integer getUsed() {return used;}public void setUsed(Integer used) {this.used = used;}public Integer getResidue() {return residue;}public void setResidue(Integer residue) {this.residue = residue;}
}
package net.demo.c.mapper;
import net.demo.c.entity.Storage;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StorageMapper {Storage selectByProductId(Long productId);int decrease(Storage record);
}
id, product_id, total, used, residue update t_storagetotal = #{total,jdbcType=INTEGER}, used = #{used,jdbcType=INTEGER}, residue = #{residue,jdbcType=INTEGER}, where product_id = #{productId,jdbcType=BIGINT}
package net.demo.c.service;
public interface StorageService {int decrease(Long productId, Integer count);
}
package net.demo.c.service.impl;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.Storage;
import net.demo.c.mapper.StorageMapper;
import net.demo.c.service.StorageService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {@ResourceStorageMapper storageMapper;@Overridepublic int decrease(Long productId, Integer count) {log.info("------->storage-service中扣减库存开始");log.info("------->storage-service 开始查询商品是否存在");Storage storage = storageMapper.selectByProductId(productId);if (storage != null && storage.getResidue().intValue() >= count.intValue()) {Storage storage2 = new Storage();storage2.setProductId(productId);storage.setUsed(storage.getUsed() + count);storage.setResidue(storage.getTotal().intValue() - storage.getUsed());int decrease = storageMapper.decrease(storage);log.info("------->storage-service 扣减库存成功");return decrease;} else {log.info("------->storage-service 库存不足,开始回滚!");throw new RuntimeException("库存不足,扣减库存失败!");}}
}
package net.demo.c.controller;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.CommonResult;
import net.demo.c.service.StorageService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@Slf4j
public class StorageController {@Resourceprivate StorageService storageService;@Value("${server.port}")private String serverPort;@PostMapping(value = "/storage/decrease")CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {int decrease = storageService.decrease(productId, count);CommonResult result;if (decrease > 0) {result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease);} else {result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "库存扣减失败");}return result;}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "net.demo")
public class SpringCloudAlibabaSeataStorage8006Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataStorage8006Application.class, args);}
}
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',`user_id` bigint 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 '剩余可用额度',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES ('1', '1', '1000', '0', '1000');-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8;
4.0.0 net.demo.c 1.0-SNAPSHOT spring-cloud-alibaba-demo net.demo.c spring-cloud-alibaba-seata-account-8007 0.0.1-SNAPSHOT spring-cloud-alibaba-seata-account-8007 Demo project for Spring Boot 1.8 1.4.2 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-netflix-ribbon org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer com.alibaba.cloud spring-cloud-starter-alibaba-seata org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test net.demo.c spring-cloud-alibaba-api ${project.version} junit junit 4.12 mysql mysql-connector-java 8.0.19 ch.qos.logback logback-core org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 org.springframework.boot spring-boot-starter-actuator com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.mybatis.generator mybatis-generator-maven-plugin 1.4.0 src/main/resources/mybatis-generator/generatorConfig.xml true true mysql mysql-connector-java 8.0.19 org.mybatis.generator mybatis-generator-core 1.4.0 org.springframework.boot spring-boot-maven-plugin
spring:cloud:## Nacos认证信息nacos:config:username: nacospassword: nacoscontext-path: /nacosserver-addr: 127.0.0.1:1111 # 设置配置中心服务端地址namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
spring:application:name: spring-cloud-alibaba-seata-account-8007datasource:driver-class-name: com.mysql.cj.jdbc.Drivername: defaultDataSourceurl: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTCusername: rootpassword: rootcloud:nacos:discovery:server-addr: 127.0.0.1:1111namespace: publicusername:password:sentinel:transport:dashboard: 127.0.0.1:8080port: 8719alibaba:seata:tx-service-group: service-account-group
server:port: 8007
seata:application-id: ${spring.application.name}tx-service-group: service-account-groupservice:grouplist:seata-server: 127.0.0.1:8091registry:type: nacosnacos:server-addr: 127.0.0.1:1111config:type: nacosnacos:server-addr: 127.0.0.1:1111
feign:sentinel:enabled: true
management:endpoints:web:exposure:include: "*"
###################################### MyBatis 配置 ######################################
mybatis:# 指定 mapper.xml 的位置mapper-locations: classpath:mybatis/mapper/*.xml#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名type-aliases-package: net.demo.c.entityconfiguration:#默认开启驼峰命名法,可以不用设置该属性map-underscore-to-camel-case: true
package net.demo.c.entity;
import java.math.BigDecimal;
public class Account {private Long id;private Long userId;private BigDecimal total;private BigDecimal used;private BigDecimal residue;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}public BigDecimal getTotal() {return total;}public void setTotal(BigDecimal total) {this.total = total;}public BigDecimal getUsed() {return used;}public void setUsed(BigDecimal used) {this.used = used;}public BigDecimal getResidue() {return residue;}public void setResidue(BigDecimal residue) {this.residue = residue;}
}
package net.demo.c.mapper;
import net.demo.c.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
@Mapper
public interface AccountMapper {Account selectByUserId(Long userId);int decrease(Long userId, BigDecimal money);
}
id, user_id, total, used, residue UPDATE t_accountSET residue = residue - #{money},used = used + #{money}WHERE user_id = #{userId};
package net.demo.c.service;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
public interface AccountService {/*** 扣减账户余额** @param userId 用户id* @param money 金额*/int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package net.demo.c.service.impl;
import lombok.extern.slf4j.Slf4j;
import net.demo.c.entity.Account;
import net.demo.c.mapper.AccountMapper;
import net.demo.c.service.AccountService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {@ResourceAccountMapper accountMapper;@Overridepublic int decrease(Long userId, BigDecimal money) {log.info("------->account-service 开始查询账户余额");Account account = accountMapper.selectByUserId(userId);log.info("------->account-service 账户余额查询完成," + account);if (account != null && account.getResidue().intValue() >= money.intValue()) {log.info("------->account-service 开始从账户余额中扣钱!");int decrease = accountMapper.decrease(userId, money);log.info("------->account-service 从账户余额中扣钱完成");return decrease;} else {log.info("账户余额不足,开始回滚!");throw new RuntimeException("账户余额不足!");}}
}
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "net.demo")
public class SpringCloudAlibabaSeataAccount8007Application {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataAccount8007Application.class, args);}
}
2021-11-25 15:16:27.389 INFO 19564 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : defaultDataSource - Start completed.
2021-11-25 15:16:27.553 INFO 19564 --- [ restartedMain] i.s.c.r.netty.NettyClientChannelManager : will connect to 172.30.194.1:8091
2021-11-25 15:16:27.553 INFO 19564 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://localhost:3306/seata_order
2021-11-25 15:16:27.557 INFO 19564 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:172.30.194.1:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/seata_order', applicationId='spring-cloud-alibaba-seata-order-8005', transactionServiceGroup='service-order-group'} >
2021-11-25 15:16:28.699 INFO 19564 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.4.2,channel:[id: 0xc6da1cb4, L:/172.30.194.1:49945 - R:/172.30.194.1:8091]
2021-11-25 15:16:28.707 INFO 19564 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 78 ms, version:1.4.2,role:RMROLE,channel:[id: 0xc6da1cb4, L:/172.30.194.1:49945 - R:/172.30.194.1:8091]
2021-11-25 15:16:28.621 INFO 14772 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : defaultDataSource - Start completed.
2021-11-25 15:16:28.969 INFO 14772 --- [ restartedMain] i.s.c.r.netty.NettyClientChannelManager : will connect to 172.30.194.1:8091
2021-11-25 15:16:28.970 INFO 14772 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://localhost:3306/seata_storage
2021-11-25 15:16:28.974 INFO 14772 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:172.30.194.1:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/seata_storage', applicationId='spring-cloud-alibaba-seata-storage-8006', transactionServiceGroup='service-storage-group'} >
2021-11-25 15:16:30.171 INFO 14772 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.4.2,channel:[id: 0x7311ae2a, L:/172.30.194.1:52026 - R:/172.30.194.1:8091]
2021-11-25 15:16:30.182 INFO 14772 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 174 ms, version:1.4.2,role:RMROLE,channel:[id: 0x7311ae2a, L:/172.30.194.1:52026 - R:/172.30.194.1:8091]
2021-11-25 15:16:29.914 INFO 8616 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : defaultDataSource - Start completed.
2021-11-25 15:16:30.253 INFO 8616 --- [ restartedMain] i.s.c.r.netty.NettyClientChannelManager : will connect to 172.30.194.1:8091
2021-11-25 15:16:30.253 INFO 8616 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://localhost:3306/seata_account
2021-11-25 15:16:30.257 INFO 8616 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:172.30.194.1:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/seata_account', applicationId='spring-cloud-alibaba-seata-account-8007', transactionServiceGroup='service-account-group'} >
2021-11-25 15:16:31.930 INFO 8616 --- [ restartedMain] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.4.2,channel:[id: 0xa57ead6d, L:/172.30.194.1:52057 - R:/172.30.194.1:8091]
2021-11-25 15:16:31.941 INFO 8616 --- [ restartedMain] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 114 ms, version:1.4.2,role:RMROLE,channel:[id: 0xa57ead6d, L:/172.30.194.1:52057 - R:/172.30.194.1:8091]
{"code":200,"message":"订单创建成功","data":null}
SELECT * FROM seata_order.t_order;
结果如下。
id | user_id | product_id | count | money | status |
---|---|---|---|---|---|
1 | 1 | 2 | 20 | 1 |
从上表可以看出,已经创建一条订单数据,且订单状态(status)已修改为“已完成”。
SELECT * FROM seata_storage.t_storage;
结果如下。
id | product_id | total used | residue | |
---|---|---|---|---|
1 | 1 | 100 | 2 | 98 |
从上表可以看出,商品库存已经扣减 2 件,仓库中商品储量从 100 件减少到了 98 件。
SELECT * FROM seata_account.t_account;
结果如下。
id | user_id | total | used | residue |
---|---|---|---|---|
1 | 1 | 1000 | 20 | 980 |
从上表可以看出,账户余额已经扣减,金额从 1000 元减少到了 980 元。
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.Thu Nov 25 15:27:03 CST 2021
There was an unexpected error (type=Internal Server Error, status=500).
[500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.demo.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]
feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.demo.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]
注:在本次请求中,用户购买 10 件商品(商品 ID 为 1),商品总价为 1000 元,此时用户账户余额 为 980 元,因此账户服务会抛出“账户余额不足!”的运行时异常。
从上图可以看出:
在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。
当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。
@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:
接下来,我们就使用 @GlobalTransactional 注解对业务系统进行改造,步骤如下。
/*** 使用 @GlobalTransactional 注解对分布式事务进行管理* @param productId* @param count* @param money* @return*/
@GetMapping("/order/createByAnnotation/{productId}/{count}/{money}")
@GlobalTransactional(name = "c-demo-net-create-order", rollbackFor = Exception.class)
public CommonResult createByAnnotation(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count, @PathVariable("money") BigDecimal money) {Order order = new Order();order.setProductId(Integer.valueOf(productId).longValue());order.setCount(count);order.setMoney(money);return orderService.create(order);
}
从以上代码可以看出,添加的 createByAnnotation() 方法与 create()
方法无论是参数还是代码逻辑都一摸一样,唯一的不同就是前者标注了 @GlobalTransactional 注解。
将数据恢复到浏览器访问“http://localhost:8005/order/createByAnnotation/1/2/20”之后。
重启订单(Order)服务、库存(Storage)服务和账户(Account)服务,并使用浏览器访问“http://localhost:8005/order/createByAnnotation/1/10/1000”,结果如下。
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.Thu Nov 25 15:27:03 CST 2021
There was an unexpected error (type=Internal Server Error, status=500).
[500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.demo.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]
feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.demo.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]
在本次请求中,用户购买商品的总价为 1000 元,但用户账户余额只有 980 元,因此账户服务会抛出运行时异常,异常信息为“账户余额不足!”。
2021-11-25 15:26:57.586 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->开始新建订单
2021-11-25 15:26:58.276 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单服务开始调用库存服务,开始扣减库存
2021-11-25 15:26:58.413 WARN 19564 --- [nio-8005-exec-1] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching.
2021-11-25 15:27:00.705 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单微服务开始调用库存,扣减库存结束
2021-11-25 15:27:00.705 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单服务开始调用账户服务,开始从账户扣减商品金额
2021-11-25 15:27:00.723 WARN 19564 --- [nio-8005-exec-1] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching.
2021-11-25 15:27:03.665 INFO 19564 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=172.30.194.1:8091:2702361983450404762,branchId=2702361983450404764,branchType=AT,resourceId=jdbc:mysql://localhost:3306/seata_order,applicationData=null
2021-11-25 15:27:03.670 INFO 19564 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.30.194.1:8091:2702361983450404762 2702361983450404764 jdbc:mysql://localhost:3306/seata_order
2021-11-25 15:27:03.738 INFO 19564 --- [h_RMROLE_1_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 172.30.194.1:8091:2702361983450404762 branch 2702361983450404764, undo_log deleted with GlobalFinished
2021-11-25 15:27:03.742 INFO 19564 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2021-11-25 15:27:03.817 INFO 19564 --- [nio-8005-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [172.30.194.1:8091:2702361983450404762] rollback status: Rollbacked
2021-11-25 15:27:03.853 ERROR 19564 --- [nio-8005-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.demo.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]] with root cause
2021-11-25 15:26:59.315 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service中扣减库存开始
2021-11-25 15:26:59.316 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service 开始查询商品是否存在
2021-11-25 15:27:00.652 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service 扣减库存成功
2021-11-25 15:27:03.568 INFO 14772 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=172.30.194.1:8091:2702361983450404762,branchId=2702361983450404769,branchType=AT,resourceId=jdbc:mysql://localhost:3306/seata_storage,applicationData=null
2021-11-25 15:27:03.572 INFO 14772 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.30.194.1:8091:2702361983450404762 2702361983450404769 jdbc:mysql://localhost:3306/seata_storage
2021-11-25 15:27:03.631 INFO 14772 --- [h_RMROLE_1_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 172.30.194.1:8091:2702361983450404762 branch 2702361983450404769, undo_log deleted with GlobalFinished
2021-11-25 15:27:03.635 INFO 14772 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2021-11-25 15:27:03.366 INFO 8616 --- [nio-8007-exec-1] n.b.c.service.impl.AccountServiceImpl : ------->account-service 开始查询账户余额
2021-11-25 15:27:03.484 INFO 8616 --- [nio-8007-exec-1] n.b.c.service.impl.AccountServiceImpl : ------->account-service 账户余额查询完成,net.demo.c.entity.Account@2a95537f
2021-11-25 15:27:03.485 INFO 8616 --- [nio-8007-exec-1] n.b.c.service.impl.AccountServiceImpl : 账户余额不足,开始回滚!
2021-11-25 15:27:03.499 ERROR 8616 --- [nio-8007-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 账户余额不足!] with root cause
java.lang.RuntimeException: 账户余额不足!
图12:Seata 事务回滚
从图 12 可以看出,这次并没有出现分布式事务造成的数据不一致问题。