跳至主要內容

SpringCloud2024

sixkey大约 65 分钟后端SpringCloud微服务

SpringCloud2024

Cloud之Consul

Consul作为新一代服务注册中心和分布式配置,官网:https://www.consul.io

下载安装

https://developer.hashicorp.com/consul/downloads

下载完成后只有一个consul.exe文件,全路径下查看版本号信息

screenshot-1712817095119
screenshot-1712817095119

开发者模式启动

consul agent -dev

访问路径

http://localhost:8500
screenshot-1712817225728

服务注册与发现

依赖

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

yml配置

####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

主启动类开启服务发现

@SpringBootApplication
@MapperScan("org.cloud.mapper")
@EnableDiscoveryClient
public class Main8001
{
    public static void main(String[] args)
    {
        SpringApplication.run(Main8001.class,args);
    }
}

启动服务并查看consul控制台

screenshot-1712818936175
screenshot-1712818936175

经过以上步骤就一个将一个服务注册到consul中进行管理

服务配置与刷新

面临问题

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。

作用:一次修改,处处生效

需求:

通用全局配置信息,直接注册进consul服务器,从consul获取 既然从consul获取自然要遵守consul的配置规则要求

实践

操作对象:修改cloud-payment-service8001服务

1、导入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2、新增配置文件bootstrap.yml

bootstrap.yml是什么呢?:

applicaiton.yml是用户级的资源配置项

bootstrap.yml是系统级的,优先级更加高

Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap contextApplication Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。

application.yml文件改为bootstrap.yml,这是很关键的或者两者共存

因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml

配置内容

将原来application.yml中consul的一些配置抽到bootstrap.yml中

spring:
  application:
    name: cloud-payment-service
    ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
      config:
        profile-separator: '-' # default value is ",",we update '-'
        format: YAML

原来的application.yml文件内容

server:
  port: 8001

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: '020708'

# Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: org.cloud.entity
  configuration:
    map-underscore-to-camel-case: true

抽离后的文件

server:
  port: 8001

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: '020708'
  profiles:
    active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: org.cloud.entity
  configuration:
    map-underscore-to-camel-case: true
3、consul服务器k/v配置填写
screenshot-1712821630127

为什么要配置k/v配置填写呢,因为规则是人家定的,要遵守规矩配置,步骤如下

1、创建config文件夹,以/结尾

2、config文件夹下分别创建3个文件夹,分别为默认环境:cloud-payment-service;开发环境:cloud-payment-service-dev;生产环境:cloud-payment-service-prod这几个文件后缀名分别对应着application.yml中的profiles: active: dev这个参数。

3、上述3个文件夹下分别创建data内容,data不再是文件夹

screenshot-1712822054433screenshot-1712822233783

data中的内容

cloud:
  info: welcometoclouddefaultstudy
4、测试:测试是否可以读取到配置好的内容
@Value("${server.port}")
    private String port;

    @GetMapping(value = "/pay/get/info")
    private String getInfoByConsul(@Value("${cloud.info}") String cloudInfo)
    {
        return "cloudInfo: "+cloudInfo+"\t"+"port: "+port;
    }

可以看到我们已经读取到了内容

screenshot-1712822912151
5、问题引出

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

screenshot-1712823307970

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

screenshot-1712823608668

解决方案:

启动类添加刷新注解

@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@RefreshScope // 动态刷新
public class Main8001
{
    public static void main(String[] args)
    {
        SpringApplication.run(Main8001.class,args);
    }
}

修改bootstrap.yml配置文件:将官方默认的55s修改为1s

spring:
  application:
    name: cloud-payment-service
    ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
      config:
        profile-separator: '-' # default value is ",",we update '-'
        format: YAML
        #实际生产不建议修改
        watch:
          wait-time: 1
6、持久化问题

以上所有的配置,如果关闭了consul,所有的配置文件立即丢失,需要做持久化配置处理

解决:解决持久化并加入到后台启动

1、在consul同级目录下新建MyData文件夹存放配置文件进行持久化,再放一个脚本实现后台开机自启

screenshot-1712825026182
screenshot-1712825026182

2、右键管理员方式启动

启动错误:第一次启动会提示启动失败,服务已经被启动,这时候以管理员身份执行cmd,然后sc delete

consul,最后再执行.bat文件即可

screenshot-1712825935157
screenshot-1712825935157

3、后续consul的配置文件就保存在mydata文件夹中了,启动就有

Cloud之OpenFeign

是什么?

OpenFeign是一个声明式的Web服务客户端

能干嘛?

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持SentineI和它的Fallback
  • 支持SrinCloudLoadBalancer的负载均衡
  • 支持HTTP请求和响应的压缩

怎么玩?

1、接口 + 注解

架构说明图

screenshot-1712827051217
screenshot-1712827051217

2、流程步骤

①、建Model

cloud-consumer-openfeign-order

②、改pom

<dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--SpringCloud consul discovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包 -->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--hutool-all-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

写yml

server:
  port: 7999

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}

主启动类开启注解@EnableFeignClients

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OpenFeignConsumerApp
{
    public static void main( String[] args )
    {
        SpringApplication.run(OpenFeignConsumerApp.class,args);
    }
}

apis包下包含所有向外暴露的接口api

screenshot-1712828110614
/**
 * 支付模块相关接口
 * 注意:一定要添加注解@FeignClient,否则无法远程调用服务
 * name的值为提供服务类的应用名称
 */
@FeignClient(name = "cloud-payment-service")
public interface PaymentFeignService {
    @GetMapping("/pay/add")
    ResultData addOrder(PayDto payDTO);

    @GetMapping("/pay/query/{orderNo}")
    ResultData queryOrder(@PathVariable("orderNo") String orderNo);
}

controller调用服务

@Slf4j
@RestController
@RequestMapping("/feign")
@RequiredArgsConstructor
public class PaymentFeignController {

    private final PaymentFeignService paymentFeignService;

    /**
     * 一般情况下,通过浏览器的地址栏输入url,发送的只能是get请求
     * 我们底层调用的是post方法,模拟消费者发送get请求,客户端消费者
     * 参数可以不添加@RequestBody
     * @param payDTO
     * @return
     */
    @GetMapping("/pay/add")
    public ResultData addOrder(PayDto payDTO){
        paymentFeignService.addOrder(payDTO);
        return ResultData.success();
    }

    /**
     * @param orderNo
     * @return
     */
    @GetMapping("/pay/query/{orderNo}")
    public ResultData queryOrder(@PathVariable("orderNo") String orderNo){
        ResultData result = paymentFeignService.queryOrder(orderNo);
        return ResultData.success(result.getData());
    }
}

3、小总结

重点说明:OpenFeign自带负载均衡功能,默认是轮询

screenshot-1712829454383
screenshot-1712829454383

高级特性

1、超时控制

默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。

为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好

yml文件中开启配置:

connectTimeout 连接超时时间

readTimeout 请求处理超时时间

screenshot-1712841720091
screenshot-1712841720091

如何控制呢?

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

screenshot-1712841837845

开始配置

  • 全局配置

    • 修改yml配置文件,添加以下内容即可

      spring:
        cloud:
          openfeign:
            client:
              config:
                default:
                  #连接超时时间
                   connectTimeout: 3000
                  #读取超时时间
                   readTimeout: 3000
      
  • 指定特定服务配置

    • 可以指定为某个服务提供者的超时时间控制,如支付模块

      spring:
        application:
          name: cloud-consumer-openfeign-order
        ####Spring Cloud Consul for Service Discovery
        cloud:
          consul:
            host: localhost
            port: 8500
            discovery:
              prefer-ip-address: true #优先使用服务ip进行注册
                      service-name: ${spring.application.name}
          openfeign:
            client:
              config:
               #default:
                  #connectTimeout: 4000 #连接超时时间
                  #readTimeout: 4000 #读取超时时间
               cloud-payment-service:
                  connectTimeout: 8000 #连接超时时间
                  readTimeout: 8000 #读取超时时间
      

