【java】Spring Cloud --Spring Cloud Alibaba 教程

文章目录

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 是阿里巴巴结合自身丰富的微服务实践而推出的微服务开发的一站式解决方案,是 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 包含了多种开发分布式微服务系统的必需组件

  • 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 的应用场景如下:

  • 大型复杂的系统,例如大型电商系统。
  • 高并发系统,例如大型门户网站、商品秒杀系统。
  • 需求不明确,且变更很快的系统,例如创业公司业务系统。

Spring Cloud 两代实现组件对比

下表展示了 Spring Cloud 两代实现的组件对比情况。

Spring Cloud 第一代实现(Netflix)状态Spring Cloud 第二代实现(Alibaba)状态
Ereka2.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.12021.12.4.2
Spring Cloud Hoxton.SR122.2.7.RELEASE2.3.12.RELEASE
Spring Cloud Hoxton.SR92.2.6.RELEASE2.3.2.RELEASE
Spring Cloud Greenwich.SR62.1.4.RELEASE2.1.13.RELEASE
Spring Cloud Hoxton.SR32.2.1.RELEASE2.2.5.RELEASE
Spring Cloud Hoxton.RELEASE2.2.0.RELEASE2.2.X.RELEASE
Spring Cloud Greenwich2.1.2.RELEASE2.1.X.RELEASE
Spring Cloud Finchley2.0.4.RELEASE(停止维护,建议升级)2.0.X.RELEASE
Spring Cloud Edgware1.5.1.RELEASE(停止维护,建议升级)1.5.X.RELEASE

Spring Cloud Alibaba 组件版本关系

Spring Cloud Alibaba 下各组件版本关系如下表。

Spring Cloud Alibaba 版本Sentinel 版本Nacos 版本RocketMQ 版本Dubbo 版本Seata 版本
2.2.7.RELEASE1.8.12.0.34.6.12.7.131.3.0
2.2.6.RELEASE1.8.11.4.24.4.02.7.81.3.0
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE1.8.01.4.14.4.02.7.81.3.0
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE1.8.01.3.34.4.02.7.81.3.0
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE1.7.11.2.14.4.02.7.61.2.0
2.2.0.RELEASE1.7.11.1.44.4.02.7.4.11.0.0
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE1.7.01.1.44.4.02.7.30.9.0
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE1.6.31.1.14.4.02.7.30.7.1

Spring Cloud Alibaba Nacos

Nacos 英文全称为 Dynamic Naming and Configuration Service,是一个由阿里巴巴团队使用 Java 语言开发的开源项目。

Nacos 是一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台(参考自 Nacos 官网)。

Nacos 的命名是由 3 部分组成:

组成部分全称描述
Nanaming/nameServer即服务注册中心,与 Spring Cloud Eureka 的功能类似。
coconfiguration即配置中心,与 Spring Cloud Config+Spring Cloud Bus 的功能类似。
sservice即服务,表示 Nacos 实现的服务注册中心和配置中心都是以服务为核心的。

我们可以将 Nacos 理解成服务注册中心和配置中心的组合体,它可以替换 Eureka 作为服务注册中心,实现服务的注册与发现;还可以替换 Spring Cloud Config 作为配置中心,实现配置的动态刷新。

Nacos 作为服务注册中心经历了十年“双十一”的洪峰考验,具有简单易用、稳定可靠、性能卓越等优点,可以帮助用户更敏捷、容易地构建和管理微服务应用。

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 ServerNacos 服务端,与 Eureka Server 不同,Nacos Server 由阿里巴巴团队使用 Java 语言编写并将 Nacos Server 的下载地址给用户,用户只需要直接下载并运行即可。Nacos Server 可以作为服务注册中心,帮助 Nacos Client 实现服务的注册与发现。Nacos Server 可以作为配置中心,帮助 Nacos Client 在不重启的情况下,实现配置的动态刷新。
Nacos ClientNacos 客户端,通常指的是微服务架构中的各个服务,由用户自己搭建,可以使用多种语言编写。Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-discovery,在服务注册中心(Nacos Server)中实现服务的注册与发现。
Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-config,在配置中心(Nacos Server)中实现配置的动态刷新。

Nacos 服务注册中心

Nacos 作为服务注册中心可以实现服务的注册与发现,流程如下图。

Nacos 服务注册与发现

图1:Nacos 服务注册与发现

在图 1 中共涉及到以下 3 个角色:

  • 服务注册中心(Register Service):它是一个 Nacos Server,可以为服务提供者和服务消费者提供服务注册和发现功能。
  • 服务提供者(Provider Service):它是一个 Nacos Client,用于对外服务。它将自己提供的服务注册到服务注册中心,以供服务消费者发现和调用。
  • 服务消费者(Consumer Service):它是一个 Nacos Client,用于消费服务。它可以从服务注册中心获取服务列表,调用所需的服务。

Nacos 实现服务注册与发现的流程如下:

  1. 从 Nacos 官方提供的下载页面中,下载 Nacos Server 并运行。
  2. 服务提供者 Nacos Client 启动时,会把服务以服务名(spring.application.name)的方式注册到服务注册中心(Nacos Server);
  3. 服务消费者 Nacos Client 启动时,也会将自己的服务注册到服务注册中心;
  4. 服务消费者在注册服务的同时,它还会从服务注册中心获取一份服务注册列表信息,该列表中包含了所有注册到服务注册中心上的服务的信息(包括服务提供者和自身的信息);
  5. 在获取了服务提供者的信息后,服务消费者通过 HTTP 或消息中间件远程调用服务提供者提供的服务。

安装和运行 Nacos Server

下面我们以 Nacos 2.0.3 为例,演示下如何安装和运行 Nacos Server,步骤如下。

  1. 使用浏览器访问 Nacos Server 下载页面,并在页面最下方点击链接 nacos-server-2.0.3.zip,如下图。

Nacos 下载

图2:Nacos Server 下载

  1. 下载完成后,解压 nacos-server-2.0.3.zip,目录结构如下。

在这里插入图片描述

图3:Nacos Server 目录结构

Nacos Server 下各目录说明如下:

  • bin:用于存放 Nacos 的可执行命令。
  • conf:用于存放 Nacos 配置文件。
  • target:用于存放 Nacos 应用的 jar 包。
  1. 打开命令行窗口,跳转到 Nacos Server 安装目录的 bin 下,执行以下命令,以单机模式启动 Nacos Server。
startup.cmd -m standalone
  1. Nacos Server 启动日志如下。
"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
  1. 使用浏览器访问“http://localhost:8848/nacos”,跳转到 Nacos Server 登陆页面,如下图。

Nacos 登陆页面

图4:Nacos Server 登陆页面

  1. 在登陆页输入登录名和密码(默认都是 nacos),点击提交按钮,跳转到 Nacos Server 控制台主页,如下图。

Nacos Server 主页

图5:Nacos Server 控制台

自此,我们就完成了 Nacos Server 的下载、安装和运行工作。

搭建服务提供者

接下来,我们来搭建一个服务提供者,步骤如下。

  1. 创建一个名为 spring-cloud-alibaba-demo 的 Maven 工程 ,该工程的 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>
    <packaging>pom</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-demo</artifactId>
    <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-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!--Spring Cloud Alibaba 的版本信息-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.1</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>
        </dependencies>
    </dependencyManagement>
</project>

在该工程的 pom.xml 中,我们通过 dependencyManagement 对 Spring Cloud Alibaba 的版本信息进行管理,该工程下的各个子模块在引入 Spring Cloud Alibaba 的各个组件时就不要单独指定版本号了。

  1. 在 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-provider-8001 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父工程-->
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-provider-8001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-provider-8001</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>
        <!--Spring Cloud Alibaba Nacos discovery -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-provider-8001 的配置文件 application.properties 中添加以下配置,内容如下。
#端口号
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=*
  1. 在 net.biacheng.c.controller 包下,创建一个名为 DeptController 的 Controller 类,代码如下。
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 "<h2>提醒您,服务访问成功!</h2>服务名:spring-cloud-alibaba-provider<br /> 端口号: " + serverPort + "<br /> 传入的参数:" + id;
    }
}
  1. 在 spring-cloud-alibaba-provider-8001 的主启动类上,使用 @EnableDiscoveryClient 注解开启 Nacos 服务发现功能,代码如下。
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);
    }
}
  1. 启动 spring-cloud-alibaba-provider-8001,使用浏览器访问“http://localhost:8001/dept/nacos/1”。

  2. 使用浏览器访问“http://localhost:8848/nacos”,查看“服务管理”下的“服务列表”,如下图。

Nacos 服务列表

图7:服务注册列表

