Spring Cloud Alibaba是什么
Spring Cloud 本身并不是一个拿来即可用的框架,它是一套微服务规范,这套规范共有两代实现。
- 第一代实现: Spring Cloud Netflix。
- 第二代实现: Spring Cloud Alibaba。
2018 年 12 月12 日,Netflix 公司宣布 Spring Cloud Netflix 系列大部分组件都进入维护模式,不再添加新特性。这严重地限制了 Spring Cloud 的高速发展,于是各大互联网公司和组织开始把目光转向 Spring Cloud 的第二代实现:Spring Cloud Alibaba。
Spring Cloud Alibaba
中文学习网站: spring-cloud-alibaba/README-zh.md at 2.2.x · alibaba/spring-cloud-alibaba · GitHub
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 在业界得到了广泛的使用,成功案例也越来越多。
主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
Spring Cloud Alibaba 组件
Spring Cloud Alibaba 包含了多种开发分布式微服务系统的必需组件
- Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现,配置管理和服务管理平台。
- Sentinel:阿里巴巴开源产品,把流量作为切入点,从流量控制,熔断降级,系统负载保护等多个维度保护服务的稳定性。
- RocketMQ:Apache RocketMQ 是一款基于Java 的高性能、高吞吐量的分布式消息和流计算平台。
- Dubbo:Apache Dubbo 是一款高性能的 Java RPC 框架。
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud OSS:阿里云对象存储服务器(Object Storage Service,简称OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。
- Alibaba Cloud Schedulerx:阿里中间件团队开发的一款分布式调度产品,支持周期性的任务与固定时间点触发任务。
通过 Spring Cloud Alibaba 的这些组件,我们只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
Spring Cloud Alibaba 的应用场景
Spring Cloud Alibaba 的应用场景如下:
- 大型复杂的系统,例如大型电商系统。
- 高并发系统,例如大型门户网站、商品秒杀系统。
- 需求不明确,且变更很快的系统,例如创业公司业务系统。
如何使用
如何引入依赖
如果需要使用已发布的版本,在 dependencyManagement
中添加如下配置。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后在 dependencies
中添加自己所需使用的依赖即可使用。
Spring Cloud 两代实现组件对比
下表展示了 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 Alibaba 版本依赖
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 下各组件版本关系如下表。
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:Spring Cloud Alibaba服务注册与配置中心
Nacos 英文全称为 Dynamic Naming and Configuration Service,是一个由阿里巴巴团队使用 Java 语言开发的开源项目。
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 支持几乎所有主流类型“服务”的发现、配置和管理:
- Kubernetes Service
- gRPC & Dubbo RPC Service
- Spring Cloud RESTful Service
Nacos 的特性
Nacos 提供了一系列简单易用的特性,能够帮助我们快速地实现动态服务发现、服务配置等功能。
服务发现
Nacos 支持基于 DNS 和 RPC 的服务发现。当服务提供者使用原生 SDK、OpenAPI 或一个独立的 Agent TODO 向 Nacos 注册服务后,服务消费者可以在 Nacos 上通过 DNS TODO 或 HTTP&API 查找、发现服务。
服务健康监测
Nacos 提供对服务的实时健康检查,能够阻止请求发送到不健康主机或服务实例上。Nacos 还提供了一个健康检查仪表盘,能够帮助我们根据健康状态管理服务的可用性及流量。
动态配置服务
动态配置服务可以让我们以中心化、外部化和动态化的方式,管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效、敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的 UI 帮助我们管理所有服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助我们更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
动态 DNS 服务
Nacos 提供了动态 DNS 服务,能够让我们更容易地实现负载均衡、流量控制以及数据中心内网的简单 DNS 解析服务。
Nacos 提供了一些简单的 DNS APIs TODO,可以帮助我们管理服务的关联域名和可用的 IP:PORT 列表。
服务及其元数据管理
Nacos 能让我们从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及 metrics 统计数据。
Nacos 两大组件
与 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 服务注册中心
Nacos 作为服务注册中心可以实现服务的注册与发现,流程如下图。
共涉及到以下 3 个角色:
- 服务注册中心(Register Service):它是一个 Nacos Server,可以为服务提供者和服务消费者提供服务注册和发现功能。
- 服务提供者(Provider Service):它是一个 Nacos Client,用于对外服务。它将自己提供的服务注册到服务注册中心,以供服务消费者发现和调用。
- 服务消费者(Consumer Service):它是一个 Nacos Client,用于消费服务。它可以从服务注册中心获取服务列表,调用所需的服务。
Nacos 实现服务注册与发现的流程如下:
- 从 Nacos 官方提供的下载页面中,下载 Nacos Server 并运行。
- 服务提供者 Nacos Client 启动时,会把服务以服务名(spring.application.name)的方式注册到服务注册中心(Nacos Server);
- 服务消费者 Nacos Client 启动时,也会将自己的服务注册到服务注册中心;
- 服务消费者在注册服务的同时,它还会从服务注册中心获取一份服务注册列表信息,该列表中包含了所有注册到服务注册中心上的服务的信息(包括服务提供者和自身的信息);
- 在获取了服务提供者的信息后,服务消费者通过 HTTP 或消息中间件远程调用服务提供者提供的服务。
安装和运行 Nacos Server
- 使用浏览器访问 Releases · alibaba/nacos · GitHub,并在页面中选择合适的版本并点击进入下载,如下图。
- 下载完成后,解压 ,目录结构如下。
Nacos Server 下各目录说明如下:
- bin:用于存放 Nacos 的可执行命令。
- conf:用于存放 Nacos 配置文件。
- target:用于存放 Nacos 应用的 jar 包。
- 打开命令行窗口,跳转到 Nacos Server 安装目录的 bin 下,执行以下命令,以单机模式启动 Nacos Server。
startup.cmd -m standalone
- Nacos Server 启动日志如下。
- 使用浏览器访问“http://localhost:8848/nacos”,跳转到 Nacos Server 登陆页面,如下图。
- 在登陆页输入登录名和密码(默认都是 nacos),点击提交按钮,跳转到 Nacos Server 控制台主页,如下图。
自此,我们就完成了 Nacos Server 的下载、安装和运行工作。
搭建服务提供者
搭建父工程alibaba-parent,maven工程,删除src
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>alibaba-parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<spring-boot-dependencies.version>2.5.6</spring-boot-dependencies.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--Spring Cloud Alibaba 的版本信息-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Cloud 的版本信息-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
接下来,新建module,搭建一个服务提供者user-service-8001,步骤如下。
- 创建user-service-8001工程,application.yml如下
server:
port: 8001
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos server服务地址
management:
endpoints:
web:
exposure:
include: '*' #暴露所有端口
- pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>alibaba-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service-8001</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Spring Cloud Alibaba Nacos discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
- 创建UserController,进行测试:
@RestController
public class UserController {
@GetMapping("/nacos/{id}")
public String getInfo(@PathVariable("id") Integer id){
return "用户服务正常执行" + id;
}
}
启动类添加
@EnableDiscoveryClient
注解,启用nacos服务发现功能@SpringBootApplication @EnableDiscoveryClient public class UserService8001Application { public static void main(String[] args) { SpringApplication.run(UserService8001Application.class,args); } }
启用当前服务,访问localhost:8001/nacos/1,测试服务可用即可!!!
使用浏览器访问“http://localhost:8848/nacos”,查看“服务管理”下的“服务列表”,如下。
搭建服务消费者
接下来,我们就来搭建一个服务消费者 goods-service-8701 来消费user-service-8001提供的服务,步骤如下。
- 在父工程下新建goods-service-api(api工程书写实体类以及feign服务调用)工程,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>alibaba-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>goods-service-api</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
在goods-service-api工程下新建UserFeign接口,实现对user-service的http接口调用
@RequestMapping("/") @FeignClient(value = "user-service") public interface UserFeign { @GetMapping("/nacos/{id}") String getInfo(@PathVariable("id") Integer id); }
接下来,新建goods-service-8071工程,用于调用user-servie-8001服务提供的接口
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>alibaba-parent</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>goods-service-8071</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--SpringCloud ailibaba nacos discovery--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--由于 Netflix Ribbon 进入停更维护阶段,因此新版本的 Nacos discovery 都已经移除了 Ribbon ,此时我们需要引入 loadbalancer 代替 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>goods-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
application.yml配置文件如下:
server:
port: 8071
spring:
application:
name: goods-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos server 服务地址
启动类添加
@EnableDiscoveryClient
、@EnableFeignClients
注解@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(basePackages = {"edu.zut.goods.feign"}) //将api依赖中配置的feign加载 public class GoodsService8071Application { public static void main(String[] args) { SpringApplication.run(GoodsService8071Application.class,args); } }
创建GoodsController,调用feign接口中的方法,实现服务调用
@RestController public class GoodsController { @Autowired private UserFeign userFeign; @GetMapping("/goods/{id}") public String test(@PathVariable("id") Integer id){ String info = userFeign.getInfo(id); return info; } }
启动goods-service-8071工程,查看nacos服务列表
- 访问localhost:8071/goods/1,查看调用结果即可!!!
Nacos 配置中心
Nacos Server 还可以作为配置中心,对 Spring Cloud 应用的外部配置进行统一地集中化管理。Nacos Server的使用只需要在应用的 POM 文件中引入 spring-cloud-starter-alibaba-nacos-config 即可实现配置的获取与动态刷新。
从配置管理的角度看,Nacos 可以说是 Spring Cloud Config 的替代方案,但相比后者 Nacos 的使用更简单,操作步骤也更少。
通过一个实例来演示下 Nacos 是如何实现配置的统一管理和动态刷新的。
搭建config-client-3377服务(实际就是微服务模块),pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>alibaba-parent</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>config-client-3377</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--SpringCloud2020及以后的版本默认不启用 bootstrap 配置,我们需要在pom里面显式地引入:--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--Spring Cloud Alibaba Config 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--SpringCloud ailibaba nacos 服务注册与发现模块 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--Spring Boot 监控模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> </project>
注意:Spring Cloud 2020 版本默认不启用 bootstrap,若想要在应用启动时加载 bootstrap 配置(例如 bootstrap.yml 或 bootstrap.properties),就需要我们在 pom.xml 中显式的引入 spring-cloud-starter-bootstrap 依赖。
- 在 config-client-3377 的类路径(例如 /resources 目录)下,添加一个 bootstrap.yml,配置内容如下。
server:
port: 3377 #端口号
spring:
application:
name: config-client #服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEFAULT_GROUP
- 添加一个 application.yml,配置内容如下。
spring:
profiles:
active: dev #激活 dev 的配置
- 创建一个名为 ConfigController的 Controller 类,并在该类上使用 @RefreshScope 注解实现配置的自动更新,代码如下。(演示动态获取配置中心的配置信息)
@RestController
@RefreshScope //支持nacos的动态刷新功能
public class ConfigController {
@Value("${config.info}")
private String ConfigInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return ConfigInfo;
}
}
- config-client-3377 的主启动类上,使用 @EnableDiscoveryClient 注解开启服务发现功
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class,args);
}
}
- 启动 Nacos Server,并在 Nacos Server 控制台的“配置管理”下的“配置列表”中,点击“+”按钮,新建如下配置。
Data ID: config-client-dev.yaml
Group : DEFAULT_GROUP
配置格式: YAML
配置内容: config:
info: com.zhiyou100
在 Nacos Server 中,配置的 dataId(即 Data ID)的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
dataId 格式中各参数说明如下:
- ${prefix}:默认取值为微服务的服务名,即配置文件中 spring.application.name 的值,我们可以在配置文件中通过配置 spring.cloud.nacos.config.prefix 来指定。
- ${spring.profiles.active}:表示当前环境对应的 Profile,例如 dev、test、prod 等。当没有指定环境的 Profile 时,其对应的连接符也将不存在, dataId 的格式变成 p r e f i x . {prefix}. prefix.{file-extension}。
- ${file-extension}:表示配置内容的数据格式,我们可以在配置文件中通过配置项 spring.cloud.nacos.config.file-extension 来配置,例如 properties 和 yaml。
- 启动 3377服务,并使用浏览器访问“http://localhost:3377/config/info”,显示config.info信息即可!!!
Nacos Server 集群化部署
在实际的项目开发中,一个微服务系统往往由十几,几十个甚至几百个微服务组成。 这些服务若全部注册到同一台 Nacos Server,就极有可能导致 Nacos Server 因为不堪重负而崩溃,最终导致整个微服务系统瘫痪。解决这个问题最直接的办法就是使用 Nacos Server 集群。
Nacos Server 的集群化部署有一个十分明显的优点,那就是可以保障系统的高可用性。在集群化部署中,只要不是所有的 Nacos Server 都停止工作,Nacos Client 就还可以从集群中正常的 Nacos Server 上获取服务信息及配置,而不会导致系统的整体瘫痪,这就是 Nacos Server 集群化部署的高可用性。
下图展示了 Nacos Server 集群化部署的基本架构。
示例:
- 作为配置中心的nacos,可以通过mysql存储其配置信息,故而,需要设置mysql的配置。nacos安装包里,已经有现成的sql初始化文件,同样在/nacos/conf目录。
新建名为nacos-config的数据库,执行nacos-mysql.sql脚本,用于存储配置信息
- 在 Nacos Server 安装目录下的 conf 文件夹中,将 cluster.conf.example 重命名为 cluster.conf,然后在该文件中添加以下内容。
192.168.188.1:3333
192.168.188.1:4444
192.168.188.1:5555
配置说明如下:
- 192.168.188.1 为本地电脑主机的 IP 地址,这里最好不要写成 localhost 或 127.0.0.1,否则 Nacos Server 集群可能会搭建失败!
- 本次搭建的 Nacos Server 集群的端口分别为:3333、4444、5555。
需要分别在三台机器上都分别进行以上操作,当然,也可以修改一份,然后直接复制到另外两台机器上。
- 在 config 目录下的 application.properties 中,将 server.port(端口号)修改为 3333,并在该文件中添加 MySQL 数据库配置,具体修改内容如下。
server.port=3333
################ MySQL 数据库配置##################
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
db.user=root
db.password=qwer1234
同理,4444、5555nacos服务也进行配置
- 分别启动三台虚拟机/nacos/bin目录下的startup.sh脚本,启动成功后,查看/nacos/logs/start.out日志信息,显示如下,则表示启动成功了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTKml2o4-1660888778321)(images/image-20220511223143774.png)]
- 访问localhost:3333/nacos,查看集群节点信息
- 配置ngnix,通过ngnix服务器实现反向代理。下载 Nginx,并修改 Nginx 中 conf 目录下的 nginx.conf 的配置,内容如下。
#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;
}
}
}
- 双击 Nignx 安装目录下的 nginx.exe,启动 Nginx。使用浏览器访问“http://localhost:1111/nacos/”,若成功访问 Nacos Server 的控制台,则说明 Nacos 集群部署成功,如下图。
- 将微服务模块中的配置文件中的 Nacos Server 地址统一修改为:localhost:1111。
server:
port: 8001
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:1111 #集群版 Nacos Server 的地址
management:
endpoints:
web:
exposure:
include: '*' #暴露所有端口
- 重启user-service服务,查看nacos下服务列表
更多配置项
配置项 | key | 默认值 | 说明 |
服务端地址 | spring.cloud.nacos.discovery.server-addr | ||
服务名 | spring.cloud.nacos.discovery.service | spring.application.name | |
权重 | spring.cloud.nacos.discovery.weight | 1 | 取值范围 1 到 100,数值越大,权重越大 |
网卡名 | spring.cloud.nacos.discovery.network-interface | 当IP未配置时,注册的IP为此网卡所对应的IP地址,如果此项也未配置,则默认取第一块网卡的地址 | |
注册的IP地址 | spring.cloud.nacos.discovery.ip | 优先级最高 | |
注册的端口 | spring.cloud.nacos.discovery.port | -1 | 默认情况下不用配置,会自动探测 |
命名空间 | spring.cloud.nacos.discovery.namespace | 常用场景之一是不同环境的注册的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。 | |
AccessKey | spring.cloud.nacos.discovery.access-key | ||
SecretKey | spring.cloud.nacos.discovery.secret-key | ||
Metadata | spring.cloud.nacos.discovery.metadata | 使用Map格式配置 | |
日志文件名 | spring.cloud.nacos.discovery.log-name | ||
接入点 | spring.cloud.nacos.discovery.endpoint | UTF-8 | 地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址 |
是否集成Ribbon | ribbon.nacos.enabled | true |
Sentinel:Spring Cloud Alibaba高可用流量控制组件
Sentinel 是由阿里巴巴中间件团队开发的开源项目,是一种面向分布式微服务架构的轻量级高可用流量控制组件。
中文网站:介绍 · alibaba/Sentinel Wiki · GitHub 、 introduction (sentinelguard.io)
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它可以在 ClassPath 路径下的 META-INF/services 文件夹查找文件,并自动加载文件中定义的类。
从功能上来说,Sentinel 与 Spring Cloud Netfilx Hystrix 类似,但 Sentinel 要比 Hystrix 更加强大,例如 Sentinel 提供了流量控制功能、比 Hystrix 更加完善的实时监控功能等等。
Sentinel 的主要特性:
Sentinel 的组成
Sentinel 主要由以下两个部分组成:
Sentinel 核心库:Sentinel 的核心库不依赖任何框架或库,能够运行于 Java 8 及以上的版本的运行时环境中,同时对 Spring Cloud、Dubbo 等微服务框架提供了很好的支持。
Sentinel 控制台(Dashboard):Sentinel 提供的一个轻量级的开源控制台,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。
Sentinel 核心库不依赖 Sentinel Dashboard,但两者结合使用可以有效的提高效率,让 Sentinel 发挥它最大的作用。
Sentinel 的基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel 功能和设计理念
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
熔断降级
什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。
Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
- 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
Sentinel 是如何工作的
Sentinel 的主要工作机制如下:
- 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
- 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
- Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。
@SentinelResource 注解
@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 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
exceptionsToIgnore | 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 | 可选项 | - |
注:在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。
Sentinel 控制台
Sentinel 提供了一个轻量级的开源控制台 Sentinel Dashboard,它提供了机器发现与健康情况管理、监控(单机和集群)、规则管理与推送等多种功能。
Sentinel 控制台提供的功能如下:
- 查看机器列表以及健康情况:Sentinel 控制台能够收集 Sentinel 客户端发送的心跳包,判断机器是否在线。
- 监控(单机和集群聚合):Sentinel 控制台通过 Sentinel 客户端暴露的监控 API,可以实现秒级的实时监控。
- 规则管理和推送:通过 Sentinel 控制台,我们还能够针对资源定义和推送规则。
- 鉴权:从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。
Sentinel Dashboard 是我们配置和管理规则(例如流控规则、熔断降级规则等)的重要入口之一。通过它,我们不仅可以对规则进行配置和管理,还能实时查看规则的效果。
安装 Sentinel 控制台
下载和安装 Sentinel 控制台,具体步骤如下。
下载地址:Releases · alibaba/Sentinel · GitHub,根据需求下载合适的版本
- 打开命令行窗口,跳转到 Sentinel Dashboard jar 包所在的目录,执行以下命令,启动 Sentinel Dashboard。
java -jar sentinel-dashboard-1.8.3.jar
- 启动完成后,使用浏览器访问“http://localhost:8080/”,跳转到 Sentinel 控制台登陆页面,如下图。
- 分别输入用户名和密码(默认都是 sentinel),点击下方的登录按钮,结果如下图。
Sentinel 的开发流程
Sentinel 的开发流程如下:
- 引入 Sentinel 依赖:在项目中引入 Sentinel 的依赖,将 Sentinel 整合到项目中;
- 定义资源:通过对主流框架提供适配或 Sentinel 提供的显式 API 和注解,可以定义需要保护的资源,此外 Sentinel 还提供了资源的实时统计和调用链路分析;
- 定义规则:根据实时统计信息,对资源定义规则,例如流控规则、熔断规则、热点规则、系统规则以及授权规则等。
- 检验规则是否在生效:运行程序,检验规则是否生效,查看效果。
引入 Sentinel 依赖
为了减少开发的复杂程度,Sentinel 对大部分的主流框架都进行了适配,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux 和 Reactor 等。以 Spring Cloud 为例,我们只需要引入 spring-cloud-starter-alibaba-sentinel 的依赖,就可以方便地将 Sentinel 整合到项目中。
- 创建一个名为 sentinel-service-8401 的 Spring Boot 模块,并在其 pom.xml 中添加以下依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>alibaba-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-service-8401</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--Nacos 服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Snetinel 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
添加配置文件application.yml
server: port: 8401 #端口 spring: application: name: sentinel-service #服务名 cloud: nacos: discovery: #Nacos服务注册中心(集群)地址 server-addr: localhost:8848 sentinel: transport: #配置 Sentinel dashboard 地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*'
创建一个名为 SentinelController 的 Controller 类,进行测试,代码如下。
@RestController
@Slf4j
public class SentinelController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/testA")
public String testA() {
return "服务访问成功------testA";
}
@GetMapping("/testB")
public String testB() {
return "服务访问成功------testB";
}
}
- sentinel-service-8401 主启动类如下。
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelServiceApplication.class,args);
}
}
- 依次启动 Nacos Server 、 sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”,结果如下图。
- 使用浏览器访问 Sentinel 控制台主页,我们发现在“首页”下方新增了一个“sentinel-servcie”的菜单,而这正是 spring-cloud-alibaba-sentinel-service-8401 的服务名(spring.application.name),说明 Sentinel 已经监控到这个服务,如下图。
- 刷新请求,点击实时监控,查看监控页面
查看簇点链路
例如通过sentinel实现限流,修改/testA资源的流控规则,如下
修改单机阈值为1,重新刷新/testA请求,当QPS超过1时,会提示:
此时实时监控处:
定义资源
资源是 Sentinel 中的核心概念之一。在项目开发时,我们只需要考虑这个服务、方法或代码是否需要保护,如果需要保护,就可以将它定义为一个资源。
Sentinel 为我们提供了多种定义资源的方式:
- 适配主流框架自动定义资源
- 通过 SphU 手动定义资源
- 通过 SphO 手动定义资源
- 注解方式定义资源
适配主流框架自动定义资源
Sentinel 对大部分的主流框架都进行了适配,我们只要引入相关的适配模块(例如 spring-cloud-starter-alibaba-sentinel),Snetinel 就会自动将项目中的服务(包括调用端和服务端)定义为资源,资源名就是服务的请求路径。此时,我们只要再定义一些规则,这些资源就可以享受到 Sentinel 的保护。
我们可以在 Sentinel 控制台的“簇点链路”中,直接查看被 Sentinel 监控的资源,如下图。
通过 SphU 手动定义资源
Sentinel 提供了一个名为 SphU 的类,它包含的 try-catch 风格的 API ,可以帮助我们手动定义资源。
下面我们就通过一个实例,来演示下如何通过 SphU 定义资源。
- 在 sentinel-service-8401 下的 SentinelController 中,新增一个 testAbySphU() 方法定义一个名为 testAbySphU 的资源,代码如下。
/**
* 通过 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 手动定义资源
Sentinel 还提供了一个名为 SphO 的类,它包含了 if-else 风格的 API,能帮助我们手动定义资源。通过这种方式定义的资源,发生了限流之后会返回 false,此时我们可以根据返回值,进行限流之后的逻辑处理。
@RestController
@Slf4j
public class SentinelController {
@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 服务被限流";
//流控逻辑处理 - 结束
}
}
}
注解方式定义资源(推荐)
除了以上两种方式外,我们还可以通过 Sentinel 提供的 @SentinelResource 注解定义资源。
sentinel-service-8401 中 SentinelController 类中增加以下代码。
/**
* 通过SentinelResource注解定义资源
* @return
*/
@GetMapping("/testC")
@SentinelResource(value = "testCbyAnnotation")
public String testC() {
return "服务访问成功------testC..........";
}
启动服务,访问localhost:8401/testA 、localhost:8401/testB 、localhost:8401/testC,查看sentinel控制台,可以看到定义的资源。
规则的种类
Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。
Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则 和 热点参数规则。
流量控制规则 (FlowRule)
任何系统处理请求的能力都是有限的,但任意时间内到达系统的请求量往往是随机且不可控的,如果在某一个瞬时时刻请求量急剧增,那么系统就很有可能被瞬时的流量高峰冲垮。为了避免此类情况发生,我们都需要根据系统的处理能力对请求流量进行控制,这就是我们常说的“流量控制”,简称“流控”。
Sentinel 作为一种轻量级高可用流量控制组件,流量控制是它最主要的工作之一。
我们可以针对资源定义流控规则,Sentinel 会根据这些规则对流量相关的各项指标进行监控。当这些指标当达到或超过流控规则规定的阈值时,Sentinel 会对请求的流量进行限制(即“限流”),以避免系统被瞬时的流量高峰冲垮,保障系统的高可用性。
一条流量规则主要由下表中的属性组成,我们可以通过组合这些属性来实现不同的限流效果。
流量规则的定义
重要属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 或线程数模式 | QPS 模式 |
limitApp | 流控针对的调用来源 | default ,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流 | 直接拒绝 |
QPS 表示并发请求数,换句话说就是,每秒钟最多通过的请求数。
同一个资源可以创建多条流控规则,Sentinel 会遍历这些规则,直到有规则触发限流或者所有规则遍历完毕为止。
Sentinel 触发限流时,资源会抛出 BlockException 异常,此时我们可以捕捉 BlockException 来自定义被限流之后的处理逻辑。
通过 Sentinel 控制台定义流控规则
通过 Sentinel 控制台,直接对资源定义流控规则,操作步骤如下
例如通过sentinel实现限流,修改/testA资源的流控规则,如下
- 新增流控规则
- 修改单机阈值为1,重新刷新/testA请求,当QPS超过1时,会提示:
此时实时监控处:
若页面中出现以上信息,则说明该服务已被限流,但这种提示是 Sentinel 系统自动生成的,用户体验不好。
- 在服务代码中使用 @SentinelResource 注解定义资源名称,并在 blockHandler 属性指定一个限流函数,自定义服务限流信息,代码如下。
/**
* 通过SentinelResource注解定义资源,当前请求触发限流后执行的函数
* @return
*/
@GetMapping("/testC")
@SentinelResource(value = "testCbyAnnotation",blockHandler ="blockHandlerTestC" )
public String testC() {
return "服务访问成功------testC..........";
}
public String blockHandlerTestC(BlockException exception) {
return "testC服务访问失败! 您已被限流,请稍后重试";
}
通过 @SentinelResource 注解的 blockHandler 属性指定了一个 blockHandler 函数,进行限流之后的后续处理。
使用 @SentinelResource 注解的 blockHandler 属性时,需要注意以下事项:
- blockHandler 函数访问范围需要是 public;
- 返回类型需要与原方法相匹配;
- 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;
- blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandlerClass为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- 请务必添加 blockHandler 属性来指定自定义的限流处理方法,若不指定,则会跳转到错误页
- 重启服务,重新添加流控规则,访问结果如下:
通过代码定义流控规则
还可以在服务代码中,调用 FlowRuleManager 类的 loadRules() 方法来定义流控规则,该方法需要一个 FlowRule 类型的 List 集合作为其参数,代码如下。
public static void loadRules(List<FlowRule> rules) {
currentProperty.updateValue(rules);
}
FlowRule 可以通过以下属性定义流控规则,如下表。
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,即流控规则的作用对象 | - |
count | 限流的阈值。 | - |
grade | 流控阈值的类型,包括 QPS 或并发线程数 | QPS |
limitApp | 流控针对的调用来源 | default,表示不区分调用来源 |
strategy | 调用关系限流策略,包括直接、链路和关联 | 直接 |
controlBehavior | 流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流 | 直接拒绝 |
通过代码定义流控规则,步骤如下。
- 在添加一个 initFlowRules() 方法,为名为 testCbyAnnotation的资源定义流控规则:每秒最多只能通过 2 个请求,即 QPS 的阈值为 2
/**
* 代码设置流控规则
*/
private static void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<>();
//定义一个限流规则对象
FlowRule rule1 = new FlowRule();
//资源名称
rule1.setResource("testCbyAnnotation");
// Set max qps to 2
rule1.setCount(2);
//限流阈值的类型
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
//定义限流规则
FlowRuleManager.loadRules(rules);
}
- 重启 sentinel-service-8401,并使用浏览器访问“http://localhost:8401/testC”,当访问频率超过2时
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EOViBB24-1660888778326)(file://F:\Java课件\SpringCloud\images\image-20220513133311904.png?lastModify=1652420812)]
流控模式
流控—线程数直接失败
Sentinel流控—关联
- 当自己关联的资源达到阈值时,就限流自己
- 当与A关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)
设置testD请求
当关联资源/testC的QPS阀值超过1时,就限流/testD的Rest访问地址,当关联资源到阈值后限制配置好的资源名。
定义testCResource资源的阈值是1
当testCResource达到阈值时,testDResource触发限流:
利用postman模拟并发访问测试:
添加并发访问:
点击RUN Test模拟testCResource资源并发访问,此时点击浏览器访问testD,自动限流
流控模式-链路(根据调用链路入口限流:链路限流)
微服务中,资源之间的调用链路,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root
的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
上图中来自入口 Entrance1
和 Entrance2
的请求都调用到了资源 NodeA
,Sentinel 允许只根据某个入口的统计信息对资源限流。
简而言之:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
例如有两条请求链路:
- /test1 --> /common
- /test2 --> /common
如果只希望统计从/test2进入到/common的请求,则可以这样配置
流控效果
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
流控效果-warm up
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 threshold / coldFactor,持续指定时长后,逐渐提高到threshold值。而coldFactor的默认值是3. 例如,我设置QPS的threshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10
流控效果-排队等待
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常
热点参数限流
热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
例如: 代表的含义是:对testEResource这个资源的0号参数(第一个参数)做统计,每1秒相同参数值的请求数不能超过2
@GetMapping("/testE/{id}")
@SentinelResource(value = "testEResource",
blockHandlerClass = {MyBlockHandler.class},
blockHandler = "blockHandlerTestE"
)
public String testE(@PathVariable("id") Integer id) {
return "服务访问成功------testE..........." + id;
}
在热点参数限流的高级选项中,可以对部分参数设置例外配置:
结合上一个配置,这里的含义是对0号的int类型参数限流,每1秒相同参数的QPS不能超过2,有两个例外:
- 如果参数值是100,则每1秒允许的QPS为5
- 如果参数值是101,则每1秒允许的QPS为3
熔断降级规则 (DegradeRule)
除了流量控制以外,对调用链路中不稳定资源的熔断降级,也是保障服务高可用的重要措施之一。
在分布式微服务架构中,一个系统往往由多个服务组成,不同服务之间相互调用,组成复杂的调用链路。如果链路上的某一个服务出现故障,那么故障就会沿着调用链路在系统中蔓延,最终导致整个系统瘫痪。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 熔断状态
Sentinel 熔断降级中共涉及 3 种状态,熔断状态的之间的转换过程如下图。
Sentinel 熔断降级中共涉及 3 种状态,如下表。
状态 | 说明 | 触发条件 |
---|---|---|
熔断关闭状态 (CLOSED) | 处于关闭状态时,请求可以正常调用资源。 | 满足以下任意条件,Sentinel 熔断器进入熔断关闭状态:全部请求访问成功。单位统计时长(statIntervalMs)内请求数目小于设置的最小请求数目。未达到熔断标准,例如服务超时比例、异常数、异常比例未达到阈值。处于探测恢复状态时,下一个请求访问成功。 |
熔断开启状态 (OPEN) | 处于熔断开启状态时,熔断器会一定的时间(规定的熔断时长)内,暂时切断所有请求对该资源的调用,并调用相应的降级逻辑使请求快速失败避免系统崩溃。 | 满足以下任意条件,Sentinel 熔断器进入熔断开启状态:单位统计时长内请求数目大于设置的最小请求数目,且已达到熔断标准,例如请求超时比例、异常数、异常比例达到阈值。处于探测恢复状态时,下一个请求访问失败。 |
探测恢复状态 (HALF-OPEN) | 处于探测恢复状态时,Sentinel 熔断器会允许一个请求调用资源。则若接下来的一个请求成功完成(没有错误)则结束熔断,熔断器进入熔断关闭(CLOSED)状态;否则会再次被熔断,熔断器进入熔断开启(OPEN)状态。 | 在熔断开启一段时间(降级窗口时间或熔断时长,单位为 s)后,Sentinel 熔断器自动会进入探测恢复状态。 |
Sentinel 熔断规则属性
熔断降级规则包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
同一个资源可以同时有多个降级规则。
Sentinel 实现熔断降级过程
Sentinel 实现熔断降级的步骤如下:
- 在项目中,使用 @SentinelResource 注解的 fallback 属性可以为资源指定熔断降级逻辑(方法)。
- 通过 Sentinel 控制台或代码定义熔断规则,包括熔断策略、最小请求数、阈值、熔断时长以及统计时长等。
- 若单位统计时长(statIntervalMs)内,请求数目大于设置的最小请求数目且达到熔断标准(例如请求超时比例、异常数、异常比例达到阈值),Sentinel 熔断器进入熔断开启状态(OPEN)。
- 处于熔断开启状态时, @SentinelResource 注解的 fallback 属性指定的降级逻辑会临时充当主业务逻辑,而原来的主逻辑则暂时不可用。当有请求访问该资源时,会直接调用降级逻辑使请求快速失败,而不会调用原来的主业务逻辑。
- 在经过一段时间(在熔断规则中设置的熔断时长)后,熔断器会进入探测恢复状态(HALF-OPEN),此时 Sentinel 会允许一个请求对原来的主业务逻辑进行调用,并监控其调用结果。
- 若请求调用成功,则熔断器进入熔断关闭状态(CLOSED ),服务原来的主业务逻辑恢复,否则重新进入熔断开启状态(OPEN)。
通过 Sentinel 控制台定义熔断降级规则
通过 Sentinel 控制台直接对资源定义熔断降级规则
下面通过一个实例,来演示如何通过 Sentinel 控制台,对资源定义降级规则。
- 修改user-service的get/{id}请求,利用线程添加延迟操作,list请求正常执行模拟。
@RestController
@Slf4j
public class UserController {
@GetMapping("/get/{id}")
public String getInfo(@PathVariable("id") Integer id){
//模拟根据id获取时延迟1s
try {
TimeUnit.SECONDS.sleep(1);
log.info("休眠 1秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "用户服务正常执行,获取id为" + id + "的用户信息成功";
}
@GetMapping("/list")
public String getList(){
return "获取全部用户信息成功!!!!!!";
}
}
- 修改goods-service,基于openfeign调用用户服务get与list接口
@RestController
public class GoodsController {
@Autowired
private UserFeign userFeign;
@GetMapping("/consumer/get/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback")
public String get(@PathVariable("id") Integer id) {
//调用监听事件,用于测试熔断器状态
monitor();
//调用用户接口
String info = userFeign.getInfo(id);
if (id == 6) {
System.err.println("--------->>>>主业务逻辑,抛出非法参数异常,id = " + id);
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
//如果查到的记录也是 null 也控制正异常
}
return info;
}
@GetMapping("/consumer/list")
public String list() {
return userFeign.getList();
}
//处理异常的回退方法(服务降级)
public String handlerFallback(@PathVariable Integer id, Throwable e) {
System.err.println("--------->>>>服务降级逻辑");
return "id为" + id + "--------服务被降级!异常信息为:" + e.getMessage();
}
/**
* 自定义事件监听器,监听熔断器状态转换
*/
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 属性时,需要注意以下事项:
返回值类型必须与原函数返回值类型一致;
方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- 在goods-service中的配置文件中添加feign整合sentinel
server:
port: 8071
spring:
application:
name: goods-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置 Sentinel dashboard 地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
feign:
sentinel:
enabled: true #开启feign启用sentinel
- 启动user-service和goods-service,打开sentinel控制台,设置资源的熔断规则
熔断策略可以时慢调用比例、异常比例、异常数。以上图为例,统计时间2s内,最少请求2次,异常比例达到0.5,进行熔断,熔断时长5s。
请求异常请求:localhost:8071/consumer/get/6,频率达到2s至少两次,此时进入熔断,接着访问localhost:8071/consumer/get/4,此时正常请求默认快速失败
查看控制台打印,可以看到熔断器状态切换
系统保护规则 (SystemRule)
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
(1)Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
(2)CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
(3)平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
(4)并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
(5)入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
系统规则包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
highestSystemLoad | load1 触发值,用于触发自适应控制阶段 |
-1 (不生效) |
avgRt | 所有入口流量的平均响应时间 | -1 (不生效) |
maxThread | 入口流量的最大并发数 | -1 (不生效) |
qps | 所有入口资源的 QPS | -1 (不生效) |
highestCpuUsage | 当前系统的 CPU 使用率(0.0-1.0) | -1 (不生效) |
访问控制规则 (AuthorityRule)
很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin
)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
授权规则,即黑白名单规则(AuthorityRule
)非常简单,主要有以下配置项:
resource
:资源名,即限流规则的作用对象limitApp
:对应的黑名单/白名单,不同 origin 用,
分隔,如appA,appB
strategy
:限制模式,AUTHORITY_WHITE
为白名单模式,AUTHORITY_BLACK
为黑名单模式,默认为白名单模式
流控应用填写的是调用方标识,Sentinel提供了RequestOriginParser接口来处理标识信息。
只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser的实现类去解析调用者标识。
- 自定义调用方标识处理规则
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
/**
* 根据请求参数serviceName进行标识处理
* @param request
* @return
*/
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
StringBuffer url = request.getRequestURL();
if (url.toString().endsWith("favicon.ico")) {
// 浏览器会向后台请求favicon.ico图标
return serviceName;
}
if (StringUtils.isEmpty(serviceName)) {
throw new IllegalArgumentException("serviceName must not be null");
}
return serviceName;
}
}
在GoodsController中添加测试接口
@GetMapping("/goods/msg") public String goodsMsg(String serviceName){ return serviceName; }
配置授权规则
这个配置的意思是,serviceName=pc不能访问(黑名单)
动态规则扩展
dynamic-rule-configuration (sentinelguard.io)
规则
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则:
- 通过 API 直接修改 (
loadRules
) - 通过
DataSource
适配不同数据源修改
DataSource 扩展
上述 loadRules()
方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。DataSource
接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource
接口是更加可靠的做法。
我们推荐通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource
接口端监听规则中心实时获取变更,流程如下:
DataSource
扩展常见的实现方式有:
- 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
- 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
Sentinel 目前支持以下数据源扩展:
推模式:使用 Nacos 配置规则
Nacos 是阿里中间件团队开源的服务发现和动态配置中心。Sentinel 针对 Nacos 作了适配,底层可以采用 Nacos 作为规则配置数据源。使用时只需添加以下依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>x.y.z</version>
</dependency>
然后创建 NacosDataSource
并将其注册至对应的 RuleManager 上即可。比如:
// remoteAddress 代表 Nacos 服务端的地址
// groupId 和 dataId 对应 Nacos 中相应配置
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
Seata:Spring Cloud Alibaba分布式事务组件
随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。
例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:
- 调用订单服务创建订单数据
- 调用库存服务扣减库存
- 调用账户服务扣减账户金额
- 最后调用订单服务修改订单状态
为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。
Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
阿里巴巴对于分布式事务问题先后发布了以下解决方案:
- 2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
- 2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
- 2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
- 2019 年 fescar 被重命名为了seata(simple extensiable autonomous transaction architecture)。
- TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。
分布式事务相关概念
分布式事务主要涉及以下概念:
- 事务:由一组操作构成的可靠、独立的工作单元,事务具备 ACID 的特性,即原子性、一致性、隔离性和持久性。
- 本地事务:本地事务由本地资源管理器(通常指数据库管理系统 DBMS,例如 MySQL、Oracle 等)管理,严格地支持 ACID 特性,高效可靠。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
- 全局事务:全局事务指的是一次性操作多个资源管理器完成的事务,由一组分支事务组成。
- 分支事务:在分布式事务中,就是一个个受全局事务管辖和协调的本地事务。
可以将分布式事务理解成一个包含了若干个分支事务的全局事务。全局事务的职责是协调其管辖的各个分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足 ACID 特性的本地事务。
Seata 整体工作流程
Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。
XID
XID 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
核心组件
Seata 定义了 3 个核心组件:
- TC(Transaction Coordinator):事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
- TM(Transaction Manager):事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
- RM(Resource Manager):资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
以上三个组件相互协作,TC 以 **Seata 服务器(Server)**形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。
Seata 的整体工作流程如下:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
- XID 通过服务的调用链传递到其他服务;
- RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
- TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
- TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。
Seata AT 模式
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。
在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
AT 模式的前提
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:
- 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
- 应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。
此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表,记录回滚日志
回滚日志表
UNDO_LOG Table:不同数据库在类型上会略有差别。
以 MySQL 为例:
Field | Type |
---|---|
branch_id | bigint PK |
xid | varchar(100) |
context | varchar(128) |
rollback_info | longblob |
log_status | tinyint |
log_created | datetime |
log_modified | datetime |
-- 注意此处0.7.0+ 增加字段 context
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;
AT 模式的工作机制
Seata 的 AT 模式工作时大致可以分为以两个阶段,
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
假设某数据库中存在一张名为 product 的表,表结构如下
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段
Seata AT 模式一阶段的工作流程如下:
过程:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
- 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG
表中。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
提交前,向 TC 注册分支:申请
product
表中,主键值等于 1 的记录的 全局锁 (行锁)。以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。
本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
将本地事务提交的结果上报给 TC。
二阶段-回滚
若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交
当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。
若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。
下载 Seata 服务器
- 使用浏览器访问“https://github.com/seata/seata/releases/tag/v1.4.2”,在 Seata Server 下载页面分别下载“seata-server-1.4.2.zip”
- 解压 seata-server-1.4.2.zip,其目录结构如下图。
Seata Server 目录中包含以下子目录:
- bin:用于存放 Seata Server 可执行命令。
- conf:用于存放 Seata Server 的配置文件。
- lib:用于存放 Seata Server 依赖的各种 Jar 包。
- logs:用于存放 Seata Server 的日志。
Seata 配置中心
所谓“配置中心”,就像是一个“大衣柜”,内部存放着各种各样的配置文件,我们可以根据自己的需要从其中获取指定的配置文件,加载到对应的客户端中;比如Seata Client端(TM,RM),Seata Server(TC),会去读取全局事务开关,事务会话存储模式等信息。
Seata 支持多种配置中心:
- nacos
- consul
- apollo
- etcd
- zookeeper
- file (读本地文件,包含 conf、properties、yml 等配置文件)
Seata 整合 Nacos 配置中心
对于 Seata 来说,Nacos 是一种重要的配置中心实现。
Seata 整合 Nacos 配置中心的操作步骤十分简单,大致步骤如下。
添加Maven 依赖
首先,您需要将 nacos-client
的 Maven 依赖添加到您的项目 pom.xml
文件中,建议使用 Seata 1.4.0+
:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>最新版</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.2.0及以上版本</version>
</dependency>
在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
Seata Server 配置
在 Seata Server 安装目录下的 config/registry.conf 中,将配置方式(config.type)修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,示例配置如下。
config {
# Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心
#配置方式修改为 nacos
type = "nacos"
nacos {
#修改为使用的 nacos 服务器地址
serverAddr = "127.0.0.1:8848"
#配置中心的命名空间
namespace = ""
#配置中心所在的分组
group = "SEATA_GROUP"
#Nacos 配置中心的用户名
username = "nacos"
#Nacos 配置中心的密码
password = "nacos"
}
}
Seata Client 配置
我们可以在 Seata Client(即微服务架构中的服务)中,通过 application.yml 等配置文件对 Nacos 配置中心进行配置,示例代码如下。
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # Nacos 配置中心的地址
group : "SEATA_GROUP" #分组
namespace: ""
username: "nacos" #Nacos 配置中心的用于名
password: "nacos" #Nacos 配置中心的密码
上传配置到 Nacos 配置中心
在完成了 Seata 服务端和客户端的相关配置后,接下来,我们就可以将配置上传的 Nacos 配置中心了,操作步骤如下。
通过dataId配置
- 从v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,你只需要额外添加一个dataId配置项。
- 首先你需要在nacos新建配置,此处dataId为seataServer.properties,配置内容参考https://github.com/seata/seata/tree/develop/script/config-center 的config.txt并按需修改保存
- 在client参考如下配置进行修改,
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group : "SEATA_GROUP"
namespace: ""
dataId: "seataServer.properties"
username: "nacos"
password: "nacos"
通过脚本上传配置到Nacos
参考https://github.com/seata/seata/tree/develop/script/config-center 的config.txt并修改,之后运行仓库中提供的nacos脚本,将信息提交到nacos控制台,如果有需要更改,可直接通过控制台更改.
eg: sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca -u username -w password
在 /script/config-center/nacos 目录中,有以下 2 个 Seata 脚本:
- nacos-config.py:python 脚本。
- nacos-config.sh:为 Linux 脚本,我们可以在 Windows 下通过 Git 命令,将 config.txt 中的 Seata 配置上传到 Nacos 配置中心。
在 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 8848 -g SEATA_GROUP -u nacos -w nacos
Git 命令各参数说明如下:
- -h:Nacos 的 host,默认取值为 localhost
- -p:端口号,默认取值为 8848
- -g:Nacos 配置的分组,默认取值为 SEATA_GROUP
- -u:Nacos 用户名
- -w:Nacos 密码
验证 Nacos 配置中心
在以上所有步骤完成后,启动 Nacos Server,登陆 Nacos 控制台查看配置列表,结果如下
Seata 注册中心
- 什么是注册中心?注册中心可以说是微服务架构中的”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用.比如Seata Client端(TM,RM),发现Seata Server(TC)集群的地址,彼此通信.
- Seata的注册中心与Dubbo,Spring cloud的注册中心区别是?在广义上来说,并无区别,只不过Dubbo与Spring cloud的注册中心仅是作用于它们自身的组件,而Seata的注册中心也是一样是作用于Seata自身.(注:Dubbo与Spring cloud的注册中心与Seata无关)
在分布式微服务架构中,各个微服务都可以将自己注册到注册中心,当其他服务需要调用某个服务时,就可以从这里找到它的服务地址进行调用,常见的服务注册中心有 Nacos、Eureka、zookeeper 等。
Seata 支持多种服务注册中心:
- eureka
- consul
- nacos
- etcd
- zookeeper
- sofa
- redis
- file (直连)
Seata 通过这些服务注册中心,我们可以获取 Seata Sever 的服务地址,进行调用。
Seata 整合 Nacos 注册中心
对于 Seata 来说,Nacos 是一种重要的注册中心实现。
Seata 整合 Nacos 注册中心的步骤十分简单,步骤如下。
添加 Maven 依赖
将 nacos-client 的 Maven 依赖添加到项目的 pom.xml 文件中:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>最新版</version>
</dependency><dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.2.0及以上版本</version>
</dependency>
在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
Seata Server 配置注册中心
在 Seata Server 安装目录下的 config/registry.conf 中,将注册方式(registry.type)修改为 Nacos,并对 Nacos 注册中心的相关信息进行配置,示例配置如下。
registry {
# Seata 支持 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 作为其注册中心
# 将注册方式修改为 nacos
type = "nacos"
nacos {
application = "seata-server"
# 修改 nacos 注册中心的地址
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
在 Seata Client 的 application.yml 中,对 Nacos 注册中心进行配置,示例配置如下。
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848 # Nacos 注册中心的地址
group : "SEATA_GROUP" #分组
namespace: ""
username: "nacos" #Nacos 注册中心的用户名
password: "nacos" # Nacos 注册中心的密码
验证 Nacos 注册中心
在以上所有步骤完成后,先启动 Nacos Server 再启动 Seata Server,登录 Nacos 控制台查看服务列表,结果如下图。
Seata 事务分组
事务分组是 Seata 提供的一种 TC(Seata Server) 服务查找机制。
事务分组如何找到后端Seata集群?
- 首先应用程序(客户端)中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数)。若应用程序是SpringBoot则通过seata.tx-service-group 配置
- 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping .[事务分组配置项],取得配置项的值就是TC集群的名称。若应用程序是SpringBoot则通过seata.service.vgroup-mapping.事务分组名=集群名称 配置
- 拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同(前提是Seata-Server已经完成服务注册,且Seata-Server向注册中心报告cluster名与应用程序(客户端)配置的集群名称一致)
- 拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表(即Seata-Server集群节点列表)
以 Nacos 服务注册中心为例
Seata Server 配置
在 Seata Server 的 config/registry.conf 中,进行如下配置。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #使用 Nacos作为注册中心
nacos {
serverAddr = "127.0.0.1:1111" # Nacos 注册中心的地址
namespace = "" # Nacos 命名空间id,"" 为 Nacos 保留 public 空间控件,用户勿配置 namespace = "public"
cluster = "default" # seata-server在 Nacos 的集群名
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos" # 使用nacos作为配置中心
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
}
Seata Client 配置
Seata Client 中 application.yml 的配置如下。
spring:
cloud:
alibaba:
seata:
tx-service-group: service-order-group #事务分组名
seata:
registry:
type: nacos #从 Nacos 获取 TC 服务
nacos:
server-addr: 127.0.0.1:8848
config:
type: nacos #使用 Nacos 作为配置中心
nacos:
server-addr: 127.0.0.1:8848
在以上配置中,通过 spring.cloud.alibaba.seata.tx-service-group 来配置 Seata 事务分组名,其默认取值为:服务名-fescar-service-group
上传配置到 Nacos
将以下配置上传到 Nacos 配置中心。
service.vgroupMapping.service-order-group=default
在以上配置中,
- service-order-group:为事务分组的名称;
- default:为 TC 集群的名称。
获取事务分组
先启动 Nacos,再启动 Seata Server,最后再启动 Seata Client。
Seata Client 在启动时,会从 application.yml 的配置中,根据 spring.cloud.alibaba.seata.tx-service-group 获取事务分组的名称:service-order-group。
获取 TC 集群名
使用事务分组名“service-order-group”拼接成“service.vgroupMapping.service-order-group”,并从 Nacos 配置中心获取该配置的取值,这个值就是 TC 集群的名称:“default”。
查找 TC 服务
根据 TC 集群名、Nacos 注册中心的地址(server-addr)以及命名空间(namespace),在 Nacos 注册中心找到真实的 TC 服务列表。
总结
通过事务分组获取服务名,共需要以下 3 步:
- 服务启动时,从配置文件中获取服务分组的名称;
- 从配置中心,通过事务分组名获取 TC 集群名;
- 根据 TC 群组名以及其他信息构建服务名,获取真实的 TC 服务列表。
启动 Seata Server
1. 建表
Seata Server 共有以下 3 种存储模式(store.mode):
模式 | 说明 | 准备工作 |
---|---|---|
file | 文件存储模式,默认存储模式; 该模式为单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 root.data,性能较高 | - |
db | 数据库存储模式; 该模式为高可用模式,全局事务会话信息通过数据库共享,性能较低。 | 建数据库表 |
redis | 缓存处处模式; Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险, | 配置 redis 持久化配置 |
在 db 模式下,我们需要针对全局事务的会话信息创建以下 3 张数据库表。
- 全局事务表,对应的表为:global_table
- 分支事务表,对应的表为:branch_table
- 全局锁表,对应的表为:lock_table
在 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 = InnoDB
DEFAULT 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 = InnoDB
DEFAULT 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 = InnoDB
DEFAULT CHARSET = utf8;
2. 修改 Seata Server 配置
在 seata-server-1.4.2/conf/ 目录下的 registry.conf 中,将 Seata Server 的服务注册方式(registry.type)和配置方式(config.type)都修改为 Nacos,修改内容如下。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 将注册方式修改为 nacos
type = "nacos"
nacos {
application = "seata-server"
# 修改 nacos 的地址
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
#配置方式修改为 nacos
type = "nacos"
nacos {
#修改为使用的 nacos 服务器地址
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
#不使用 seataServer.properties 方式配置
#dataId = "seataServer.properties"
}
}
3. 将 Seata 配置上传到 Nacos
- 下载并解压 Seata Server 的源码 seata-1.4.2.zip,然后修改 seata-1.4.2/script/config-center 目录下的 config.txt,修改内容如下。
#将 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=qwer1234
# 自定义事务分组
service.vgroupMapping.service-order-group=default
service.vgroupMapping.service-storage-group=default
service.vgroupMapping.service-account-group=default
- 在 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 8848 -g SEATA_GROUP -u nacos -w nacos
- 当 Git 命令窗口出现以下执行日志时,则说明配置上传成功。
=========================================================================
Complete initialization parameters, total-count:87 , failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.
- 使用浏览器访问 Nacos 服务器主页,查看 Nacos 配置列表,如下图。
注意:在使用 Git 命令将配置上传到 Nacos 前,应该先确保 Nacos 服务器已启动。
4. 启动 Seata Server
双击 Seata Server 端 bin 目录下的启动脚本 seata-server.bat ,启动 Seata Server。
Seata Server 启动日志如下。
查看nacos
业务系统集成 Seata
接下来,就以电商系统为例,来演示下业务系统是如何集成 Seata 的。
用例
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
架构图
- Order(订单服务):创建和修改订单。
- Storage(库存服务):对指定的商品扣除仓库库存。
- Account(账户服务) :从用户帐户中扣除商品金额。
这三个微服务分别使用三个不同的数据库,架构图如下所示。
当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:
- 调用 Order 服务,创建一条订单数据,订单状态为“未完成”;
- 调用 Storage 服务,扣减商品库存;
- 调用 Account 服务,从用户账户中扣除商品金额;
- 调用 Order 服务,将订单状态修改为“已完成”。
建立数据库
创建数据库seata-demo用于模拟业务
UNDO_LOG
表
-- 注意此处0.3.0+ 增加唯一索引 ux_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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
创建storage_tbl、order_tbl、account_tbl表
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建订单(Order)服务
1. 创建订单服务seata-order-8501:
pom文件如下:
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
2. 添加配置文件bootstrap.yml
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:8848 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 添加配置文件application.yml
server:
port: 8501 #端口
spring:
application:
name: seata-order-8501 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动
url: jdbc:mysql://localhost:3306/seata-demo?serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: qwer1234 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos 服务器地址
namespace: public #nacos 命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
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: edu.zut.entity
4. 创建实体类Order与CommonResult
@Data
public class Order {
private Integer id;
private String user_id;
private String commodity_code;
private Integer count;
private Integer money;
}
public class CommonResult {
private Integer code;
private String msg;
private Object data;
public CommonResult(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public CommonResult() {
}
public CommonResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5. 创建数据层OrderMapper
@Mapper
public interface OrderMapper {
//1. 创建订单
@Insert("insert into order_tbl (user_id, commodity_code,count,money) " +
"values (#{user_id},#{commodity_code},#{count},#{money})")
int create(Order order);
}
6. 创建业务类接口OrderService、openFeign接口AccountService、StorageService
public interface OrderService {
CommonResult create(String userId, String commodityCode, int orderCount, int money);
}
@FeignClient(value = "seata-account-8701")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") String userId, @RequestParam("money") Integer money);
}
@FeignClient(value = "seata-storage-8601")
public interface StorageService {
//扣减库存
@GetMapping(value = "/storage/decrease")
public CommonResult decrease(@RequestParam("commodity_code") String commodity_code, @RequestParam("count") Integer count);
}
7. 创建OrderServiceImpl实现OrderService
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单
*
* @param userId
* @param commodityCode
* @param orderCount
* @return
*/
@Override
public CommonResult create(String userId, String commodityCode, int orderCount, int money) {
log.info("--------开始下单-------");
Order order = new Order();
order.setUser_id(userId);
order.setCommodity_code(commodityCode);
order.setCount(orderCount);
order.setMoney(money);
//新建订单
orderMapper.create(order);
log.info("扣除库存");
storageService.decrease(commodityCode,orderCount);
log.info("扣减账户");
accountService.decrease(userId,money);
log.info("下单结束");
return new CommonResult(200,"订单创建成功");
}
}
8. 创建OrderController提供访问接口
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* @param productId
* @param count
* @param money
* @param userId
* @return
*/
@GetMapping("/order/create/{productId}/{count}/{money}/{userId}")
public CommonResult create(@PathVariable("productId") String productId, @PathVariable("count") Integer count
, @PathVariable("money") Integer money,@PathVariable("userId") String userId) {
return orderService.create(userId,productId,count,money);
}
}
9. 创建启动类,添加相应注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
创建库存(storage)服务
1. pom如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>alibaba-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-storage-8601</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
</project>
2. 配置文件bootstrap.yml如下
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:8848 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. application.yml如下
server:
port: 8601 #端口
spring:
application:
name: seata-storage-8601 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动
url: jdbc:mysql://localhost:3306/seata-demo?serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: qwer1234 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos 服务器地址
namespace: public #nacos 命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-storage-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-storage-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
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: edu.zut.entity
4. 创建实体类
@Data
public class Storage {
private Integer id;
private String commodity_code;
private Integer count;
}
package edu.zut.entity;
public class CommonResult {
private Integer code;
private String msg;
private Object data;
public CommonResult(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public CommonResult() {
}
public CommonResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5.创建数据层接口
@Mapper
public interface StorageMapper {
@Select("select * from storage_tbl where commodity_code = #{code}")
Storage selectByProductId(String commodity_code);
@Update("update storage_tbl set count = #{count} where commodity_code=#{commodity_code}")
int decrease(Storage record);
}
6.创建业务接口以及实现类
public interface StorageService {
int decrease(String commodity_code, Integer count);
}
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Override
public int decrease(String commodity_code, Integer count) {
log.info("------->storage-service中扣减库存开始");
log.info("------->storage-service 开始查询商品是否存在");
Storage storage = storageMapper.selectByProductId(commodity_code);
log.info(storage.toString());
if (storage != null && storage.getCount() >= count) {
storage.setCount(storage.getCount() - count);
log.info(storage.toString());
int decrease = storageMapper.decrease(storage);
log.info("------->storage-service 扣减库存成功");
return decrease;
} else {
log.info("------->storage-service 库存不足,开始回滚!");
throw new RuntimeException("库存不足,扣减库存失败!");
}
}
}
7.创建控制层提供访问接口
@RestController
public class StorageController {
@Value("${server.port}")
private String serverPort;
@Autowired
private StorageService storageService;
@GetMapping(value = "/storage/decrease")
public CommonResult decrease(@RequestParam("commodity_code") String commodity_code, @RequestParam("count") Integer count) {
int decrease = storageService.decrease(commodity_code, 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;
}
}
8.添加启动类
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class,args);
}
}
创建账户(account)服务
1.pom如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>alibaba-parent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-account-8701</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
</project>
2.bootstrap.yml配置文件如下
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:8848 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3.application.yml配置文件
server:
port: 8701 #端口
spring:
application:
name: seata-account-8701 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动
url: jdbc:mysql://localhost:3306/seata-demo?serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: qwer1234 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos 服务器地址
namespace: public #nacos 命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-account-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-account-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
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: edu.zut.entity
4.实体类
@Data
public class Account {
private Integer id;
private String user_id;
private Integer money;
}
public class CommonResult {
private Integer code;
private String msg;
private Object data;
public CommonResult(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public CommonResult() {
}
public CommonResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5.数据层接口
@Mapper
public interface AccountMapper {
@Select("select * from account_tbl where user_id = #{userId}")
Account selectByUserId(String userId);
@Delete("update account_tbl set money = money - #{money} where user_id = #{userId}")
int decrease(@Param("userId") String userId, @Param("money") Integer money);
}
6.业务层接口以及实现类
public interface AccountService {
/**
* 扣减账户余额
*
* @param userId 用户id
* @param money 金额
*/
int decrease(@RequestParam("userId") String userId, @RequestParam("money") int money);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public int decrease(String userId, int money) {
log.info("------->account-service 开始查询账户余额");
Account account = accountMapper.selectByUserId(userId);
log.info("------->account-service 账户余额查询完成," + account);
if (account != null && account.getMoney() >= money) {
log.info("------->account-service 开始从账户余额中扣钱!");
int decrease = accountMapper.decrease(userId, money);
log.info("------->account-service 从账户余额中扣钱完成");
return decrease;
} else {
log.info("账户余额不足,开始回滚!");
throw new RuntimeException("账户余额不足!");
}
}
}
7.控制层提供访问接口
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping(value = "/account/decrease")
public CommonResult decrease(@RequestParam("userId") String userId, @RequestParam("money") Integer money){
int decrease = accountService.decrease(userId, money);
return new CommonResult(200,"扣款成功");
}
}
8.添加启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
启动测试
依次启动 Nacos Server(集群)和 Seata Server(集群),最后启动seata-order-8501,seata-account-8701,seata-storage-8601服务,当控制台出现以下日志时,说明该服务已经成功连接上 Seata Server(TC)。
使用浏览器访问http://localhost:8501/order/create/1001/2/20/1,创建订单
查看数据库,验证订单表 、库存表、账户表都做了更新
接着,使用浏览器访问http://localhost:8501/order/create/1001/2/2000/1,此时订单金额超过账户余额,因此账户服务会抛出“账户余额不足!”的运行时异常。
但是查看order表却发现订单创建成功,同时库存已扣除,账户余额未扣除,此时我们的订单业务出现数据不一致,原因在于不同的服务之间各自的事务不相关
@GlobalTransactional 注解
在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。
当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。
@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:
- 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
- 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。
为了解决上述分布式事务问题,在订单访问接口处添加 @GlobalTransaction 注解,指定使用分布式事务
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 使用 @GlobalTransactional 注解对分布式事务进行管理
* @param productId
* @param count
* @param money
* @param userId
* @return
*/
@GetMapping("/order/create/{productId}/{count}/{money}/{userId}")
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public CommonResult create(@PathVariable("productId") String productId, @PathVariable("count") Integer count
, @PathVariable("money") Integer money,@PathVariable("userId") String userId) {
return orderService.create(userId,productId,count,money);
}
}
重启订单(Order)服务、库存(Storage)服务和账户(Account)服务,重新访问
http://localhost:8501/order/create/1001/2/2000/1
此时会显式的看到异常信息,因为余额不足,出现异常
同时,观察订单以及库存服务控制台日志打印,可以看到回滚日志
再次查看数据库,不在出现数据不一致问题!!!