特别说明:当全局和指定配置都配置时,优先遵循指定配置超时时间控制

2、重试机制

有默认值,但是被官方关闭了

screenshot-1712843153808

开启重试机制

新增配置类:FeignConfig并修改Retryer配置

/**
 * ClassName: FeignConfig
 * Package: org.cloud.config
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/11 - 21:48
 * @Version: v1.0
 */

/**
 * 重试机制配置
 */
@Configuration
public class FeignConfig
{
    @Bean
    public Retryer myRetryer()
    {
        //return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的

        //最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
        return new Retryer.Default(100,1,3);
    }
}

3、默认HttpClient替换

是什么?

OpenFeign中http client如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,

由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。

替换之前,默认使用的

screenshot-1712843536484
screenshot-1712843536484

替换为Apache HttpClient 5

修改cloud-consumer-openfeign-order

导入依赖

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.1</version>
</dependency>

写yaml

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 4000
            #读取超时时间
            readTimeout: 4000
      #  Apache HttpClient5 配置开启
      httpclient:
        hc5:
          enabled: true

替换之后使用的HttpClient

screenshot-1712845643443
screenshot-1712845643443

4、请求响应 / 压缩

官网说明

screenshot-1712845895260
screenshot-1712845895260

是什么?

对请求和响应进行GZIP压缩

Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

通过下面的两个参数设置,就能开启请求与相应的压缩功能:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.response.enabled=true

细粒度化设置

对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,

只有超过这个大小的请求才会进行压缩:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型

spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

修改cloud-consumer-openfeign-order

写yml

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 4000
            #读取超时时间
            readTimeout: 4000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true

5、日志打印功能

日志级别

NONE:默认的,不显示任何日志;

BASIC:仅记录请求方法、URL、响应状态码及执行时间;

HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;

FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

配置日志类bean

/**
 * 重试机制配置
 */