从图 7 可以看到,我们搭建的服务提供者 spring-cloud-alibaba-provider-8001 所提供的服务已经被注册到了 Nacos Server 上了。

搭建服务消费者

下面,我们就来搭建一个服务消费者来消费 spring-cloud-alibaba-provider-8001 提供的服务,步骤如下。

  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-consumer-nacos-8801 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-consumer-nacos-8081</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-consumer-nacos-8081</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </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.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>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意:由于 Netflix Ribbon 已经进入停更维护状态,Nacos Discovery 已经停了对 Ribbon 的支持,因此我们需要在服务消费者的 pom.xml 中引入 spring-cloud-loadbalancer 才能调用服务提供者提供的服务。

  1. 在 spring-cloud-alibaba-consumer-nacos-8801 的配置文件 application.yml 中,添加以下配置。
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 #服务提供者的服务名
  1. 在 spring-cloud-alibaba-consumer-nacos-8801 的主启动类上,使用 @EnableDiscoveryClient 注解开启服务发现功能,代码如下。
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);
    }
}
  1. 在 net.demo.c.config 包下创建一个名为 ApplicationContextBean 的配置类,并使用 @LoadBalanced 注解与 Ribbon 进行集成开启负载均衡功能,代码如下。
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();
    }
}
  1. 在 net.demo.c.controller 包下,创建一个名为 DeptController_Consumer 的 Controller 类,代码如下。
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 {
    @Resource
    private 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);
    }
}
  1. 启动 spring-cloud-alibaba-consumer-nacos-8801,查看 Nacos Server 的服务列表,如下图。

服务消费者这 Nacos Server

图8:服务消费者

  1. 使用浏览器访问“http://localhost:8801/consumer/dept/nacos/1”。

Nacos 配置中心

Nacos Server 还可以作为配置中心,对 Spring Cloud 应用的外部配置进行统一地集中化管理。而我们只需要在应用的 POM 文件中引入 spring-cloud-starter-alibaba-nacos-config 即可实现配置的获取与动态刷新。

从配置管理的角度看,Nacos 可以说是 Spring Cloud Config 的替代方案,但相比后者 Nacos 的使用更简单,操作步骤也更少。

接下来,我们通过一个实例来演示下 Nacos 是如何实现配置的统一管理和动态刷新的。

  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-config-client-3377 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-config-client-3377</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-nacos-config-client-3377</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </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-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 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>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意:我们使用的 Spring Cloud 2020 版本默认不启用 bootstrap,若想要在应用启动时加载 bootstrap 配置(例如 bootstrap.yml 或 bootstrap.properties),就需要我们在 pom.xml 中显式的引入 spring-cloud-starter-bootstrap 依赖。

  1. 在 spring-cloud-alibaba-config-client-3377 的类路径(例如 /resources 目录)下,添加一个 bootstrap.yml,配置内容如下。
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格式的配置
  1. 在 spring-cloud-alibaba-config-client-3377 的类路径(例如 /resources 目录)下,添加一个 application.yml,配置内容如下。
spring:
  profiles:
    active: dev #激活 dev 的配置
  1. 在 net.demo.c.controller 包下,创建一个名为 ConfigClientController 的 Controller 类,并在该类上使用 @RefreshScope 注解实现配置的自动更新,代码如下。
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;
    }
}
  1. 在 spring-cloud-alibaba-config-client-3377 的主启动类上,使用 @EnableDiscoveryClient 注解开启服务发现功能,代码如下。
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);
    }
}
  1. 启动 Nacos Server,并在 Nacos Server 控制台的“配置管理”下的“配置列表”中,点击“+”按钮,新建如下配置。
Data ID:        config-client-dev.yaml

Group  :        DEFAULT_GROUP

配置格式:        YAML

配置内容:      config:
                  info: c.demo.net

在 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。
  1. 启动 spring-cloud-alibaba-config-client-3377,并使用浏览器访问“http://localhost:3377/config/info”,结果如下图。

Nacos Config

图10:Nacos Config

  1. 在 Nacos Server 中,将 config-client-dev.yaml 中的配置修改成如下内容。
config:
    info: this is c.demo.net
  1. 在不重启 spring-cloud-alibaba-config-client-3377 的情况下,使用浏览器再次访问“http://localhost:3377/config/info”,结果如下图。
    在这里插入图片描述

图11:Nacos Cofig

Nacos Server 集群化部署

在实际的项目开发中,一个微服务系统往往由十几,几十个甚至几百个微服务组成。 这些服务若全部注册到同一台 Nacos Server,就极有可能导致 Nacos Server 因为不堪重负而崩溃,最终导致整个微服务系统瘫痪。解决这个问题最直接的办法就是使用 Nacos Server 集群。

Nacos Server 的集群化部署有一个十分明显的优点,那就是可以保障系统的高可用性。在集群化部署中,只要不是所有的 Nacos Server 都停止工作,Nacos Client 就还可以从集群中正常的 Nacos Server 上获取服务信息及配置,而不会导致系统的整体瘫痪,这就是 Nacos Server 集群化部署的高可用性。

下图展示了 Nacos Server 集群化部署的基本架构。

Nacos Server 集群架构

图12:Nacos Server 集群架构

下面我们以 Windows 系统为例,演示如何部署 Nacos Server 集群。

  1. 在 MySQL 中,新建一个名为 nacos_config 的数据库实例,并在该数据库下执行以下 SQL 语句。
/******************************************/
/*   数据库全名 = 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');
  1. 在 Nacos Server 安装目录下的 conf 文件夹中,将 cluster.conf.example 重命名为 cluster.conf,然后在该文件中添加以下内容。
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。

  1. 在 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=root
4. 将该 Nacos Server 目录复制到另外两台机器上,并将它们的端口号分别修改为: 4444 和 5555。

  1. 下载 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;
        }
    }
}
  1. 启动集群中所有的 Nacos Server,当出现以下内容时,表示 Nacos Server 启动成功。
"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
  1. 当集群中的所有 Nacos Server 都启动成功后,双击 Nignx 安装目录下的 nginx.exe,启动 Nginx。

Nginx

图13:Nginx 启动脚本

  1. 使用浏览器访问“http://localhost:1111/nacos/”,若成功访问 Nacos Server 的控制台,则说明 Nacos 集群部署成功,如下图。
    在这里插入图片描述

图14:Nacos 集群

  1. 将主工程 spring-cloud-alibaba-demo 下所有子模块配置文件中的 Nacos Server 地址统一修改为:localhost:1111。我们以 spring-cloud-alibaba-consumer-nacos-8801 为例,配置文件 application.yml 的配置内容如下。
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 #服务提供者的服务名
  1. 重启 spring-cloud-alibaba-consumer-nacos-8801,并使用浏览器访问“http://localhost:1111/nacos”,查看“服务管理”下的“服务列表”,结果如下图。

Nacos 集群 2

图15:服务注册到 Nacos Server 集群上

Spring Cloud Alibaba Sentinel

Sentinel 是由阿里巴巴中间件团队开发的开源项目,是一种面向分布式微服务架构的轻量级高可用流量控制组件。

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 的核心库不依赖任何框架或库,能够运行于 Java 8 及以上的版本的运行时环境中,同时对 Spring Cloud、Dubbo 等微服务框架提供了很好的支持。
  • Sentinel 控制台(Dashboard):Sentinel 提供的一个轻量级的开源控制台,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。

Sentinel 核心库不依赖 Sentinel Dashboard,但两者结合使用可以有效的提高效率,让 Sentinel 发挥它最大的作用。

Sentinel 的基本概念

Sentinel 的基本概念有两个,它们分别是:资源和规则。

基本概念描述
资源资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。我们可以通过 Sentinel 提供的 API 来定义一个资源,使其能够被 Sentinel 保护起来。通常情况下,我们可以使用方法名、URL 甚至是服务名来作为资源名来描述某个资源。
规则围绕资源而设定的规则。Sentinel 支持流量控制、熔断降级、系统保护、来源访问控制和热点参数等多种规则,所有这些规则都可以动态实时调整。

@SentinelResource 注解

@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表。

属性说明必填与否使用要求
value用于指定资源的名称必填-
entryTypeentry 类型可选项(默认为 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 提供了一个轻量级的开源控制台 Sentinel Dashboard,它提供了机器发现与健康情况管理、监控(单机和集群)、规则管理与推送等多种功能。

Sentinel 控制台提供的功能如下:

  • 查看机器列表以及健康情况:Sentinel 控制台能够收集 Sentinel 客户端发送的心跳包,判断机器是否在线。
  • 监控(单机和集群聚合):Sentinel 控制台通过 Sentinel 客户端暴露的监控 API,可以实现秒级的实时监控。
  • 规则管理和推送:通过 Sentinel 控制台,我们还能够针对资源定义和推送规则。
  • 鉴权:从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。

Sentinel Dashboard 是我们配置和管理规则(例如流控规则、熔断降级规则等)的重要入口之一。通过它,我们不仅可以对规则进行配置和管理,还能实时查看规则的效果。

安装 Sentinel 控制台

下面我们就来演示下,如何下载和安装 Sentinel 控制台,具体步骤如下。

  1. 使用浏览器访问 Sentinel Dashboard 下载页面下载 Sentinel 控制台的 jar 包,如下图。

Sentinel Dashboard 下载

图1:Sentinel 控制台下载

  1. 打开命令行窗口,跳转到 Sentinel Dashboard jar 包所在的目录,执行以下命令,启动 Sentinel Dashboard。
java -jar sentinel-dashboard-1.8.2.jar
  1. 启动完成后,使用浏览器访问“http://localhost:8080/”,跳转到 Sentinel 控制台登陆页面,如下图。

sentinel dashboard 登录页

图2:Sentinel 控制台登录页

  1. 分别输入用户名和密码(默认都是 sentinel),点击下方的登录按钮,结果如下图。

Sentinel Dashboard

图3:Sentinel 控制台主页

Sentinel 的开发流程

Sentinel 的开发流程如下:

  1. 引入 Sentinel 依赖:在项目中引入 Sentinel 的依赖,将 Sentinel 整合到项目中;
  2. 定义资源:通过对主流框架提供适配或 Sentinel 提供的显式 API 和注解,可以定义需要保护的资源,此外 Sentinel 还提供了资源的实时统计和调用链路分析;
  3. 定义规则:根据实时统计信息,对资源定义规则,例如流控规则、熔断规则、热点规则、系统规则以及授权规则等。
  4. 检验规则是否在生效:运行程序,检验规则是否生效,查看效果。

引入 Sentinel 依赖

为了减少开发的复杂程度,Sentinel 对大部分的主流框架都进行了适配,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux 和 Reactor 等。以 Spring Cloud 为例,我们只需要引入 spring-cloud-starter-alibaba-sentinel 的依赖,就可以方便地将 Sentinel 整合到项目中。

下面我们就通过一个简单的实例,演示如何将 Sentinel 整合到 Spring Cloud 项目中,步骤如下。

  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-service-8401</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-sentinel-service-8401</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </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>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-sentinel-service-8401 的类路径下,新建一个配置文件 application.yml,配置内容如下。
server:
  port: 8401 #端口
spring:
  application:
    name: sentinel-service #服务名
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心(集群)地址
        server-addr: localhost:1111
    sentinel:
      transport:
        #配置 Sentinel dashboard 地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719
management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 在 net.demo.c.controller 下,创建一个名为 SentinelFlowLimitController 的 Controller 类,代码如下。
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"
    }
}
  1. spring-cloud-alibaba-sentinel-service-8401 主启动类代码如下。
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);
    }
}
  1. 依次启动 Nacos Server 集群、 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”.

  2. 使用浏览器访问 Sentinel 控制台主页,我们发现在“首页”下方新增了一个“sentinel-servcie”的菜单,而这正是 spring-cloud-alibaba-sentinel-service-8401 的服务名(spring.application.name),说明 Sentinel 已经监控到这个服务,如下图。
    在这里插入图片描述

图5:Sentinel 控制台主页

  1. 点击“实时监控”,查看 sentinel-service 下各请求的实时监控数据,如下图所示。

在这里插入图片描述

图6:Sentinel 实时监控

定义资源

资源是 Sentinel 中的核心概念之一。在项目开发时,我们只需要考虑这个服务、方法或代码是否需要保护,如果需要保护,就可以将它定义为一个资源。

Sentinel 为我们提供了多种定义资源的方式:

  • 适配主流框架自动定义资源
  • 通过 SphU 手动定义资源
  • 通过 SphO 手动定义资源
  • 注解方式定义资源

适配主流框架自动定义资源

Sentinel 对大部分的主流框架都进行了适配,我们只要引入相关的适配模块(例如 spring-cloud-starter-alibaba-sentinel),Snetinel 就会自动将项目中的服务(包括调用端和服务端)定义为资源,资源名就是服务的请求路径。此时,我们只要再定义一些规则,这些资源就可以享受到 Sentinel 的保护。

我们可以在 Sentinel 控制台的“簇点链路”中,直接查看被 Sentinel 监控的资源,如下图。

Sentinel 簇点链路

图7:Sentinel 控制台-簇点链路
通过 SphU 手动定义资源
Sentinel 提供了一个名为 SphU 的类,它包含的 try-catch 风格的 API ,可以帮助我们手动定义资源。

下面我们就通过一个实例,来演示下如何通过 SphU 定义资源。

  1. 在 spring-cloud-alibaba-sentinel-service-8401 下的 SentinelFlowLimitController 中,新增一个 testAbySphU() 方法定义一个名为 testAbySphU 的资源,代码如下。
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();
            }
        }
    }
}
  1. 重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”,结果如下。
    提醒您,服务访问成功------testA:8401

  2. 访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。

Sentinel SphU

图8:Sentinel 通过 SphU 定义资源
通过 SphO 手动定义资源
Sentinel 还提供了一个名为 SphO 的类,它包含了 if-else 风格的 API,能帮助我们手动定义资源。通过这种方式定义的资源,发生了限流之后会返回 false,此时我们可以根据返回值,进行限流之后的逻辑处理。

下面我们就通过一个实例,来演示下如何通过 SphO 定义资源。

  1. 在 spring-cloud-alibaba-sentinel-service-8401 下的 SentinelFlowLimitController 中,新增一个 testBbySphO() 方法定义一个名为 testBbySphO 的资源,代码如下。
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 服务被限流";
            //流控逻辑处理 - 结束
        }
    }
}
  1. 重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testB”,结果如下。
    提醒您,服务访问成功------testB:8401

  2. 访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。

Sentinel SphO
图9:Sentinel 通过 SphO 定义资源
注解方式定义资源(推荐)
除了以上两种方式外,我们还可以通过 Sentinel 提供的 @SentinelResource 注解定义资源。

下面我们就通过一个实例,来演示下如何通过 @SentinelResource 注解定义资源。

  1. 将 spring-cloud-alibaba-sentinel-service-8401 中 SentinelFlowLimitController 类中增加以下代码。
@GetMapping("/testC")
@SentinelResource(value = "testCbyAnnotation") //通过注解定义资源
public String testC() {
    log.info("提醒您,服务访问成功------testC:" + serverPort);
    return "提醒您,服务访问成功------testC:" + serverPort;
}
  1. 重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testC”,结果如下。
    服务访问成功------testC:8401

  2. 访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。

Sentinel 注解方式定义资源

图10:Sentinel 注解方式定义资源
Sentinel 流量控制
任何系统处理请求的能力都是有限的,但任意时间内到达系统的请求量往往是随机且不可控的,如果在某一个瞬时时刻请求量急剧增,那么系统就很有可能被瞬时的流量高峰冲垮。为了避免此类情况发生,我们都需要根据系统的处理能力对请求流量进行控制,这就是我们常说的“流量控制”,简称“流控”。

Sentinel 作为一种轻量级高可用流量控制组件,流量控制是它最主要的工作之一。

我们可以针对资源定义流控规则,Sentinel 会根据这些规则对流量相关的各项指标进行监控。当这些指标当达到或超过流控规则规定的阈值时,Sentinel 会对请求的流量进行限制(即“限流”),以避免系统被瞬时的流量高峰冲垮,保障系统的高可用性。

一条流量规则主要由下表中的属性组成,我们可以通过组合这些属性来实现不同的限流效果。

属性说明默认值
资源名的作用对象。-
阈值值。-
阈值类型流控阈值的类型,包括 QPS 或并发线程数。QPS
针对来源流控针对的调用来源。default,表示不区分调用来源
流控模式调用关系限流策略,包括直接、链路和关联。直接
流控效果流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流。直接拒绝

注:QPS 表示并发请求数,换句话说就是,每秒钟最多通过的请求数。

同一个资源可以创建多条流控规则,Sentinel 会遍历这些规则,直到有规则触发限流或者所有规则遍历完毕为止。

Sentinel 触发限流时,资源会抛出 BlockException 异常,此时我们可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

注意:这里我们主要讲解 Sentinel 流控规则的定义与使用,至于详细的流控规则配置,请参考 Sentinel 官方流控文档。

通过 Sentinel 控制台定义流控规则
我们可以通过 Sentinel 控制台,直接对资源定义流控规则,操作步骤如下。

  1. 在 spring-cloud-alibaba-sentinel-service-8401 的 SentinelFlowLimitController 中新增一个名为 testD 的服务方法,代码如下。
/**
 * 通过 Sentinel 控制台定义流控规则
 *
 * @return
 */