@Configuration
public class FeignConfig
{
    @Bean
    public Retryer myRetryer()
    {
        return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的

        //最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
//        return new Retryer.Default(100,1,3);
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

配置yaml开启日志功能

公式(三段):logging.level + 含有@FeignClient注解的完整带包名的接口名+debug

# feign日志以什么级别监控哪个接口
logging:
  level:
    org:
      cloud:
        apis:
          PaymentFeignService: debug

Cloud之Sleuth

Micrometer+ ZipKin分布式链路追踪

Sleuth简介

Sleuth也进入维护模式,改头换面

79c97cc6aff9917c10c0a3da1377bba6.png

Sleuth未来替换方案:Micrometer Tracing

分布式链路追踪概述

为什么会出现,解决什么问题?

在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。

36cd6bb9f5830c5fcd7a3b3e62d97a74.png

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

4a163bbc8bad7ff0436caf2243077ca6.png

解决方案

在分布式与微服务场景下,我们需要解决如下问题:

  • 在大规模分布式与微服务集群下,如何实时观测系统的整体调用链路情况。

  • 在大规模分布式与微服务集群下,如何快速发现并定位到问题。

  • 在大规模分布式与微服务集群下,如何尽可能精确的判断故障对系统的影响范围与影响程度。

  • 在大规模分布式与微服务集群下,如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。

  • 在大规模分布式与微服务集群下,如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。

  • 在大规模分布式与微服务集群下,如何尽可能精确的分析系统的存储瓶颈与容量规划。

上述问题就是我们的落地议题答案:

分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

Micrometer+ ZipKin

新一代分布式链路追踪:Micrometer

Micrometer官网:

https://docs.micrometer.io/

ZipKin?

**SpringCloudSleuth(micrometer)**提供了一套完整的分布式链路追踪(DistributedTracing) 解决方案且兼容支持了zipkin展现。

ceaa9c90d9ae1c271476c23e4485b926.png

小总结

将一次分布式请求还原成调用链路,进行日志记录和能监控,并将一次分布式请求的调用情况集中Web展示。

其他方案

行业内比较成熟的其它分布式链路追踪技术解决方案。

8384af3fbbfe629ee189ff9e19de62c7.png

链路追踪原理

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

44ada9e6374eda9fd621f27e7d7ee659.png
44ada9e6374eda9fd621f27e7d7ee659.png

追踪过程

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

6eb8be09660292926bd334d0a763f382.png
6eb8be09660292926bd334d0a763f382.png
1第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
2第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。
3第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
4第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。
5第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
6第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
7第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。
8通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。

ZipKin

官网:https://zipkin.io/

ZipKin概述

Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。

为什么会出现?单有Sleuth(Micrometer)行不行?

d74be7f816a350309277bfbf0a4fdd87.png

说明:

当没有配置 Sleuth 链路追踪的时候,INFO 信息里面是 [passjava-question,,,],后面跟着三个空字符串。

当配置了 Sleuth 链路追踪的时候,追踪到的信息是 [passjavaquestion,504a5360ca906016,e55ff064b3941956,false] ,第一个是 Trace ID,第二个是 Span ID。只有日志没有图,观看不方便,不美观,so,引入图形化Zipkin链路监控让你好看,O(∩_∩)O

下载 + 安装 + 运行

下载地址:Quickstart · OpenZipkinopen in new window

运行命令:java -jar zipkin-server-3.0.0-rc0-exec.jar

8f2030735a193953bc1eec7682991459.png

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

96870918665ec0fe81190e0426780ea2.png

案例实战

分工说明

  • Micometer
    • 数据采样
  • ZipKin
    • 图像展示

步骤

1、总体父工程

新增pom依赖

 <properties>
<-- 分布式链路追踪依赖 --></--> 
        <micrometer-tracing.version>1.2.0</micrometer-tracing.version>
        <micrometer-observation.version>1.12.0</micrometer-observation.version>
        <feign-micrometer.version>12.5</feign-micrometer.version>
        <zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
</properties>

<dependencyManagement>
        <dependencies>
<!--micrometer-tracing-bom导入链路追踪版本中心  1-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing-bom</artifactId>
                <version>${micrometer-tracing.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--micrometer-tracing指标追踪  2-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing</artifactId>
                <version>${micrometer-tracing.version}</version>
            </dependency>
            <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing-bridge-brave</artifactId>
                <version>${micrometer-tracing.version}</version>
            </dependency>
            <!--micrometer-observation 4-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-observation</artifactId>
                <version>${micrometer-observation.version}</version>
            </dependency>
            <!--feign-micrometer 5-->
            <dependency>
                <groupId>io.github.openfeign</groupId>
                <artifactId>feign-micrometer</artifactId>
                <version>${feign-micrometer.version}</version>
            </dependency>
            <!--zipkin-reporter-brave 6-->
            <dependency>
                <groupId>io.zipkin.reporter2</groupId>
                <artifactId>zipkin-reporter-brave</artifactId>
                <version>${zipkin-reporter-brave.version}</version>
            </dependency>
            
            </dependencies>
    </dependencyManagement>

依赖说明

由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:

1micrometer-tracing-bom导入链路追踪版本中心,体系化说明
2micrometer-tracing指标追踪
3micrometer-tracing-bridge-brave一个Micrometer模块,用于与分布式跟踪工具 Brave 集成,以收集应用程序的分布式跟踪数据。Brave是一个开源的分布式跟踪工具,它可以帮助用户在分布式系统中跟踪请求的流转,它使用一种称为"跟踪上下文"的机制,将请求的跟踪信息存储在请求的头部,然后将请求传递给下一个服务。在整个请求链中,Brave会将每个服务处理请求的时间和其他信息存储到跟踪数据中,以便用户可以了解整个请求的路径和性能。
4micrometer-observation一个基于度量库 Micrometer的观测模块,用于收集应用程序的度量数据。
5feign-micrometer一个Feign HTTP客户端的Micrometer模块,用于收集客户端请求的度量数据。
6zipkin-reporter-brave一个用于将 Brave 跟踪数据报告到Zipkin 跟踪系统的库。

补充包:spring-boot-starter-actuator SpringBoot框架的一个模块用于监视和管理应用程序

2、服务提供者8001

cloud-provider-payment8001

改pom

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.cloud</groupId>
        <artifactId>cloud2024</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-provider-payment8001</artifactId>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <!--micrometer-tracing指标追踪  1-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing</artifactId>
        </dependency>
        <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing-bridge-brave</artifactId>
        </dependency>
        <!--micrometer-observation 3-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-observation</artifactId>
        </dependency>
        <!--feign-micrometer 4-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-micrometer</artifactId>
        </dependency>
        <!--zipkin-reporter-brave 5-->
        <dependency>
            <groupId>io.zipkin.reporter2</groupId>
            <artifactId>zipkin-reporter-brave</artifactId>
        </dependency>
        <!--自定义-->
        <dependency>
            <groupId>org.cloud</groupId>
            <artifactId>cloud-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--consul-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
        <!--mybatis-plus和springboot整合-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <!--Mysql数据库驱动8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

写yaml

server:
  port: 8001

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud_24?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: '020708'
  profiles:
    active: dev

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: org.cloud.entity
  configuration:
    map-underscore-to-camel-case: true
    
# ========================zipkin===================
management:
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans
  tracing:
    sampling:
      probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。

新建业务类

/**
 * ClassName: PayMicrometerController
 * Package: org.cloud.controller
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/16 - 13:53
 * @Version: v1.0
 */
@RestController
public class PayMicrometerController
{
    /**
     * Micrometer(Sleuth)进行链路监控的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/micrometer/{id}")
    public String myMicrometer(@PathVariable("id") Integer id)
    {
        return "Hello, 欢迎到来myMicrometer inputId:  "+id+" \t    服务返回:" + IdUtil.simpleUUID();
    }
}

3、远程调用openfeign

@FeignClient(name = "cloud-payment-service")
public interface PaymentFeignService {
    /**
     * Micrometer(Sleuth)进行链路监控的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/micrometer/{id}")
    public String myMicrometer(@PathVariable("id") Integer id);
}

4、服务消费者

cloud-consumer-payment80

改pom

<!--micrometer-tracing指标追踪  1-->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing</artifactId>
    </dependency>
    <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-brave</artifactId>
    </dependency>
    <!--micrometer-observation 3-->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation</artifactId>
    </dependency>
    <!--feign-micrometer 4-->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-micrometer</artifactId>
    </dependency>
    <!--zipkin-reporter-brave 5-->
    <dependency>
        <groupId>io.zipkin.reporter2</groupId>
        <artifactId>zipkin-reporter-brave</artifactId>
    </dependency>

写yml

# zipkin图形展现地址和采样率设置
management:
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans
  tracing:
    sampling:
      probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。

新建业务类

@RestController
@Slf4j
public class OrderMicrometerController
{
    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping(value = "/feign/micrometer/{id}")
    public String myMicrometer(@PathVariable("id") Integer id)
    {
        return payFeignApi.myMicrometer(id);
    }
}

测试

本次案例默认已启动ZipKin

将微服务模块注册进consul注册中心

测试地址:http://localhost/feign/micrometer/1

查看ZipKin控制台

  • 会出现以下界面

    3148792fe52d9c35185a02dfaec129d9.png
    3148792fe52d9c35185a02dfaec129d9.png
  • 点击【SHOW】按钮查看

    9a5cd7d428c56b474d6e4fc520f1e3ad.png
  • 查看依赖关系

56522c011f845d5c6088f88de6966e3f.png

Cloud之Gateway

官网总述

三大核心

  • Route:路由是构建网关的基本模块,它由ID,目标URL,一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):参考的是Java8的java.util.function.predicate 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
screenshot-1712888226159

总结:

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

工作流程

screenshot-1712888789839

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。

在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;

在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑

路由转发 + 断言判断 + 执行过滤器链

创建gateway项目

创建

  • 建Model
cloud-gateway9527
  • 改Pom
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.cloud</groupId>
        <artifactId>cloud2024</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-gateway9527</artifactId>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 写Yaml
server:
  port: 9527

spring:
  application:
    name: cloud-gateway9527 #以微服务注册进consul或nacos服务列表内
  cloud:
    consul: #配置consul地址
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}
  • 主启动类
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class GatewayApp
{
    public static void main(String[] args)
    {
        SpringApplication.run(GatewayApp.class,args);
    }
}

添加网关前测试

在cloud-payment-service服务提供者中新增测试网关的两个接口

/**
     * 支付新增接口
     * @param payDto
     * @return
     */
    @PostMapping("/gateway/add")
    @Operation(summary = "新增",description = "支付接口")
    public ResultData gateWayAddPayment(@RequestBody PayDto payDto){
        payService.addPayment(payDto);
        return ResultData.success();
    }

    /**
     * 查询支付接口
     * @param orderNo
     * @return
     */
    @GetMapping("/gateway/query/{orderNo}")
    @Operation(summary = "查询",description = "查询支付接口")
    public ResultData gateWayQueryOrder(@PathVariable("orderNo") String orderNo){
        List<PayDto> payDto = payService.queryOrder(orderNo);
        return ResultData.success(payDto);
    }

进行测试,查看是否可以请求到数据

测试地址:localhost:8001/pay/gateway/query/9798464swxopen in new window

自测通过,不过这是没有添加网关之前的请求,因为本身已经知道了内部请求地址,所以能通过很正常。

添加网关后测试

  • cloud-gateway9527的yaml配置修改
server:
  port: 9527

spring:
  application:
    name: cloud-gateway9527 #以微服务注册进consul或nacos服务列表内
  cloud:
    consul: #配置consul地址
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}
    gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001                #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/query/**              # 断言,路径相匹配的进行路由


        - id: pay_routh2 #pay_routh2                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001                #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/add/**              # 断言,路径相匹配的进行路由

进行测试,查看是否可以请求到数据

测试地址:localhost:9527/pay/gateway/query/9798464swxopen in new window

自测通过,说明网关配置没有问题。

引出问题

看看网关9527的Yml配置,映射写死问题

screenshot-1712890841286

高级特性

Route动态获取服务URL

以微服务名称,动态获取服务URL,解决URL地址写死问题

  • 修改9527网关的yaml配置
    gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#          uri: http://localhost:8001                #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service            #以微服务名称动态获取URL
          predicates:
            - Path=/pay/gateway/query/**              # 断言,路径相匹配的进行路由


        - id: pay_routh2 #pay_routh2                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#          uri: http://localhost:8001                #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service            #以微服务名称动态获取URL
          predicates:
            - Path=/pay/gateway/add/**              # 断言,路径相匹配的进行路由

Predicate断言配置

是什么?
screenshot-1712898946998
screenshot-1712898946998
架构概述
screenshot-1713000128791
screenshot-1713000128791

内置断言

screenshot-1712899012868
screenshot-1712899012868
常用内置断言

配置语法总体概述:有两种

  • Shortcut Configuration(常用)

    screenshot-1712899333697

  • Fully Expanded Arguments

screenshot-1712899517397

示例

常用断言api包括以上内置的十几种,这里以After断言举例,其他的配置举一反三

作用:配置了After断言意味着只有在这个时间段后才可以通过访问请求

screenshot-1712899698076

我们的问题是:上述这个After好懂,这个时间串串???对应的格式如何获得?

获得ZonedDateTime

/**
 * @auther wenxueshi
 * @create 2024-4-12 17:37
 */
public class ZonedDateTimeDemo
{
    public static void main(String[] args)
    {
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
              System.out.println(zbj);
    }
}

Yaml配置

gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001                #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service          #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/get/**              # 断言,路径相匹配的进行路由
            - After=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai]
自定义断言

官方内置的断言不够用时,可以看视频解决自定义断言。

总结

Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

Filter过滤器链

常用的内置过滤器讲解

1、请求头(RequestHeader)相关组

具体讲解,其他的举一反三

网关服务添加yaml

 filters:
            - AddRequestHeader=X-Request-atguigu1,atguiguValue1  # 请求头kv,若一头含有多参则重写一行设置
            - AddRequestHeader=X-Request-atguigu2,atguiguValue2

2、请求参数(RequestParameter)相关组

自定义过滤器
全局过滤器

案例分析:统计接口调用耗时情况,通过自定义全局过滤器解决上述要求

  • 步骤

    • 新建类MyGlobalFilter并实现GlobalFilter和Ordered两个接口

    • code

      package org.cloud.config;
      
      /**
       * ClassName: MyGlobalFilter
       * Package: org.cloud.config
       * Description:
       *
       * @Author: @weixueshi
       * @Create: 2024/4/12 - 15:50
       * @Version: v1.0
       */
      
      import jakarta.validation.constraints.Size;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.cloud.gateway.filter.GatewayFilterChain;
      import org.springframework.cloud.gateway.filter.GlobalFilter;
      import org.springframework.core.Ordered;
      import org.springframework.web.server.ServerWebExchange;
      import reactor.core.publisher.Mono;
      
      /**
       * 自定义全局网关过滤器
       */
      @Slf4j
      @Component
      public class MyGlobalFilter implements GlobalFilter, Ordered {
      
          /**
           * 开始访问时间
           */
          private static final String BEGIN_VISIT_TIME = "begin_visit_time";
          /**
           *第2版,各种统计
           * @param exchange
           * @param chain
           * @return
           */
          @Override
          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
              //先记录下访问接口的开始时间
              exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
      
              return chain.filter(exchange).then(Mono.fromRunnable(()->{
                  Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
                  if (beginVisitTime != null){
                      log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
                      log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
                      log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
                      log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
                      log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
                      log.info("我是美丽分割线: ###################################################");
                      System.out.println();
                  }
              }));
          }
      
          /**
           * 过滤器执行优先级,数字越小优先级越大
           * @return
           */
          @Override
          public int getOrder() {
              return 0;
          }
      }
      
单一内置过滤器

是什么?

**区别:**以上的全局过滤器是对网关中的所有请求路径下的所有方法都有效,单一内置过滤器是针对某一个请求路径下的所有方法有效。范围比全局过滤器要小。

案例分析:我们内置的这个过滤器的作用是进行请求时需要携带一个statusTest参数并且值为任意才可以通过过滤器

定义步骤

1、新建类名xxx需要以GatewayFilterFactory结尾并继承AbstractGatewayFilterFact0ry类

@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
}

2、新建xxxGatewayFilterFactory.Config内部类

public static class Config {
        @Setter @Getter
        private String status;
}

3、重写apply方法

@Override
    public GatewayFilter apply(MyGatewayFilterFactory.Config config)
    {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpRequest request =  exchange.getRequest();
                System.out.println("进入自定义网关过滤器MyGatewayFilterFactory,status===="+config.getStatus());
                if(request.getQueryParams().containsKey("statusTest")) {
                    return chain.filter(exchange);
                }else {
                    exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                    return exchange.getResponse().setComplete();
                }
            }
        };
    }

4、重写shortcutFieldOrder

@Override
public List<String> shortcutFieldOrder() {
     return Arrays.asList("status");
}

5、空参构造方法,内部调用super()

public MyGatewayFilterFactory() {
        super(MyGatewayFilterFactory.Config.class);
    }

如何在网关配置

  • 先看出厂配置

    screenshot-1712911648995
    screenshot-1712911648995
  • 自己定制My配置

    screenshot-1712911965900
    screenshot-1712911965900

测试

由上图可知,我们只配置了id为pay_routh3下的请求路径

请求路径:

CloudAlibaba之Nacos

架构介绍

screenshot-1713000202053

Nacos简介

是什么?

官网:Nacos官网 | Nacos 官方社区 | Nacos 下载 | Nacosopen in new window一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台

一句话:Nacos就是注册中心 + 配置中心的组合

screenshot-1712985300729
screenshot-1712985300729

各种注册中心比较

screenshot-1712985348127
screenshot-1712985348127

下载安装

官网下载安装nacos,解压安装包,运行bin目录下控制台执行命令startup.cmd -m standalone开启nacos,运行成功后直接访问http://localhost:8848/nacos

作为注册中心

将其他服务注册进nacos

添加依赖

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

添加yaml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        enabled: true

主启动类添加

@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaOpenFeignApp
{
    public static void main( String[] args )
    {
        SpringApplication.run(AlibabaOpenFeignApp.class,args);
    }
}

与gateway集成

注意:虽然nacos与consul作用相似,但是集成gateway和openfeign时有区别

nacos与gateway网关集成

添加依赖注意一定要添加loadbalancer依赖,否则配置网关时无法通过服务名称去请求,会一直报503错误,与consul与gateway集成就不需要添加loadbalancer依赖。

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