@GetMapping("/testD")
public String testD() {
    log.info("提醒您,服务访问成功------testD:" + serverPort);
    return "提醒您,服务访问成功------testD:" + serverPort;
}
  1. 重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testD”,结果如下。
    提醒您,服务访问成功------testD:8401

  2. 使用浏览器访问“http://localhost:8080”,登陆 Sentinel 控制台主页,点击 sentinel-sevice 下的“簇点链路”,结果如下图。

Sentinel 定义流控规则

图11:Sentinel 控制台定义流控规则

  1. 点击“/testD”右侧的“+流控”按钮,在弹出的“新增流控规则”窗口中定义流控规则,如下图。

Sentinel 流控规则配置

图12:Sentinel 控制台定义流控规则

  1. 点击下方的“新增”按钮,跳转到“流控规则”列表,如下图。

Sentinel 流控规则列表

图13:Sentinel 控制台流控规则列表

  1. 快速连续(频率大于每秒钟 2 次)访问“http://localhost:8401/testD”,结果如下。
    Blocked by Sentinel (flow limiting)

若页面中出现以上信息,则说明该服务已被限流,但这种提示是 Sentinel 系统自动生成的,用户体验不好。

  1. 在服务代码中使用 @SentinelResource 注解定义资源名称,并在 blockHandler 属性指定一个限流函数,自定义服务限流信息,代码如下。
/**
 * 通过 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 属性来指定自定义的限流处理方法,若不指定,则会跳转到错误页(用户体验不好)。

  1. 重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问 Sentinel 控制台主页,点击 sentinel-sevice 下的“簇点链路”,结果如下图。

Sentinel控制台定义流控规则

图14:Sentinel 控制台-簇点链路

  1. 点击资源“testD-resource”右侧的“+流控”按钮,并在弹出的“新增流控规则”窗口中为这个资源定义流控规则,流控规则内容为 :QPS 的阈值为 2(即每秒最多通过 2 个请求),如下图。

Sentinel 控制台新增流控规则

图15:Sentinel 控制台新增流控规则

  1. 快速连续(频率大于每秒钟 2 次)访问“http://localhost:8401/testD”,结果如下。
提醒您,TestD服务访问失败! 您已被限流,请稍后重试

通过代码定义流控规则
我们还可以在服务代码中,调用 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、匀速排队),不支持按调用关系限流直接拒绝

下面我们就通过一个简单的实例,来演示下如何通过代码定义流控规则,步骤如下。

  1. 在 spring-cloud-alibaba-sentinel-service-8401 的 SentinelFlowLimitController 中添加一个 initFlowRules() 方法,为名为 testD-resource 的资源定义流控规则:每秒最多只能通过 2 个请求,即 QPS 的阈值为 2。
/**
 * 通过代码定义流量控制规则
 */
private static void initFlowRules() {
    List<FlowRule> rules = new ArrayList<>();
    //定义一个限流规则对象
    FlowRule rule = new FlowRule();
    //资源名称
    rule.setResource("testD-resource");
     //限流阈值的类型
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // 设置 QPS 的阈值为 2
    rule.setCount(2);
    rules.add(rule);
    //定义限流规则
    FlowRuleManager.loadRules(rules);
}
  1. 在 testD() 方法中调用 initFlowRules() 方法,初始化流控规则,代码如下。
@GetMapping("/testD")
@SentinelResource(value = "testD-resource", blockHandler = "blockHandlerTestD") //通过注解定义资源
public String testD() {
    initFlowRules(); //调用初始化流控规则的方法
    log.info("提醒您,服务访问成功------testD:" + serverPort);
    return "提醒您,服务访问成功------testD:" + serverPort;
}
  1. 重启 spring-cloud-alibaba-sentinel-service-8401,并使用浏览器访问“http://localhost:8401/testD”,结果如下。
    提醒您,服务访问成功------testD:8401

  2. 快速连续(频率大于每秒钟 2 次)访问“http://localhost:8401/testD”,结果如下。
    提醒您,TestD服务访问失败! 您已被限流,请稍后重试

  3. 打开命令行窗口,执行以下命令查看资源的实时统计信息。
    curl http://localhost:8719/cnode?id=testD-resource

  4. 控制台输出内容如下。

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

实时统计信息各列名说明如下:

  • thread: 代表当前处理该资源的并发数;
  • pass: 代表一秒内到来到的请求;
  • blocked: 代表一秒内被流量控制的请求数量;
  • success: 代表一秒内成功处理完的请求;
  • total: 代表到一秒内到来的请求以及被阻止的请求总和;
  • RT: 代表一秒内该资源的平均响应时间;
  • 1m-pass: 则是一分钟内到来的请求;
  • 1m-block: 则是一分钟内被阻止的请求;
  • 1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;
  • exception: 则是一秒内业务本身异常的总和。

熔断降级规则

除了流量控制以外,对调用链路中不稳定资源的熔断降级,也是保障服务高可用的重要措施之一。

在分布式微服务架构中,一个系统往往由多个服务组成,不同服务之间相互调用,组成复杂的调用链路。如果链路上的某一个服务出现故障,那么故障就会沿着调用链路在系统中蔓延,最终导致整个系统瘫痪。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 熔断状态转换

图16:Sentinel 熔断状态转换

Sentinel 熔断降级中共涉及 3 种状态,如下表。

状态说明触发条件
熔断关闭状态(CLOSED)处于关闭状态时,请求可以正常调用资源。 满足以下任意条件,Sentinel 熔断器进入熔断关闭状态:全部请求访问成功。单位统计时长(statIntervalMs)内请求数目小于设置的最小请求数目。未达到熔断标准,例如服务超时比例、异常数、异常比例未达到阈值。处于探测恢复状态时,下一个请求访问成功。
熔断开启状态(OPEN)处于熔断开启状态时,熔断器会一定的时间(规定的熔断时长)内,暂时切断所有请求对该资源的调用,并调用相应的降级逻辑使请求快速失败避免系统崩溃。 满足以下任意条件,Sentinel 熔断器进入熔断开启状态:单位统计时长内请求数目大于设置的最小请求数目,且已达到熔断标准,例如请求超时比例、异常数、异常比例达到阈值。处于探测恢复状态时,下一个请求访问失败。
探测恢复状态(HALF-OPEN)处于探测恢复状态时,Sentinel 熔断器会允许一个请求调用资源。则若接下来的一个请求成功完成(没有错误)则结束熔断,熔断器进入熔断关闭(CLOSED)状态;否则会再次被熔断,熔断器进入熔断开启(OPEN)状态。 在熔断开启一段时间(降级窗口时间或熔断时长,单位为 s)后,Sentinel 熔断器自动会进入探测恢复状态。

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 实现熔断降级的步骤如下:

  1. 在项目中,使用 @SentinelResource 注解的 fallback 属性可以为资源指定熔断降级逻辑(方法)。
  2. 通过 Sentinel 控制台或代码定义熔断规则,包括熔断策略、最小请求数、阈值、熔断时长以及统计时长等。
  3. 若单位统计时长(statIntervalMs)内,请求数目大于设置的最小请求数目且达到熔断标准(例如请求超时比例、异常数、异常比例达到阈值),Sentinel 熔断器进入熔断开启状态(OPEN)。
  4. 处于熔断开启状态时, @SentinelResource 注解的 fallback 属性指定的降级逻辑会临时充当主业务逻辑,而原来的主逻辑则暂时不可用。当有请求访问该资源时,会直接调用降级逻辑使请求快速失败,而不会调用原来的主业务逻辑。
  5. 在经过一段时间(在熔断规则中设置的熔断时长)后,熔断器会进入探测恢复状态(HALF-OPEN),此时 Sentinel 会允许一个请求对原来的主业务逻辑进行调用,并监控其调用结果。
  6. 若请求调用成功,则熔断器进入熔断关闭状态(CLOSED ),服务原来的主业务逻辑恢复,否则重新进入熔断开启状态(OPEN)。

通过 Sentinel 控制台定义熔断降级规则

我们可以通过 Sentinel 控制台直接对资源定义熔断降级规则。

下面我们通过一个实例,来演示如何通过 Sentinel 控制台,对资源定义降级规则。

  1. 在 MySQL 的 demobang_jdbc 数据库中执行以下 SQL,准备测试数据。
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');
  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-provider-mysql-8003 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-provider-mysql-8003</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-provider-mysql-8003</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>net.demo.c</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</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>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-provider-mysql-8003 的类路径下,创建一个配置文件 application.yml,配置如下。