完整yaml配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: pay_routh1
          uri: lb://cloud-alibaba-openfeign-consumer
          predicates:
            - Path=/feign/pay/query/**
nacos与openfeign网关集成

添加依赖注意一定要添加loadbalancer依赖,否则配置网关时无法通过服务名称去请求,会一直报503错误,与consul与gateway集成就不需要添加loadbalancer依赖。

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

完整yaml配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        enabled: true
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 20000
            #读取超时时间
            readTimeout: 20000
      httpclient:
        hc5:
          enabled: true

CloudAlibaba之Sentinel

简介

官网:

介绍 · alibaba/Sentinel Wiki (github.com)open in new window

home | Sentinel (sentinelguard.io)open in new window

是什么?

轻量级的流量控制,熔断降级的java库

去哪下

Releases · alibaba/Sentinel (github.com)open in new window

能干嘛?

从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性

怎么玩?

  • 服务雪崩

    • 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

      所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

      screenshot-1713061519690
  • 服务降级

    • 服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。

      例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。

  • 服务熔断

    • 在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。

      服务熔断一般情况下会有三种状态:闭合、开启和半熔断;

      闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。

      开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。

      半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。

  • 服务限流

    • 服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。

      限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。

  • 服务隔离

    • 有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。

      互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。

  • 服务超时

    • 整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。

      形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。

安装Sentinel

Sentinel由两部分组成

screenshot-1713061797249
screenshot-1713061797249

安装步骤

下载

Releases · alibaba/Sentinel (github.com)open in new window

下载到本地sentinel-dashboard-1.8.7.jar

运行命令

  • 前提
    • java环境ok
    • 8080端口不能被占用
  • 命令
    • java -jar sentinel-dashboard-1.8.7.jar

访问sentinel管理界面

http://localhost:8080

账号密码均为sentinel

微服务8401整合案例

新建微服务8401

新建微服务cloudAlibaba-sentinel-service8401:意味着8401微服务将被哨兵纳入监控

改pom

<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--SpringCloud alibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--nacos-discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包 -->
        <dependency>
            <groupId>org.cloud</groupId>
            <artifactId>cloud-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

写yaml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
        port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口

主启动类

@SpringBootApplication
@EnableDiscoveryClient
public class SentinelApplication
{
    public static void main( String[] args )
    {
        SpringApplication.run(SentinelApplication.class,args);
    }
}

业务类

@RestController
public class FlowLimitController
{

    @GetMapping("/testA")
    public String testA()
    {
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB()
    {
        return "------testB";
    }
}

启动8401服务查看Sentinel控制台

  • 空空如也,啥也没有

  • Sentinel采用的懒加载说明

    • 注意:想使用Sentinel对某个接口进行限流和降级等操作,一定要先访问下接口,使Sentinel检测到相应的接口
    • 执行一次访问即可
      • http://localhost:8401/testA
      • http://localhost:8401/testB
    • 效果
      • 24f33ae69639b534d40c60c71fb2b893.png
        24f33ae69639b534d40c60c71fb2b893.png

流控规则

基本介绍

Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。参数见最下方:

5292d2a5f0a45558ef01bb784f543cc7.png
1资源名资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。
2针对来源具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。
3阈值类型QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。
4单机阈值与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。
5是否集群选中则表示集群环境,不选中则表示非集群环境。

流控模式

  • 直接

    • 默认的流控模式,当接口达到限流条件时,直接开启限流功能。
    • 配置及说明
      • 表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误
      • 7c5da6eb7dba38a890cf1cc186fcde48.png
    • 测试
      • 快速点击访问http://localhost:8401/testA
      • 结果:Blocked by Sentinel (flow limiting)
      • 思考?直接调用默认报错信息,技术方面OK,是否应该有我们自己的后续处理?类似有个fallback的兜底方法?
  • 关联

    • 是什么?
      • 当关联的资源达到阈值时,就限流自己 当与A关联的资源B达到阀值后,就限流A自己 B惹事,A挂了
    • 配置A
      • 设置效果:当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名,B惹事,A挂了
      • b09e0ba5f7f209bcbbac995eb63c0799.png
      • 测试:大批量线程高并发访问B,导致A失效,访问http://localhost:8401/testA,结果:Blocked by Sentinel (flow limiting)
  • 链路

    • 是什么?

      • 来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施, 比如C请求来访问就限流,D请求来访问就是OK
    • 修改微服务cloudAlibaba-sentinel-service8401

      • 改yaml

        server:
          port: 8401
        
        spring:
          application:
            name: cloudalibaba-sentinel-service #8401微服务提供者后续将会被纳入阿里巴巴sentinel监管
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848         #Nacos服务注册中心地址
            sentinel:
              transport:
                dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
                        port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
                    web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
        
      • 业务类

        新建FlowLimitService

        @Service
        public class FlowLimitService
        {
            @SentinelResource(value = "common")
            public void common()
            {
                System.out.println("------FlowLimitService come in");
            }
        }
        
        

        修改FlowLimitController

        @RestController
        public class FlowLimitController
        {
        
            @GetMapping("/testA")
            public String testA()
            {
                return "------testA";
            }
        
            @GetMapping("/testB")
            public String testB()
            {
                return "------testB";
            }
        
            /**流控-链路演示demo
             * C和D两个请求都访问flowLimitService.common()方法,阈值到达后对C限流,对D不管
             */
            @Resource private FlowLimitService flowLimitService;
        
            @GetMapping("/testC")
            public String testC()
            {
                flowLimitService.common();
                return "------testC";
            }
            @GetMapping("/testD")
            public String testD()
            {
                flowLimitService.common();
                return "------testD";
            }
        }
        
    • Sentinel配置

      • 说明:C和D两个请求都访问flowLimitService.common()方法,对C限流,对D不管
      • 789248abb289074dca4626f6e224d960.png
    • 测试

      • 访问http://localhost:8401/testC
      • C链路
        • 85283c11ae29a78d997fc2af1db9f559.png
      • D链路ok

流控效果

339ff2f409f9ec8852081f9990d6ad21.png
339ff2f409f9ec8852081f9990d6ad21.png

熔断规则

介绍

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

熔断规则实战

慢调用比例
异常比例
异常数

@SentinelResource注解

是什么?

@SentinelResource是一个流量防卫防护组件注解,用于指定防护资源,对配置的资源进行流量控制,熔断降级等功能。

实战

默认限流返回

安装rest地址限流 + 默认限流返回

  • 通过访问的rest地址来限流,会返回sentinel自带默认的限流处理信息

  • 新建业务类RateLimitController

    @RestController
    @Slf4j
    public class RateLimitController
    {
        @GetMapping("/rateLimit/byUrl")
        public String byUrl()
        {
            return "按rest地址限流测试OK";
        }
    }
    
  • 访问一次:http://localhost:8401/rateLimit/byUrl 没啥事

  • Sentinel控制台配置

    • be046003db348f23a2e69b1c468a31b1.png
  • 测试:疯狂点击http://localhost:8401/rateLimit/byUrl

    • 结果:
    • ec16b6583ccbb39ab6c271644a123e18.png
自定义限流返回

不想要默认自带的限流提示Blocked by Sentinel (flow limiting),想返回自定义的限流提示

  • 修改业务类RateLimitController

    @RestController
    @Slf4j
    public class RateLimitController
    {
        @GetMapping("/rateLimit/byUrl")
        public String byUrl()
        {
            return "按rest地址限流测试OK";
        }
    
    @GetMapping("/rateLimit/byResource")
    @SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException")
    public String byResource()
    {
        return "按资源名称SentinelResource限流测试OK";
    }
    public String handleException(BlockException exception)
    {
        return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o";
    }
    }
    
  • 测试地址:http://localhost:8401/rateLimit/byResource

  • Sentinel控制台配置

    3d3c481983833be6a9c30c2f884d992a.png6b6f1e5cc8816335c0c13159c8ef1094.png
  • 疯狂点击:http://localhost:8401/rateLimit/byResource

    • 返回自定义限流提示

      f1dec737ef7e9a49b4d14a981ac8f7e0.png
      f1dec737ef7e9a49b4d14a981ac8f7e0.png
自定义限流 + 服务降级处理

按照SentinelResource配置,点击超过限流配置返回自定义限流提示 + 程序异常返回fallbakc服务降级处理

  • 修改业务类RateLimitController

    @RestController
    @Slf4j
    public class RateLimitController
    {
        @GetMapping("/rateLimit/byUrl")
        public String byUrl()
        {
            return "按rest地址限流测试OK";
        }
    
        @GetMapping("/rateLimit/byResource")
        @SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException")
        public String byResource()
        {
            return "按资源名称SentinelResource限流测试OK";
        }
        public String handleException(BlockException exception)
        {
            return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o";
        }
    
        @GetMapping("/rateLimit/doAction/{p1}")
        @SentinelResource(value = "doActionSentinelResource",
                blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
        public String doAction(@PathVariable("p1") Integer p1) {
            if (p1 == 0){
                throw new RuntimeException("p1等于零直接异常");
            }
            return "doAction";
        }
    
        public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){
            log.error("sentinel配置自定义限流了:{}", e);
            return "sentinel配置自定义限流了";
        }
    
        public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){
            log.error("程序逻辑异常了:{}", e);
            return "程序逻辑异常了"+"\t"+e.getMessage();
        }
    
    }
    
  • 访问地址:http://localhost:8401/rateLimit/doAction/2

  • Sentinel控制台配置

    e7d0dfd651c039377a4361c571e705ca.png
  • 图形配置和代码关系

    416fe8f99ae3f95ee0467afa1d709f33.png
  • 测试:

    • 疯狂访问:http://localhost:8401/rateLimit/doAction/2,返回了自定义限流提示

      1f6a8b13f25126deef6df63658f3238e.png
      1f6a8b13f25126deef6df63658f3238e.png
    • p1参数为0,访问:http://localhost:8401/rateLimit/doAction/0,异常发生,返回了自定义的fallback服务降级处理

      603fb91522eb078909ca8a287480cc8e.png
      603fb91522eb078909ca8a287480cc8e.png
小结

blockHandler:主要针对sentinel配置后出现的违规情况处理

fallback:程序出现了异常,JVM抛出的异常服务降级

两者可以同时共存

热点规则

授权规则

规则持久化

Sentinel集成openfeign

作用?

实现fallback服务降级

需求说明