server:
  port: 8003 #端口
spring:
  application:
    name: spring-cloud-alibaba-provider-mysql
  cloud:
    nacos:
      discovery:
        server-addr: localhost:1111
  ######################### 数据库连接 #################################
  datasource:
    username: root        #数据库登陆用户名
    password: root        #数据库登陆密码
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_db2       #数据库url
    driver-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.entity
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
  1. 在 net.demo.c.entity 包下,创建一个名为 Dept 的实体类,代码如下。
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;
}
  1. 在 net.demo.c.entity 包下,创建一个名为 CommonResult 的 Java 类,代码如下。
package net.demo.c.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;
    public CommonResult(Integer code, String message) {
        this(code, message, null);
    }
}
  1. 在 net.demo.c.mapper 包下,创建一个名为 DeptMapper 的接口,代码如下。
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<Dept> GetAll();
}
  1. 在 spring-cloud-alibaba-provider-mysql-8003 的 /resources/mybatis/mapper/ 目录下,创建一个名为 DeptMapper.xml 的 MyBatis 映射文件,配置内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.demo.c.mapper.DeptMapper">
    <resultMap id="BaseResultMap" type="net.demo.c.entity.Dept">
        <id column="dept_no" jdbcType="INTEGER" property="deptNo"/>
        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
        <result column="db_source" jdbcType="VARCHAR" property="dbSource"/>
    </resultMap>
    <sql id="Base_Column_List">
        dept_no
        , dept_name, db_source
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from dept
        where dept_no = #{deptNo,jdbcType=INTEGER}
    </select>
    <select id="GetAll" resultType="net.demo.c.entity.Dept">
        select *
        from dept;
    </select>
</mapper>
  1. 在 net.demo.c.service 包下,创建一个名为 DeptService 的接口,代码如下。
package net.demo.c.service;
import net.demo.c.entity.Dept;
import java.util.List;
public interface DeptService {
    Dept get(Integer deptNo);
    List<Dept> selectAll();
}
  1. 在 net.demo.c.service.impl 包下,创建 DeptService 接口的实现类 DeptServiceImpl,代码如下。
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 {
    @Autowired
    private DeptMapper deptMapper;
    @Override
    public Dept get(Integer deptNo) {
        return deptMapper.selectByPrimaryKey(deptNo);
    }
    @Override
    public List<Dept> selectAll() {
        return deptMapper.GetAll();
    }
}
  1. 在 net.demo.c.controller 包下,创建一个名为 DeptController 的 Contorller 类,代码如下。
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 {
    @Autowired
    private DeptService deptService;
    @Value("${server.port}")
    private String serverPort;
    @RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)
    public CommonResult<Dept> 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<Dept> result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, dept);
        return result;
    }
    @RequestMapping(value = "/dept/list", method = RequestMethod.GET)
    public CommonResult<List<Dept>> list() {
        log.info("端口:" + serverPort + "\t+ dept/list/");
        List<Dept> depts = deptService.selectAll();
        CommonResult<List<Dept>> result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, depts);
        return result;
    }
}
  1. spring-cloud-alibaba-provider-mysql-8003 的主启动类代码如下。
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);
    }
}
  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-consumer-mysql-8803 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-consumer-mysql-8803</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-consumer-mysql-8803</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--SpringCloud ailibaba 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>
        <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>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>net.demo.c</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-consumer-mysql-8803 的类路径下,创建一个配置文件 application.yml,配置内容如下。
server:
  port: 8803
spring:
  application:
    name: spring-cloud-alibaba-consumer-mysql-feign
  cloud:
    nacos:
      discovery:
        server-addr: localhost:1111
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
# 以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码 服务提供者的服务名
service-url:
  nacos-user-service: http://spring-cloud-alibaba-provider-mysql #消费者要方位的微服务名称
# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true
  1. 在 net.demo.c.service 包下,创建一个名为 DeptFeignService 的接口,代码如下。
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<Dept> get(@PathVariable("id") int id);
    @RequestMapping(value = "/dept/list", method = RequestMethod.GET)
    public CommonResult<List<Dept>> list();
}
  1. 在 net.demo.c.controller 包下,创建一个名为 DeptFeignController 的 Controller,代码如下。
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 {
    @Resource
    DeptFeignService deptFeignService;
    @RequestMapping(value = "consumer/feign/dept/get/{id}", method = RequestMethod.GET)
    @SentinelResource(value = "fallback", fallback = "handlerFallback")
    public CommonResult<Dept> get(@PathVariable("id") int id) {
        monitor();
        System.out.println("--------->>>>主业务逻辑");
        CommonResult<Dept> 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<Dept>> 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 属性时,需要注意以下事项:

  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
  • fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  1. spring-cloud-alibaba-consumer-mysql-8803 的主启动类代码如下。
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);
    }
}
  1. 依次启动 spring-cloud-alibaba-provider-mysql-8003 和 spring-cloud-alibaba-consumer-mysql-8803,使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/3”,结果如下。
{"code":200,"message":"from mysql,serverPort:  8003","data":{"deptNo":3,"deptName":"财务部","dbSource":"spring_cloud_db2"}}
  1. 使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/7”,结果如下。
{"code":444,"message":"提醒您,服务被降级!异常信息为:NullPointerException,该ID没有对应记录,空指针异常","data":{"deptNo":7,"deptName":"null","dbSource":"null"}}
  1. 控制台输出如下。
--------->>>>主业务逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务熔断降级逻辑
  1. 使用浏览器访问 Sentinel 控制台,在“簇点链路”列表中,点击 fallback 资源的 “+熔断”按钮,如下图。

Sentinel 熔断

图17:Sentinel 熔断降级

  1. 在“新增熔断规则”的窗口中,为名为 “fallback”的资源定义以下熔断规则,如下图。
    在这里插入图片描述

图18:Sentinel 控制台定义熔断规则

在上图中,熔断规则各属性说明如下:

  • 异常数为 1;
  • 统计时长为 1000 ms(即 1s);
  • 最小请求数为 2;
  • 熔断时长为 5 秒;

我们为 fallback 资源定义的熔断规则为:当 1 秒钟内请求数大于 2 个,且请求异常数大于 1 时,服务被熔断,熔断的时长为 5 秒钟。

  1. 使用浏览器连续访问“http://localhost:8803/consumer/feign/dept/get/7”,访问频率大于每秒 2 个请求,结果如下。
{"code":444,"message":"提醒您,服务被降级!异常信息为:null","data":{"deptNo":7,"deptName":"null","dbSource":"null"}}
  1. 控制台输出如下。
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
CLOSED -> OPEN at 2021-11-17 14:06:47, 发送请求次数=2.00

从控制台输出可以看出,在 14点 06 分 47 秒时,熔断器从熔断关闭状态(CLOSED)切换到熔断开启状态(OPEN)。

  1. 在熔断开启开启状态下,使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/4”,结果页面输出如下。
    {“code”:444,“message”:“提醒您,服务被降级!异常信息为:null”,“data”:{“deptNo”:4,“deptName”:“null”,“dbSource”:“null”}}

  2. 控制台输出如下。

--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
CLOSED -> OPEN at 2021-11-17 14:09:19, 发送请求次数=2.00
--------->>>>服务降级逻辑
--------->>>>服务降级逻辑
--------->>>>服务降级逻辑

从控制台输出可知,当熔断器处于熔断开启状态时,所有的请求都直接交给降级逻辑处理。

  1. 继续使用浏览器访问“http://localhost:8803/consumer/feign/dept/get/4”,结果页面输出如下。
{"code":200,"message":"from mysql,serverPort:  8003","data":{"deptNo":4,"deptName":"市场部","dbSource":"spring_cloud_db2"}}
  1. 控制台输出如下。
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
--------->>>>主业务逻辑
--------->>>>主业务逻辑,抛出空指针异常
--------->>>>服务降级逻辑
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<DegradeRule> rules) {
    try {
        currentProperty.updateValue(rules);
    } catch (Throwable var2) {
        RecordLog.error("[DegradeRuleManager] Unexpected error when loading degrade rules", var2);
    }
}

DegradeRule 类可以用来定义一条熔断规则,它包含多个与熔断规则相关的属性,如下表。
在这里插入图片描述

下面我们就通过一个实例,演示下如何通过代码定义熔断规则,步骤如下。

  1. 在 spring-cloud-alibaba-consumer-mysql-8803 的 DeptFeignController 中,添加一个名为 initDegradeRule 的方法,代码如下。
/**
 * 初始化熔断策略
 */
private static void initDegradeRule() {
    List<DegradeRule> 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);
}
  1. 在 DeptFeignController 的 get() 方法中调用 initDegradeRule() 方法初始化熔断规则,代码如下。
@RequestMapping(value = "consumer/feign/dept/get/{id}", method = RequestMethod.GET)
@SentinelResource(value = "fallback", fallback = "handlerFallback")
public CommonResult<Dept> get(@PathVariable("id") int id) {
    initDegradeRule();
    monitor();
    System.out.println("--------->>>>主业务逻辑");
    CommonResult<Dept> 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;
}
  1. 重启 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”}}

  2. 使用浏览器访问 Sentinel 控制主页,点击“熔断规则”查看熔断规则列表,结果如下图。

Sentinel 代码定义的熔断规则

图19:Sentinel 代码定义熔断规则

从上图我们看到,通过代码也能够为资源定义熔断规则。

Spring Cloud Alibaba Seata

随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。

在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。

例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:

  1. 调用订单服务创建订单数据
  2. 调用库存服务扣减库存
  3. 调用账户服务扣减账户金额
  4. 最后调用订单服务修改订单状态

为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。

Seata,读音 [si:t], 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。

Seata 的发展历程

阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。

阿里巴巴对于分布式事务问题先后发布了以下解决方案:

  • 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 的形式集成在微服务中运行,其整体工作流程如下图。

在这里插入图片描述

图1:Sentinel 的工作流程

Seata 的整体工作流程如下:
1.TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
2.XID 通过服务的调用链传递到其他服务;
3.RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
4.TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
5.TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。

Seata AT 模式

Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。

在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。

AT 模式的前提

任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:

  • 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
  • 应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。

此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 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;

AT 模式的工作机制

Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。

假设某数据库中存在一张名为 webset 的表,表结构如下。

列名类型主键
idbigint(20)
namevarchar(255)
urlvarchar(255)

在某次分支事务中,我们需要在 webset 表中执行以下操作。

update webset set url = 'c.demo.net' where name = 'XX测试';

一阶段

Seata AT 模式一阶段的工作流程如下图所示。

Seata AT 模式一阶段

图2:Seata AT 模式一阶段

Seata AT 模式一阶段工作流程如下。

  1. 获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(webset)、判断条件(where name = ‘XX测试’)等相关信息。

  2. 查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。

select id,name,url from webset where  name='XX测试';

执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。

idnameurl
1XX测试xxx.net
  1. 执行业务 SQL(update webset set url = ‘c.demo.net’ where name = ‘XX测试’;),将这条记录的 url 修改为 c.demo.net。

  2. 查询后镜像:根据“前镜像数据”的主键(id : 1),生成“后镜像查询语句”。

select id,name,url from webset where  id= 1;

执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。

idnameurl
1XX测试c.xxx.net
  1. 插入回滚日志:将前后镜像数据和业务 SQL 的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中,示例回滚日志如下。
{
  "@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"
                    }
                  ]
                ]
              }
            ]
          ]
        }
      }
    ]
  ]
}
  1. 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM 会向 TC 注册分支事务,并针对主键 id 为 1 的记录生成行锁。

以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。

  1. 本地事务提交:将业务数据的更新和前面生成的 UNDO_LOG 一并提交。

  2. 上报执行结果:将本地事务提交的结果上报给 TC。

二阶段:提交

当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。

若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。

二阶段:回滚

若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。

  1. 查找 UNDO_LOG 记录:通过 XID 和分支事务 ID(Branch ID) 查找所有的 UNDO_LOG 记录。

  2. 数据校验:将 UNDO_LOG 中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。

  3. 生成回滚语句:根据 UNDO_LOG 中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:

update webset set url= 'demo.net' where id = 1;
  1. 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。

  2. 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

下载 Seata 服务器

  1. 使用浏览器访问“https://github.com/seata/seata/releases/tag/v1.4.2”,在 Seata Server 下载页面分别下载“seata-server-1.4.2.zip”,如下图。

Seata 服务器下载

图3:Seata 服务器下载页面

  1. 解压 seata-server-1.4.2.zip,其目录结构如下图。

Seata Server 目录结构

图4:Seata Server 目录结构

Seata Server 目录中包含以下子目录:

  • bin:用于存放 Seata Server 可执行命令。
  • conf:用于存放 Seata Server 的配置文件。
  • lib:用于存放 Seata Server 依赖的各种 Jar 包。
  • logs:用于存放 Seata Server 的日志。

Seata 配置中心

所谓“配置中心”,就像是一个“大衣柜”,内部存放着各种各样的配置文件,我们可以根据自己的需要从其中获取指定的配置文件,加载到对应的客户端中。

Seata 支持多种配置中心:

  • nacos
  • consul
  • apollo
  • etcd
  • zookeeper
  • file (读本地文件,包含 conf、properties、yml 等配置文件)

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 中,将配置方式(config.type)修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,示例配置如下。

config {
  # Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心
  #配置方式修改为 nacos
  type = "nacos"
  nacos {
    #修改为使用的 nacos 服务器地址
    serverAddr = "127.0.0.1:1111"
    #配置中心的命名空间
    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:1111 # Nacos 配置中心的地址
      group : "SEATA_GROUP"  #分组
      namespace: ""
      username: "nacos"   #Nacos 配置中心的用于名
      password: "nacos"  #Nacos 配置中心的密码

上传配置到 Nacos 配置中心

在完成了 Seata 服务端和客户端的相关配置后,接下来,我们就可以将配置上传的 Nacos 配置中心了,操作步骤如下。

  1. 我们需要获取一个名为 config.txt 的文本文件,该文件包含了 Seata 配置的所有参数明细。

我们可以通过 Seata Server 源码/script/config-center 目录中获取 config.txt,然后根据自己需要修改其中的配置,如下图。

在这里插入图片描述

图5:config.txt

  1. 在 /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 1111 -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 Nacos配置中心

图6:Seata Nacos 配置中心

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:1111"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
}

Seata Client 配置注册中心

我们可以在 Seata Client 的 application.yml 中,对 Nacos 注册中心进行配置,示例配置如下。

seata:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:1111 # Nacos 注册中心的地址
      group : "SEATA_GROUP" #分组
      namespace: ""
      username: "nacos"   #Nacos 注册中心的用户名
      password: "nacos"   # Nacos 注册中心的密码
验证 Nacos 注册中心

在以上所有步骤完成后,先启动 Nacos Server 再启动 Seata Server,登录 Nacos 控制台查看服务列表,结果如下图。
在这里插入图片描述
从图 9 可以看出,seata-server 服务已经注册到了 Nacos 注册中心。

Seata 事务分组

事务分组是 Seata 提供的一种 TC(Seata Server) 服务查找机制。

Seata 通过事务分组获取 TC 服务,流程如下:
在应用中配置事务分组。

  1. 应用通过配置中心去查找配置:service.vgroupMapping.{事务分组},该配置的值就是 TC 集群的名称。
  2. 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。
  3. 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。

下面我们以 Nacos 服务注册中心为例,介绍 Seata 事务的使用。

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 = "c.demo.net"         # 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:
     alibaba:
      seata:
        tx-service-group: service-order-group  #事务分组名
seata:
  registry:
    type: nacos   #从 Nacos 获取 TC 服务
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos   #使用 Nacos 作为配置中心
    nacos:
      server-addr: 127.0.0.1:1111
      name

在以上配置中,我们通过 spring.cloud.alibaba.seata.tx-service-group 来配置 Seata 事务分组名,其默认取值为:服务名-fescar-service-group。

上传配置到 Nacos

将以下配置上传到 Nacos 配置中心。
service.vgroupMapping.service-order-group=c.demo.net

在以上配置中,
service-order-group:为事务分组的名称;
c.demo.net:为 TC 集群的名称。

获取事务分组

  1. 先启动 Nacos,再启动 Seata Server,最后再启动 Seata Client。

  2. 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 集群的名称:“c.demo.net”。

查找 TC 服务

根据 TC 集群名、Nacos 注册中心的地址(server-addr)以及命名空间(namespace),在 Nacos 注册中心找到真实的 TC 服务列表。

小结

通过事务分组获取服务名,共需要以下 3 步:
1.服务启动时,从配置文件中获取服务分组的名称;
2.从配置中心,通过事务分组名获取 TC 集群名;
3.根据 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;
  1. 修改 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:1111"
    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:1111"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    #不使用 seataServer.properties 方式配置
    #dataId = "seataServer.properties"
  }
}
  1. 将 Seata 配置上传到 Nacos
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
  1. 在 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
  1. 当 Git 命令窗口出现以下执行日志时,则说明配置上传成功。