cloudalibaba-consumer-nacos-order83 通过OpenFeign调用 cloudalibaba-provider-payment9001

1 、83 通过OpenFeign调用 9001微服务,正常访问OK

2 、83 通过OpenFeign调用 9001微服务,异常访问error

访问者要有fallback服务降级的情况,不要持续访问9001加大微服务负担,但是通过feign接口调用的又方法各自不同,

如果每个不同方法都加一个fallback配对方法,会导致代码膨胀不好管理,工程埋雷....../(ㄒoㄒ)/~~

3 、public @interface FeignClient

通过fallback属性进行统一配置,feign接口里面定义的全部方法都走统一的服务降级,一个搞定即可

4、 9001微服务自身还带着sentinel内部配置的流控规则,如果满足也会被触发,也即本例有2个Case

4.1 、OpenFeign接口的统一fallback服务降级处理

4.2 、Sentinel访问触发了自定义的限流配置,在注解@SentinelResource里面配置的blockHandler方法。

编码步骤

修改服务提供方

1、修改微服务cloudAlibaba-provider-payment9001

  • 改pom:新增sentinel依赖即可,目的就是让服务提供方9001被哨兵sentinel监控
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
  • 写yaml:添加sentinel配置即可
spring:
  application:
    name: cloud-payment-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        enabled: true
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
        port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口

添加一个被sentinel哨兵监控的业务类用来测试

/**
 * ClassName: PaySentinelController
 * Package: org.cloud.controller
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/14 - 15:20
 * @Version: v1.0
 */
@RestController
public class PaySentinelController {

    @GetMapping("/pay/sentinel/get/{orderNo}")
    @SentinelResource(value = "getPayByOrderNo",blockHandler = "handlerBlockHandler")
    public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
    {
        //模拟从数据库查询出数据并赋值给DTO
        PayDto payDto = new PayDto();
        payDto.setOrderNo(orderNo);
        payDto.setPayNo("pay:"+ IdUtil.fastUUID());
        payDto.setUserId(1);

        return ResultData.success("查询返回值:"+payDto);
    }
    public ResultData handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception)
    {
        return ResultData.fail(ResponseCodeEnum.BAD_REQUEST.getCode(),"getPayByOrderNo服务不可用," +
                "触发sentinel流控配置规则"+"\t"+"o(╥﹏╥)o");
    }
    /*
    fallback服务降级方法纳入到Feign接口统一处理,全局一个
    public ResultData myFallBack(@PathVariable("orderNo") String orderNo,Throwable throwable)
    {
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"异常情况:"+throwable.getMessage());
    }
    */
}

修改openfeign服务

改pom:添加sentinel依赖

<!--alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

修改远程调用接口,并且添加fallback处理类

package org.cloud.apis;

import io.swagger.v3.oas.annotations.Operation;
import org.cloud.apis.fallback.PayFeignSentinelApiFallBack;
import org.cloud.model.dto.PayDto;
import org.cloud.response.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**
 * ClassName: AliPaymentFeign
 * Package: org.cloud.apis
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/13 - 14:57
 * @Version: v1.0
 */
@FeignClient(name = "cloud-payment-service",fallback = PayFeignSentinelApiFallBack.class)
public interface AliPaymentFeign {
    /**
     * sentinel哨兵监控测试
     * @param orderNo
     * @return
     */
    @GetMapping("/pay/sentinel/get/{orderNo}")
    ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}

新建PayFeignSentinelApiFallBack.class处理fallback逻辑,这就是全局唯一的服务降级处理

/**
 * ClassName: PayFeignSentinelApiFallBack
 * Package: org.cloud.apis.fallback
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/14 - 16:06
 * @Version: v1.0
 */
@Component
public class PayFeignSentinelApiFallBack implements AliPaymentFeign {
    @Override
    public ResultData getPayByOrderNo(String orderNo)
    {
        return ResultData.fail(ResponseCodeEnum.BAD_REQUEST.getCode(),"对方服务宕机或不可用,FallBack服务降级o(╥﹏╥)o");
    }
}

新建服务消费方

改pom

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.cloud</groupId>
        <artifactId>cloud2024</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloudAlibaba-consumer-payment83</artifactId>
    <dependencies>
        <!--自定义openfeign服务-->
        <dependency>
            <groupId>org.cloud</groupId>
            <artifactId>cloudAlibaba-openfeign-consumer</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--自定义-->
        <dependency>
            <groupId>org.cloud</groupId>
            <artifactId>cloud-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

写yaml

server:
  port: 83

spring:
  application:
    name: sentinel-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  main:
    allow-bean-definition-overriding: true

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

添加启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerPayment83App
{
    public static void main( String[] args )
    {
        SpringApplication.run(ConsumerPayment83App.class,args);
    }
}

添加业务类

@RestController
@RequiredArgsConstructor
public class ConsumerPaySentinelController {

    private final AliPaymentFeign aliPaymentFeign;

    @GetMapping("/consumer/pay/sentinel/get/{orderNo}")
    public ResultData getPayment(@PathVariable("orderNo") String orderNo){
        return aliPaymentFeign.getPayByOrderNo(orderNo);
    }
}

启动服务消费方时报错:

67d01bb958e455716eeba7200e6c89d7.png

解决方案

5dbe439d8eada9b0578bbeae21f6c300.png
测试

以上三个服务全部启动成功后,测试访问地址为(这个时候sentinel还未做任何的配置)

http://localhost:83/consumer/pay/sentinel/get/147

测试:即使疯狂点击进行请求handlerBlockHandler和fallback的逻辑也不会生效

配置sentinel

56d5754f1da5e1d6aceb181bf9afe4f8.pngf1d43338c867393c060afb4ea58628e5.png
  • 测试handlerBlockHandler是否生效:疯狂点击http://localhost:83/consumer/pay/sentinel/get/147

    • 结果触发了handlerBlockHandler方法,并输出自定义限流提示

      ebb82e4451e3e3e4f2a624cba3d5ced7.png
  • 测试fallback是否生效:将访问提供者9001宕机

    • 结果触发了PayFeignSentinelApiFallBack类中的方法,并输出自定义服务降级提示

      06fe25e70289b74313228e18bc9d839e.png

Sentinel与openfeign集成完毕

Sentinel集成gateway

需求说明

由Sentinel集成gateway通过限流方式来保护服务提供者cloud-Alibaba-provider-payment9001

集成步骤

建Model

cloudAlibaba-sentinel-gateway9528

改pom
<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-transport-simple-http</artifactId>
            <version>1.8.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.8.6</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
写yaml
server:
  port: 9528

spring:
  application:
    name: cloudAlibaba-sentinel-gateway9528 #以微服务注册进consul
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: pay_routh1
          uri: lb://sentinel-order-consumer
          predicates:
            - Path=/consumer/**
配置类(重要)

参考官网配置说明案例改写

/**
 * ClassName: GatewayConfiguration
 * Package: org.cloud.config
 * Description:
 *
 * @Author: @weixueshi
 * @Create: 2024/4/14 - 22:17
 * @Version: v1.0
 */

/**
 * 使用时只需注入对应的 SentinelGatewayFilter
 * 实例以及 SentinelGatewayBlockExceptionHandler 实例即可
 */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer)
    {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * javax.annotation.PostConstruct
     */
    @PostConstruct
    public void doInit() {
        initBlockHandler();
    }


    //处理/自定义返回的例外信息
    private void initBlockHandler() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /**
         * 参数1:网关唯一id
         * 参数2、3:1秒钟2个请求,超过触发限流
         */
        rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));

        GatewayRuleManager.loadRules(rules);
        BlockRequestHandler handler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
                Map<String,String> map = new HashMap<>();

                map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                map.put("errorMessage", "系统繁忙,稍后重试!");

                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(handler);
    }

}
测试
  • 原生url(不加网关)

    • http://localhost:9001/pay/sentinel/get/333
  • 加网关

    • http://localhost:9528/consumer/pay/sentinel/get/888

    • Sentinel + gateway:加快点击频率,出现限流错误

      825f519931b94f2414d0d8c748f2684d.png
      825f519931b94f2414d0d8c748f2684d.png