=========================================================================
Complete initialization parameters,  total-count:87 ,  failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.
  1. 使用浏览器访问 Nacos 服务器主页,查看 Nacos 配置列表,如下图。

Seata 配置上传到Nacos

图8:Seata 配置上传到 Nacos

注意:在使用 Git 命令将配置上传到 Nacos 前,应该先确保 Nacos 服务器已启动。

  1. 启动 Seata Server
    双击 Seata Server 端 bin 目录下的启动脚本 seata-server.bat ,启动 Seata Server。

Seata Server 启动脚本

图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

接下来,我们就以电商系统为例,来演示下业务系统是如何集成 Seata 的。

在电商系统中,用户下单购买一件商品,需要以下 3 个服务提供支持:

  • Order(订单服务):创建和修改订单。
  • Storage(库存服务):对指定的商品扣除仓库库存。
  • Account(账户服务) :从用户帐户中扣除商品金额。

这三个微服务分别使用三个不同的数据库,架构图如下所示。

Seata 案例架构

图10:电商系统架构图

当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:

  1. 调用 Order 服务,创建一条订单数据,订单状态为“未完成”;
  2. 调用 Storage 服务,扣减商品库存;
  3. 调用 Account 服务,从用户账户中扣除商品金额;
  4. 调用 Order 服务,将订单状态修改为“已完成”。

创建订单(Order)服务

  1. 在 MySQL 数据库中,新建一个名为 seata-order 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_order(订单表)和 undo_log(回滚日志表)。
-- ----------------------------
-- 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 ;
  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-seata-order-8005 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-seata-order-8005</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-seata-order-8005</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <seata.version>1.4.2</seata.version>
    </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>net.demo.c</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </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>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-seata-order-8005 的类路径(/resources 目录)下,创建一个配置文件 bootstrap.yml,配置内容如下。
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  1. 在 spring-cloud-alibaba-seata-order-8005 的类路径(/resources 目录)下,创建一个配置文件 application.yml,配置内容如下。

spring:
  application:
    name: spring-cloud-alibaba-seata-order-8005 #服务名
  #数据源配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver #数据库驱动
    name: defaultDataSource
    url: 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: 8719
    alibaba:
      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-group
  service:
    grouplist:
      #Seata 服务器地址
      seata-server: 127.0.0.1:8091
  # Seata 的注册方式为 nacos
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  # Seata 的配置中心为 nacos
  config:
    type: nacos
    nacos:
      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.entity
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
  1. 在 net.demo.c.entity 包下,创建一个名为 Order 的实体类,代码如下。
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;
    }
}
  1. 在 net.demo.c.mapper 包下,创建一个名为 OrderMapper 的 Mapper 接口,代码如下。
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 修改订单状态,从零改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);
    Order selectByPrimaryKey(Long id);
    int updateByPrimaryKeySelective(Order record);
    int updateByPrimaryKey(Order record);
}
  1. 在 /resouces/mybatis/mapper 目录下,创建一个名为 OrderMapper.xml 的 MyBatis 映射文件,代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.demo.c.mapper.OrderMapper">
    <resultMap id="BaseResultMap" type="net.demo.c.entity.Order">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="product_id" jdbcType="BIGINT" property="productId"/>
        <result column="count" jdbcType="INTEGER" property="count"/>
        <result column="money" jdbcType="DECIMAL" property="money"/>
        <result column="status" jdbcType="INTEGER" property="status"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , user_id, product_id, count, money, status
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_order
        where id = #{id,jdbcType=BIGINT}
    </select>
    <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
        delete
        from t_order
        where id = #{id,jdbcType=BIGINT}
    </delete>
    <insert id="insert" parameterType="net.demo.c.entity.Order">
        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>
    <insert id="insertSelective" parameterType="net.demo.c.entity.Order">
        insert into t_order
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">
                id,
            </if>
            <if test="userId != null">
                user_id,
            </if>
            <if test="productId != null">
                product_id,
            </if>
            <if test="count != null">
                count,
            </if>
            <if test="money != null">
                money,
            </if>
            <if test="status != null">
                status,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">
                #{id,jdbcType=BIGINT},
            </if>
            <if test="userId != null">
                #{userId,jdbcType=BIGINT},
            </if>
            <if test="productId != null">
                #{productId,jdbcType=BIGINT},
            </if>
            <if test="count != null">
                #{count,jdbcType=INTEGER},
            </if>
            <if test="money != null">
                #{money,jdbcType=DECIMAL},
            </if>
            <if test="status != null">
                #{status,jdbcType=INTEGER},
            </if>
        </trim>
    </insert>
    <insert id="create" parameterType="net.demo.c.entity.Order">
        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})
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="net.demo.c.entity.Order">
        update t_order
        <set>
            <if test="userId != null">
                user_id = #{userId,jdbcType=BIGINT},
            </if>
            <if test="productId != null">
                product_id = #{productId,jdbcType=BIGINT},
            </if>
            <if test="count != null">
                count = #{count,jdbcType=INTEGER},
            </if>
            <if test="money != null">
                money = #{money,jdbcType=DECIMAL},
            </if>
            <if test="status != null">
                status = #{status,jdbcType=INTEGER},
            </if>
        </set>
        where id = #{id,jdbcType=BIGINT}
    </update>
    <update id="updateByPrimaryKey" parameterType="net.demo.c.entity.Order">
        update t_order
        set 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>
    <update id="update">
        update t_order
        set status = 1
        where user_id = #{userId}
          and status = #{status};
    </update>
</mapper>
  1. 在 net.demo.c.service 包下,创建一个名为 OrderService 的 Service 接口,代码如下。
package net.demo.c.service;
import net.demo.c.entity.Order;
public interface OrderService {
    /**
     * 创建订单数据
     * @param order
     */
    CommonResult create(Order order);
}
  1. 在 net.demo.c.service 包下,创建一个名为 StorageService 的接口,代码如下。
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);
}
  1. 在 net.demo.c.service 包下,创建一个名为 AccountService 的接口,代码如下。
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);
}
  1. 在 net.demo.c.service.impl 包下,创建 OrderService 接口的实现类 OrderServiceImpl,代码如下。
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 {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageService storageService;
    @Resource
    private 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, "订单创建成功");
    }
}
  1. 在 net.demo.c.controller 包下,创建一个名为 OrderController 的 Controller,代码如下。
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 {
    @Autowired
    private 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);
    }
}
  1. 主启动类代码如下。
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);
    }
}

搭建库存(Storage)服务

  1. 在 MySQL 数据库中,新建一个名为 seata-storage 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_storage(库存表)和 undo_log(回滚日志表)。
-- ----------------------------
-- 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';
  1. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-seata-storage-8006 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-seata-storage-8006</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-seata-storage-8006</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <seata.version>1.4.2</seata.version>
    </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>net.demo.c</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </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>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在 spring-cloud-alibaba-seata-storage-8006 的类路径(/resources 目录)下,创建一个配置文件 bootstrap.yml,配置内容如下。
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  1. 在 spring-cloud-alibaba-seata-storage-8006 的类路径(/resources 目录)下,创建一个配置文件 application.yml,配置内容如下。
spring:
  application:
    name: spring-cloud-alibaba-seata-storage-8006
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    name: defaultDataSource
    url: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=UTC
    username: root
    password: root
   
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:1111
        namespace: public
        username:
        password:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 8719
    alibaba:
      seata:
        tx-service-group: service-storage-group
server:
  port: 8006
seata:
  application-id: ${spring.application.name}
  tx-service-group: service-storage-group
  service:
    grouplist:
      seata-server: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos
    nacos:
      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.entity
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
  1. 在 net.demo.c.entity 包下,创建一个名为 Storage 的实体类,代码如下。
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;
    }
}
  1. 在 net.demo.c.mapper 包下,创建一个名为 StorageMapper 的接口,代码如下。
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);
}
  1. 在 /resouces/mybatis/mapper 目录下,创建一个名为 StorageMapper.xml 的 MyBatis 映射文件,代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.demo.c.mapper.StorageMapper">
    <resultMap id="BaseResultMap" type="net.demo.c.entity.Storage">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="product_id" jdbcType="BIGINT" property="productId"/>
        <result column="total" jdbcType="INTEGER" property="total"/>
        <result column="used" jdbcType="INTEGER" property="used"/>
        <result column="residue" jdbcType="INTEGER" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , product_id, total, used, residue
    </sql>
    <update id="decrease" parameterType="net.demo.c.entity.Storage">
        update t_storage
        <set>
            <if test="total != null">
                total = #{total,jdbcType=INTEGER},
            </if>
            <if test="used != null">
                used = #{used,jdbcType=INTEGER},
            </if>
            <if test="residue != null">
                residue = #{residue,jdbcType=INTEGER},
            </if>
        </set>
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
    <select id="selectByProductId" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_storage
        where product_id = #{productId,jdbcType=BIGINT}
    </select>
</mapper>
  1. 在 net.demo.c.service 包下,创建一个名为 StorageService 的 Service 接口,代码如下。
package net.demo.c.service;
public interface StorageService {
    int decrease(Long productId, Integer count);
}
  1. 在 net.demo.c.service.impl 包下,创建 StorageService 的实现类 StorageServiceImpl,代码如下。
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 {
    @Resource
    StorageMapper storageMapper;
    @Override
    public 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("库存不足,扣减库存失败!");
        }
    }
}
  1. 在 net.demo.c.controller 包下,创建一个名为 StorageController 的 Controller 类,代码如下。
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 {
    @Resource
    private 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;
    }
}
  1. 主启动类的代码如下。
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);
    }
}

搭建账户(Account)服务

  1. 在 MySQL 数据库中,新建一个名为 seata-account 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_account(账户表)和 undo_log(回滚日志表)。
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;
  1. 在主启动类 spring-cloud-alibaba-seata-account-8007 下,创建一个名为 spring-cloud-alibaba-seata-account-8007 的 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>net.demo.c</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>spring-cloud-alibaba-demo</artifactId>
    </parent>
    <groupId>net.demo.c</groupId>
    <artifactId>spring-cloud-alibaba-seata-account-8007</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-seata-account-8007</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <seata.version>1.4.2</seata.version>
    </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>net.demo.c</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </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>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project> 
  1. 在 spring-cloud-alibaba-seata-account-8007 的类路径(/resources 目录)下,创建一个配置文件 bootstrap.yml,配置内容如下。
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  1. 在 spring-cloud-alibaba-seata-account-8007 的类路径(/resources 目录)下,创建一个配置文件 application.yml,配置内容如下。
spring:
  application:
    name: spring-cloud-alibaba-seata-account-8007
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    name: defaultDataSource
    url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:1111
        namespace: public
        username:
        password:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 8719
    alibaba:
      seata:
        tx-service-group: service-account-group
server:
  port: 8007
seata:
  application-id: ${spring.application.name}
  tx-service-group: service-account-group
  service:
    grouplist:
      seata-server: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos
    nacos:
      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.entity
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
  1. 在 net.demo.c.entity 包下,创建一个名为 Account 的实体类,代码如下。
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;
    }
}
  1. 在 net.demo.c.mapper 包下,创建一个名为 AccountMapper 的接口,代码如下。
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);
}
  1. 在 /resouces/mybatis/mapper 目录下,创建一个名为 AccountMapper.xml 的 MyBatis 映射文件,代码如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.demo.c.mapper.AccountMapper">
    <resultMap id="BaseResultMap" type="net.demo.c.entity.Account">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="total" jdbcType="DECIMAL" property="total"/>
        <result column="used" jdbcType="DECIMAL" property="used"/>
        <result column="residue" jdbcType="DECIMAL" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , user_id, total, used, residue
    </sql>
    <select id="selectByUserId" resultType="net.demo.c.entity.Account">
        select
        <include refid="Base_Column_List"/>
        from t_account
        where user_id = #{userId,jdbcType=BIGINT}
    </select>
    <update id="decrease">
        UPDATE t_account
        SET residue = residue - #{money},
            used    = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>
  1. 在 net.demo.c.service 包下,创建一个名为 AccountService 的接口,代码如下。
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);
}
  1. 在 net.demo.c.service.impl 包下,创建 AccountService 的实现类 AccountServiceImpl,代码如下。
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 {
    @Resource
    AccountMapper accountMapper;
    @Override
    public 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("账户余额不足!");
        }
    }
}
  1. 主启动类代码如下。
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);
    }
}
  1. 依次启动 Nacos Server(集群)和 Seata Server(集群),最后启动 spring-cloud-alibaba-seata-order-8005,当控制台出现以下日志时,说明该服务已经成功连接上 Seata Server(TC)。
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]
  1. 启动 spring-cloud-alibaba-seata-storage-8006,当控制台出现以下日志时,说明该服务已经成功连接上 Seata Server(TC)。
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]
  1. 启动 spring-cloud-alibaba-seata-account-8007,当控制台出现以下日志时,说明该服务已经成功连接上 Seata Server(TC)。
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]
  1. 使用浏览器访问“http://localhost:8005/order/createByAnnotation/1/2/20”,结果如下。
{"code":200,"message":"订单创建成功","data":null}
  1. 执行以下 SQL 语句,查询 seata_order 数据库中的 t_order 表。
SELECT * FROM seata_order.t_order;

结果如下。

iduser_idproduct_idcountmoneystatus
112201

从上表可以看出,已经创建一条订单数据,且订单状态(status)已修改为“已完成”。

  1. 执行以下 SQL 语句,查询 seata_storage 数据库中的 t_storage 表。
SELECT * FROM seata_storage.t_storage;

结果如下。

idproduct_idtotal usedresidue
11100298

从上表可以看出,商品库存已经扣减 2 件,仓库中商品储量从 100 件减少到了 98 件。

  1. 执行以下 SQL 语句,查询 seata_account 数据库中的 t_account 表,
SELECT * FROM seata_account.t_account;

结果如下。

iduser_idtotalusedresidue
11100020980

从上表可以看出,账户余额已经扣减,金额从 1000 元减少到了 980 元。

  1. 使用浏览器访问“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)]

注:在本次请求中,用户购买 10 件商品(商品 ID 为 1),商品总价为 1000 元,此时用户账户余额 为 980 元,因此账户服务会抛出“账户余额不足!”的运行时异常。

  1. 再次查询 t_order、t_storage 和 t_account 表,结果如下。

图11:分布式事务问题

从上图可以看出:

  • t_order(订单表):订单服务生成了一条订单数据(id 为 2),但订单状态(status)为未完成(0);
  • t_storage(库存表):商品库存已扣减;
  • t_account(账户表):账户金额不足,账户(Account)服务出现异常,进而导致账户金额并未扣减。

@GlobalTransactional 注解

在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。

当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。

@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:

  • 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
  • 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。

接下来,我们就使用 @GlobalTransactional 注解对业务系统进行改造,步骤如下。

  1. 在 spring-cloud-alibaba-seata-order-8005 的 net.biacheng.c 包下的 OrderController 中,添加一个名为 createByAnnotation 的方法,代码如下。
/**
 * 使用 @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 注解。

  1. 将数据恢复到浏览器访问“http://localhost:8005/order/createByAnnotation/1/2/20”之后。

  2. 重启订单(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 元,因此账户服务会抛出运行时异常,异常信息为“账户余额不足!”。

  1. spring-cloud-alibaba-seata-order-8005 控制台输出如下(标红部分为回滚日志)。
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
  1. spring-cloud-alibaba-seata-storage-8006 控制台输出如下。
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
  1. spring-cloud-alibaba-seata-account-8007 控制台输出如下。
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: 账户余额不足!
  1. 到 MySQL 数据库中,再次查询 t_order、t_storage 和 t_account 表,结果如下。

数据库表

图12:Seata 事务回滚

从图 12 可以看出,这次并没有出现分布式事务造成的数据不一致问题。

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
`spring-cloud-alibaba-dependencies`是一个Maven BOM(Bill of Materials),包含了Spring Cloud Alibaba的所有依赖版本。通过引入`spring-cloud-alibaba-dependencies`,可以简化Spring Cloud Alibaba项目的依赖管理。它提供了以下依赖: - `spring-cloud-alibaba-dependencies`:Spring Cloud Alibaba版本管理器 - `spring-cloud-starter-alibaba-nacos-discovery`:Nacos服务发现 - `spring-cloud-starter-alibaba-nacos-config`:Nacos配置中心 - `spring-cloud-starter-alibaba-sentinel`:Sentinel限流熔断 - `spring-cloud-starter-alibaba-seata`:Seata分布式事务 - `spring-cloud-starter-alibaba-rocketmq`:RocketMQ消息队列 - `spring-cloud-starter-alibaba-dubbo`:Dubbo远程调用 使用`spring-cloud-alibaba-dependencies`需要在`pom.xml`文件中引入如下配置: ```xml <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` 引入该依赖后,其他Spring Cloud Alibaba组件的依赖版本就可以省略了。例如,使用Nacos作为服务发现和配置中心,只需要引入以下依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> ``` Spring Cloud Alibaba会自动使用`spring-cloud-alibaba-dependencies`中定义的版本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值