CloudAlibaba之Seata

问题引出

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题,但是,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

31ea4fb4c944da18970a3a1e68357c9d.png

结论

迫切希望提供一种分布式事务框架,解决微服务架构下的分布式事务问题

简介

是什么?

Simple Extensible Autonomous Transaction ArchitectureSeata

Apache Seata(incubating) 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

能干嘛?

解决分布式事务

去哪下?

官网:Apache Seataopen in new window

怎么玩?

  • 本地@Transactional

  • 全局@GlobalTransactional

  • Seata的分布式交易解决方案

    3c515c833d82540654e36ad43b6aeb93.png
    3c515c833d82540654e36ad43b6aeb93.png

工作流程简介

1、纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。

cc22fad2ef0170bb12ee3632c0f9d8eb.png

2、seata对分布式事务的协调和控制就是1+3

  • 一个XID

    XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。

  • 官网版3个概念(TC—>TM—>RM)

    b6af9b0c42eafd4200fd3a34f72ff50b.png
  • 个别版3个概念

    a3083a4ad75b6619a2eb2b0f70241b1f.png
    • TC (Transaction Coordinator) - 事务协调器
      • 就是seata,负责维护全局事务和分支事务的状态。驱动全局事务提交或回滚。
    • TM (Transaction Manager) - 事务管理器
      • 标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
    • RM (Resource Manager) - 资源管理器
      • 就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事,汇报分支事务状态,驱动分支事务的提交或回滚。

3、分布式事务的执行流程———小总结

三个组件相互协作,TC以Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行,

流程如下:

172b5796b545bb86f24e06330f317cda.png

4、各事务模式

64d08732e0f4fd6beef94760b48cb8bb.png
64d08732e0f4fd6beef94760b48cb8bb.png

我们以AT模式进行学习

Seata-Server2.0.0安装

下载地址

Seata-Server下载 | Apache Seataopen in new window

配置参数参考

https://seata.apache.org/zh-cn/docs/user/configuration

新手部署指南

d6f2e7b83658276ff3935e02f72b491f.png

步骤

建库

1、新建数据库Seata

CREATE DATABASE seata;
USE seata;

SQL建表地址

https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql

SQL脚本

CREATE TABLE IF NOT EXISTS `global_table`

(

    `xid`                       VARCHAR(128) NOT NULL,

    `transaction_id`            BIGINT,

    `status`                    TINYINT      NOT NULL,

    `application_id`            VARCHAR(32),

    `transaction_service_group` VARCHAR(32),

    `transaction_name`          VARCHAR(128),

    `timeout`                   INT,

    `begin_time`                BIGINT,

    `application_data`          VARCHAR(2000),

    `gmt_create`                DATETIME,

    `gmt_modified`              DATETIME,

    PRIMARY KEY (`xid`),

    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),

    KEY `idx_transaction_id` (`transaction_id`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store BranchSession data

CREATE TABLE IF NOT EXISTS `branch_table`

(

    `branch_id`         BIGINT       NOT NULL,

    `xid`               VARCHAR(128) NOT NULL,

    `transaction_id`    BIGINT,

    `resource_group_id` VARCHAR(32),

    `resource_id`       VARCHAR(256),

    `branch_type`       VARCHAR(8),

    `status`            TINYINT,

    `client_id`         VARCHAR(64),

    `application_data`  VARCHAR(2000),

    `gmt_create`        DATETIME(6),

    `gmt_modified`      DATETIME(6),

    PRIMARY KEY (`branch_id`),

    KEY `idx_xid` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store lock data

CREATE TABLE IF NOT EXISTS `lock_table`

(

    `row_key`        VARCHAR(128) NOT NULL,

    `xid`            VARCHAR(128),

    `transaction_id` BIGINT,

    `branch_id`      BIGINT       NOT NULL,

    `resource_id`    VARCHAR(256),

    `table_name`     VARCHAR(32),

    `pk`             VARCHAR(36),

    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',

    `gmt_create`     DATETIME,

    `gmt_modified`   DATETIME,

    PRIMARY KEY (`row_key`),

    KEY `idx_status` (`status`),

    KEY `idx_branch_id` (`branch_id`),

    KEY `idx_xid` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



CREATE TABLE IF NOT EXISTS `distributed_lock`

(

    `lock_key`       CHAR(20) NOT NULL,

    `lock_value`     VARCHAR(20) NOT NULL,

    `expire`         BIGINT,

    primary key (`lock_key`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

结果

c3d334d616178e4336c0c96e98bbfb52.png
c3d334d616178e4336c0c96e98bbfb52.png
改配置

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

e3c44b72ab2eb2a0c42767f5a39ab4be.png

application.yml定稿版(一般使用里面的配置足矣)

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

 
# Seata控制台访问端口
server:
  port: 7091

 
spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

 
# Seata控制台登录用户密码
console:
  user:
    username: seata
    password: seata

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace:
      group: SEATA_GROUP         #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos    

  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
      user: root
      password: '020708'
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      max-wait: 5000

  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'

  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
启动

先启动Nacos

再启动seata-server.bat

访问http://127.0.0.1:7091

090b7123c1de0ae629aea21b759d9e03.png
090b7123c1de0ae629aea21b759d9e03.png

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

0b58cda43153aa2369202d48935aed75.png
0b58cda43153aa2369202d48935aed75.png

案例实战

案例实战说明:

即将准备三个微服务模块:订单模块、库存模块、账户余额模块

主要说明演示:使用分布式事务Seata前后的区别说明:不使用分布式事务有什么影响,使用分布式事务可以解决哪些痛点。

数据库和表准备

注意:以下演示Nacos和Seata都要先保证启动ok

案例业务说明:

这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。-------最下面还有笔记

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,

再通过远程调用账户服务来扣减用户账户里面的余额,

最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

下订单 → 减库存 → 扣余额 → 改(订单)状态

731a49082a79eb33e9a81d9a802f5602.png
建库

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

5368036f58dd62223d28e3726312990b.png
5368036f58dd62223d28e3726312990b.png

SQL脚本

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
建表

针对这3个库,各自需要建对应的undo_log回滚日志表

SQL脚本

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

结果

777c146a62862ee5d3b8c2290dd03fe6.png

针对这3个库,各自需要建对应的业务表

seata_order库建t_order表

CREATE TABLE t_order(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',

`count` INT(11) DEFAULT NULL COMMENT '数量',

`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',

`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

SELECT * FROM t_order;

seata_account建t_account

CREATE TABLE t_account(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',

`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',

`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'

)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 

INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');

SELECT * FROM t_account;

seata_storage建t_storage

CREATE TABLE t_storage(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',

`total` INT(11) DEFAULT NULL COMMENT '总库存',

`used` INT(11) DEFAULT NULL COMMENT '已用库存',

`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');


SELECT * FROM t_storage;
最终效果
6b4d2888dbb8fc9913ca09cd7eb1bb02.png

编码落地实现

业务需求:下订单——>减库存——>扣余额——>改订单状态

创建openfeign需要远程调用的一些接口方法

seata-account-service远程接口

/**
 * @Author: @weixueshi
 * @Create: 2024/4/15 - 13:07
 * @Version: v1.0
 */
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
    /**
     * 扣减账户余额
     */
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

seata-storage-service远程接口

/**
 * @Author: @weixueshi
 * @Create: 2024/4/15 - 13:07
 * @Version: v1.0
 */
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
    /**
     * 扣减库存
     */
    @PostMapping(value = "/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

新建订单Order微服务
  • 建Model

    • seata-order-service2001
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
    
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--自定义openfeign服务-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloudAlibaba-openfeign-consumer</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2001
    
    spring:
      application:
        name: seata-order-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping: # 点击源码分析
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient //服务注册和发现
    @EnableFeignClients
    public class SeataOrderServiceApp
    {
        public static void main( String[] args )
        {
            SpringApplication.run(SeataOrderServiceApp.class,args);
        }
    }
    
  • 业务类

    这里省略了mapper等哪些创建

    OrderController

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private IOrderService orderService;

    /**
     * 创建订单
     */
    @GetMapping("/order/create")
    public ResultData create(Order order)
    {
        orderService.create(order);
        return ResultData.success(order);
    }

}

OrderServiceImpl

/**
 * @author author
 * @since 2024-04-15
 * 下订单->减库存->扣余额->改(订单)状态
 */
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    @Resource
    private OrderMapper orderMapper;

    /**
     * 订单微服务通过OpenFeign去调用库存微服务
     */
    @Resource
    private StorageFeignApi storageFeignApi;

    /**
     * 订单微服务通过OpenFeign去调用账户微服务
     */
    @Resource
    private AccountFeignApi accountFeignApi;


    @Override
//    @GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class)
    public void create(Order order) {

        //xid检查
        String xid = RootContext.getXID();

        //1. 新建订单
        log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
        //订单状态status:0:创建中;1:已完结
        order.setStatus(0);
        int result = orderMapper.insert(order);

        //插入订单成功后获得插入mysql的实体对象
        Order orderFromDB = null;
        if(result > 0)
        {
            QueryWrapper<Order> wrapper = new QueryWrapper<>();
            wrapper.eq("status", order.getStatus());
            orderFromDB = orderMapper.selectOne(wrapper);
            //orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
            log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
            System.out.println();
            //2. 扣减库存
            log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
            storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
            log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
            System.out.println();
            //3. 扣减账号余额
            log.info("-------> 订单微服务开始调用Account账号,做扣减money");
            accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
            log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
            System.out.println();
            //4. 修改订单状态
            //订单状态status:0:创建中;1:已完结
            log.info("-------> 修改订单状态");
            orderFromDB.setStatus(1);
            int updateResult = baseMapper.updateById(orderFromDB);

            log.info("-------> 修改订单状态完成"+"\t"+updateResult);
            log.info("-------> orderFromDB info: "+orderFromDB);
        }
        System.out.println();
        log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
    }
}
新建库存storage微服务
  • 建Model

    • seata-storage-service2002
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2002
    
    spring:
      application:
        name: seata-storage-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping:
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataStorageServiceapp
    {
        public static void main( String[] args )
        {
            System.out.println( "Hello World!" );
        }
    }
    
  • 业务类

    StorageController

@RestController
@RequestMapping("/storage")
public class StorageController {

    @Resource
    private IStorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/decrease")
    public ResultData decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return ResultData.success("扣减库存成功!");
    }
}

StorageServiceImpl

@Slf4j
@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService {

    @Resource
    private StorageMapper storageMapper;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        storageMapper.decrease(productId,count);
        log.info("------->storage-service中扣减库存结束");
    }
}

StorageMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.cloud.mapper.StorageMapper">
    <resultMap id="BaseResultMap" type="org.cloud.model.entity.Storage">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="product_id" jdbcType="BIGINT" property="productId" />
        <result column="total" jdbcType="INTEGER" property="total" />
        <result column="used" jdbcType="INTEGER" property="used" />
        <result column="residue" jdbcType="INTEGER" property="residue" />
    </resultMap>

    <update id="decrease">
        UPDATE
            t_storage
        SET
            used = used + #{count},
            residue = residue - #{count}
        WHERE product_id = #{productId}
    </update>
</mapper>
新建账户Account微服务
  • 建Model

    • seata-account-service2003
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2003
    
    spring:
      application:
        name: seata-account-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    
    
    
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping:
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataAccountServiceApp
    {
        public static void main( String[] args )
        {
            SpringApplication.run(SeataAccountServiceApp.class,args);
        }
    }
    
  • 业务类

    AccountController

@RestController
@RequestMapping("/account")
public class AccountController {
    @Resource
    private IAccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/decrease")
    public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
        accountService.decrease(userId,money);
        return ResultData.success("扣减账户余额成功!");
    }
}

AccountServiceImpl

@Slf4j
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Resource
    AccountMapper accountMapper;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, Long money) {
        log.info("------->account-service中扣减账户余额开始");

        accountMapper.decrease(userId,money);

        //myTimeOut();
        //int age = 10/0;
        log.info("------->account-service中扣减账户余额结束");
    }

    /**
     * 模拟超时异常,全局事务回滚
     */
    private static void myTimeOut()
    {
        try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

AccountMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.cloud.mapper.AccountMapper">
    <resultMap id="BaseResultMap" type="org.cloud.model.entity.Account">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="user_id" jdbcType="BIGINT" property="userId" />
        <result column="total" jdbcType="DECIMAL" property="total" />
        <result column="used" jdbcType="DECIMAL" property="used" />
        <result column="residue" jdbcType="DECIMAL" property="residue" />
    </resultMap>


    <!--
        money   本次消费金额

        t_account数据库表
        total总额度 = 累计已消费金额(used) + 剩余可用额度(residue)
    -->
    <update id="decrease">
        UPDATE
            t_account
        SET
            residue = residue - #{money},used = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>

案例实战测试

启动服务
  • 启动Nacos
  • 启动Seata
  • 启动openfeign服务
  • 启动订单微服务2001
  • 启动库存微服务2002
  • 启动账户微服务2003
数据库初始情况
c392fd8d9746436d0847dd863e6b2275.png
正常下单

此时我们没有在订单模块添加==@GlobalTransactional==

结果:没有任何问题

超时异常出错

此时我们没有在订单模块添加==@GlobalTransactional==

故障情况:

当库存和账户全额扣减后订单状态并没有设置为已经完成。没有从零改为1

数据库情况

56d17268016290569c5dc4153036b638.png
超时异常解决

此时我们在订单模块添加==@GlobalTransactional==

@Override
@GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class) //AT
public void create(Order order)

{
    ..........
}

此时,订单模块就是TM,也是其中一个RM

06d04b1d5b58a7378643d845cac88f10.png

查看Seata后台

http://IocaIhost:7091/#/Iogin

  • 全局事务ID

    • 9ac402fe5082306cc69ab61b3dd93e70.png
  • 全局锁

    • 272d01586b4b51d049fd5fbaba202b9e.png

下单后数据库并没有任何变化,被回滚了

  • 业务中
41487e080d4a57d58b9a82c2ceff476c.png
41487e080d4a57d58b9a82c2ceff476c.png

按照正常逻辑,本该有新记录入库,等待最后完成提交。

  • 回滚后

    • Order记录根本就插入不进来

      5172cdaac923752a846fbe48f780eaf5.png
      5172cdaac923752a846fbe48f780eaf5.png

上一步准备新增的记录被彻底回滚了,保证的一致性。

全部回退

总结

AT模式如何做到对业务的无侵入

是什么?
c74b2569dfde2c5e305c1dfdb4d8e35b.png
一阶段加载

在一阶段,Seata 会拦截“业务 SQL”,

1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,

2 执行“业务 SQL”更新业务数据,在业务数据更新之后,

3 其保存成“after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

7cdfe20fdd64ca75ca16626ea1d36c93.png
7cdfe20fdd64ca75ca16626ea1d36c93.png
二阶段分2种情况
正常提交

二阶段如是顺利提交的话,

因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

8f2efadea795bcde57fe92734ef08a34.png
8f2efadea795bcde57fe92734ef08a34.png
异常回滚

二阶段回滚:

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,

如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

717a62a80ba4d36d5dfee1f25835c885.png
717a62a80ba4d36d5dfee1f25835c885.png