SpringCloud

一、Web应用架构

1. 单体

  • 概念:整个项目所有的服务都由一台服务器提供,称为单一应用架构,也就是单机结构

  • 优点:部署简单,成本低

  • 缺点:

    • 扩展性差,不便于协同开发,不利于升级维护

    • 随着访问量的增加,单机应用负载太大,这时候就需要集群

2. 集群

  • 概念:单机处理到达瓶颈的时候,可以把单机复制几份,这样就构成了一个集群

  • 优点:通过集群来进行负载均衡,提高响应速度,实现高可用性

  • 缺点:

    • 当集群服务器数量到达一定程度时所带来的加速度会越来越小,此时单纯的增加服务器已无法明显提升响应速度

    • 每台服务器都部署完整的功能服务,会出现小服务资源的浪费

3. 分布式

  • 概念:

    • 将项目按照业务功能拆分成多个独立的子系统,分别部署在独立的服务器上,以提升效率

    • 每个子系统就被称为“服务”,这些子系统能够独立运行在Web容器中

    • 服务与服务之间的调用采用RPC方式(Remote Procedure Call远程过程调用,是一种进程间的通信方式)

  • 优点:通过拆分项目的业务,实现业务上的独立,降低了开发和维护的难度,便于协同开发,提高了扩展性

4. 微服务

  • 概念:

    • 微服务是一种架构模式,微服务架构的系统是一个分布式的系统

    • 把一个大型的应用程序拆分成很多个独立的小型的应用程序,提供相应的服务

    • 每个微服务完成单一的功能服务,这样的每个服务叫做一个微服务

    • 每个微服务可以被独立部署,运行在自己的进程中,并使用轻量级的机制通信

    • 每个微服务可以使用不同的编程语言,不同数据库,以保证最低限度的集中式管理

    • 服务与服务之间的调用采用REST方式,其实就是HTTP方式

  • 优点:

    • 分而治之:单个服务功能内聚,复杂性低,方便团队的拆分和管理

    • 可伸缩:能够单独的对指定的服务进行伸缩

    • 独立部署,独立开发

  • 缺点:微服务应用的结构较复杂,跨越多个微服务

二、SpringCloud

1. 简介

SpringCloud是一套完整的微服务解决方案,基于SpringBoot框架

SpringCloud是一系列框架的有序集合,它利用SpringBoot的开发便利性简化了分布式系统的开发

SpringCloud为开发人员提供了快速构建分布式系统的一些工具,如服务中心、配置中心、服务调用、消息总线、负载均衡、服务熔断、数据监控等

2. 技术栈

  • 服务中心(服务注册与发现)

    • Eureka:目前处于停更状态,使用较少【Netflix】

    • Zookeeper:早期的一个服务中心,一般与dubbo配合使用,使用较少

    • Consul:SpringCloud官方提供的服务中心,国内使用较少

    • Nacos:阿里开发的一个组件,实现了服务中心的功能,且经过百万级并发访问的考验

  • 配置中心

    • SpringCloud Config:SpringCloud官方提供的配置中心,比较麻烦,使用较少

    • Nacos:阿里开发的一个组件,实现了配置中心的功能

  • 服务调用

    • Feign:目前处于停更状态,使用较少【Netflix】

    • OpenFeign:SpringCloud官方提供的服务调用组件

  • 负载均衡

    • Ribbon:目前处于停更状态,使用较少【Netflix】

    • LoadBalancer:SpringCloud官方提供的负载均衡组件

  • 服务熔断/服务降级

    • Hystrix:目前处于停更状态,使用较少【Netflix】

    • Sentinel:阿里开发的的服务熔断组件,服务降级/限流

  • 服务网关

    • Zuul:目前处于停更状态,使用较少【Netflix】

    • Gateway:SpringCloud官方提供的网关

  • 服务监控

    • Spring Boot Admin:一个管理和监控Spring Boot应用程序的开源监控软件,提供了详细的监控信息

  • 服务链路

    • Sleuth :SpringCloud官方提供的服务链路跟踪组件

    • SkyWalking:Apache提供的服务链路跟踪组件

  • 分布式事务

    • Seata:阿里开发的分布式事务解决方案

三、服务直接调用

1. 简介

微服务之间通过HTTP的方式进行互相通信,可以在某个微服务中直接调用另一个微服务,会使用REST方式

可以使用Spring提供的RestTemplate,发送HTTP请求,直接调用另一个服务

2. 用法

步骤:

  1. 创建父工程cloud-parent

    <packaging>pom</packaging>
    ​
    <modules>
        <module>cloud-provider-6001</module>
        <module>cloud-consumer-7001</module>
    </modules>
    ​
    <properties>
        <spring-boot.version>2.7.14</spring-boot.version>
    </properties>
    ​
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
  2. 创建子工程 cloud-provider-6001

    服务的提供者,构建Restful API服务

    步骤:

    1. 添加依赖

      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
      </dependencies>
    2. 编辑application.yml

      server:
        port: 6001
      ​
      spring:
        application:
          name: user-provider # 服务名称
    3. 创建UserController

      @RestController
      @RequestMapping("/users")
      public class UserController {
      ​
          @Value("${server.port}")
          int port ;
      ​
          @GetMapping("/test")
          public String test(){
              return "provider:"+port;
          }
      }
    4. 启动并测试

      http://localhost:6001/users/test

  3. 创建cloud-consumer-7001

    服务的消费者,访问远程的HTTP服务

    步骤:

    1. 添加依赖

      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
      </dependencies>
    2. 编辑application.yml

      server:
        port: 7001
      ​
      spring:
        application:
          name: user-consumer
    3. 创建RestTemplateConfig配置类

      @Configuration
      public class RestTemplateConfig {
      ​
          @Bean
          public RestTemplate restTemplate(){
              return new RestTemplate();
          }
      }
    4. 创建UserController

      @RestController
      @RequestMapping("/users")
      public class UserController {
      ​
          @Resource
          private RestTemplate restTemplate;
      ​
          @GetMapping("/test")
          public String test() {
              // 直接调用服务提供者
              String str = restTemplate.getForObject("http://localhost:6001/users/test", String.class);
              return "consumer-->" + str;
          }
      ​
      }
    5. 启动并测试

      http://localhost:7001/users/test

四、Nacos

1. 简介

SpringCloud Alibaba Nacos 是一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台

执行流程:

  1. 服务提供者在启动时,向注册中心注册自己提供的服务

  2. 服务消费者在启动时,向注册中心订阅自己所需的服务

  3. 注册中心返回服务提供者地址给消费者,且当发生变化时会异步通知消费者

  4. 服务消费者根据负载均衡算法从提供者地址中选择一个,并调用该服务提供者

2. 安装

2.1 安装操作

步骤:

  1. 下载

    Releases · alibaba/nacos · GitHub

  2. 解压并启动

    解压缩nacos-server-2.2.3.zip,然后进入到bin目录下

    启动:

    • windows:startup.cmd -m standalone 单机模式运行

    • linux/mac:bash startup.sh -m standalone

    停止:

    • windows:shutdown.cmd

    • linux/mac:bash shutdown.sh

  3. 访问

    通过浏览器访问 http://localhost:8848/nacos,打开nacos管理界面,默认端口是8848,默认账号和密码是nacos

  4. 补充:新版nacos默认没有开启登录认证,即不需要输入账户密码

    修改conf/application.properties文件,开启登录认证功能

    # 开启登录认证
    nacos.core.auth.enabled=true
    ​
    # 客户端和服务端交互时用于加密的密钥,随意写
    nacos.core.auth.server.identity.key=wanho                         
    nacos.core.auth.server.identity.value=wanho
    ​
    # Token认证的密钥,随意写,但长度要符合要求
    nacos.core.auth.plugin.nacos.token.secret.key=wanho012345678901234567890123456789012345678901234567890123456789
2.2 外部数据库支持

Nacos支持将所有数据写入到MySQL数据库中。

步骤:

  1. 初始化MySQL数据库

新建一个数据库nacos,数据库初始化文件conf/mysql-schema.sql

  1. 修改conf/application.properties文件,设置MySQL数据源的相关信息

    spring.datasource.platform=mysql
    
    db.num=1
    
    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=root
    db.password.0=root
    
  2. 再以单机模式启动nacos

  3. 测试

    在nacos的配置管理中随意创建一个配置,此时会自动保存到MySQL数据库中

3. 用法

步骤:

  1. 配置父工程cloud-parent,添加依赖

    <properties>
        <spring-boot.version>2.7.14</spring-boot.version>
        <spring-cloud.version>2021.0.8</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
  2. 配置子工程cloud-provider-6001

    编辑pom.xml,添加依赖

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

    编辑application.yml,配置nacos信息

    server:
      port: 6001
    
    spring:
      application:
        name: user-provider
      cloud:
        nacos:
          discovery: # 服务注册中心
            server-addr: localhost:8848
    #        username: nacos
    #        password: nacos
    

    编辑启动类,开启服务注册发现功能

    @SpringBootApplication
    @EnableDiscoveryClient // 开启服务注册发现功能
    public class CloudProvider6001Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(CloudProvider6001Application.class, args);
    	}
    
    }
    
  3. 测试

在nacos管理界面——>服务管理中,能看到已经注册的服务

4. 心跳机制

心跳机制是用于监测和管理微服务可用性的机制,监测微服务的健康状态

  • 微服务注册启动后,每隔5秒会主动向 Nacos 服务器发起心跳包(包含当前服务的名称、IP、端口、集群名、权重等)

  • 服务器会根据心跳消息的到达情况和内容来判断微服务的可用性

  • 如果连续15秒没有收到心跳消息,就会将该服务实例标记为不健康、不可用

  • 如果连续3次没有收到客户端的心跳消息,就会将该服务实例从服务列表中移除。

五、OpenFeign和LoadBalancer

1. 简介

  • OpenFeign

    • OpenFeign是一个HTTP客户端,可以更快捷、优雅地调用HTTP服务

    • 只需要创建一个接口,然后在接口上添加注解就可以了

  • LoadBalancer

    • LoadBalancer是一个客户端负载均衡的工具,用于实现微服务的负载均衡

    • 提高响应速度,实现高可用性

2. 用法

步骤:

  1. 配置cloud-consumer-7001工程,添加依赖

    <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-openfeign</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    

    如果是新建工程,可以在创建时直接选择:OpenFeign和LoadBalancer

  2. 编辑application.yml文件

    server:
      port: 7001
    
    spring:
      application:
        name: cloud-consumer
      cloud:
        nacos:
          discovery: # 服务注册中心
            server-addr: localhost:8848
    
  3. 创建客户端接口,调用服务提供者

    @FeignClient("user-provider") // 指定要调用的服务提供者名称
    public interface UserClient {
    
        @GetMapping("/users/test")
        public String test();
    }
    
  4. 编辑UserController

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Resource
        // private RestTemplate restTemplate;
        private UserClient userClient;
    
        @GetMapping("/test")
        public String consumer() {
            // 直接调用服务提供者
            // String str = restTemplate.getForObject("http://localhost:6001/users/test", String.class);
    
            // 通过服务注册中心调用服务提供者
            String str = userClient.test();
    
            return "consumer-->" + str;
        }
    }
    
    
  5. 编辑启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients  // 开启Feign功能
    public class CloudConsumer7001Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(CloudConsumer7001Application.class, args);
    	}
    
    }
    
  6. 测试

    在nacos管理界面——>服务管理中,能看到已经注册的服务

    访问 http://localhost:7001/users/test

3. 负载均衡

步骤:

  1. 配置cloud-proiver-6001工程,编辑application.yml文件

    server:
      port: ${PORT:6001} # 通过参数获取端口号
    
  2. 在运行配置中,复制多个cloud-proiver,并分别指定不同的端口号(参数),模拟有多个服务提供者

    1. 启动并测试

      依次启动:

      • CloudProvider6001Application

      • CloudProvider6002Application

      • CloudProvider6003Application

      • CloudConsumer7001Application

      在nacos管理界面——>服务管理中,能看到user-provider服务的实例数有3个

      访问 http://localhost:7001/users/test,默认是按轮询的方式选择集群中的服务器

    2. 设置负载均衡

      常见的负载均衡策略:

      • 轮询 (RoundRobin) 按顺序轮流选择服务器【默认策略】

      • 随机 (Random) 随机选择服务器

      • 加权轮询 (Weighted RoundRobin) 根据服务器的权重来选择服务器,权重越大,被选中概率越大

      • 最短响应时间 (Least Response Time) 响应时间越短的服务器被选中的可能性大

      • 最小连接数 (Least Connection) 活动连接数越少的服务器被选中的可能性大

      创建配置类,切换负载均衡策略:

      @Configuration
      public class MyLoadBalancerConfig {
      
          @Bean
          ReactorLoadBalancer<ServiceInstance> loadBalancer(Environment environment,LoadBalancerClientFactory factory) {
              String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
              ObjectProvider<ServiceInstanceListSupplier> provider = factory.getLazyProvider(name, ServiceInstanceListSupplier.class);
              // 轮询
              // return new RoundRobinLoadBalancer(provider,name);
              // 随机
              return new RandomLoadBalancer(provider,name);
          }
      }
      

      注:目前SpringCloud LoadBalancer的ReactiveLoadBalancer负载均衡接口,提供的实现类只有两个:随机和轮询

      编辑UserClient,指定负载均衡配置:

      @FeignClient("user-provider") // 指定要调用的服务提供者名称
      @LoadBalancerClients(defaultConfiguration = {MyLoadBalancerConfig.class}) // 指定负载均衡配置
      public interface UserClient {
      	....
      }
      
    3. 重启并测试

      重新启动:CloudConsumer7001Application

      访问 http://localhost:7001/users/test,此时按随机的方式选择集群中的服务器

    4. 补充:项目启动时提示如下警告信息

      Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
      

      原因:Spring Cloud LoadBalancer官方建议使用Caffeine缓存,用来对服务进行缓存,效果更高,扩展更好。

      解决:添加Caffeine依赖(不要引入过高的版本,会出现兼容性问题)

      <dependency>
      	<groupId>com.github.ben-manes.caffeine</groupId>
      	<artifactId>caffeine</artifactId>
      	<version>2.9.3</version>
      </dependency>
      

4. 传参

OpenFeign传参的注意事项:

  • 传递参数时,必须添加注解,如@RequestParam、@PathVariable、@RequestBody等

  • 如果是请求体传参,必须添加@RequestBody

  • 如果是普通对象,不能直接传递,使用Map传参,必须添加@RequestParam

步骤:

  1. 创建cloud-common

    服务的通用基础类

  2. 配置cloud-provider-6001

  3. 配置cloud-consumer-7001

六、配置中心

1. 简介

在分布式应用中,将公共配置信息统一交由配置中心进行管理,如数据源配置、Redis配置等

Nacos既可以作为服务中心,也可以作为配置中心

相关术语概念:

  • Namespace 命名空间:代表不同环境,如开发、测试、生产等,默认值为public

  • Group 组:代表某个项目,如XX电商项目、XX医疗项目等,默认值为DEFAULT_GROUP

  • DataId 配置集:唯一标识,代表项目下某个服务的配置数据

    • 命名规范为:${spring.application.name}-${spring.profiles.active}.${file-extension}

    • 项目会自动根据该命名规范读取对应的配置信息,如user-provider-dev.yml

2. 用法

在cloud-provider-6001工程中,步骤:

  1. 编辑pom.xml文件,配置依赖

    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    
  2. 在Nacos中添加配置信息

    在nacos主页——>命名空间——>新建命名空间(shop-dev、shop-test、shop-prod)

    • 命名空间ID:shop-dev

    • 命名空间名:shop-dev

    • 描述:shop-dev

    在nacos主页——>配置管理——>配置列表——>创建配置

    • 命名空间:shop-dev

    • Group:SHOP_GROUP

    • Data ID:user-provider-dev.yml

    • 配置格式:YAML

    • 配置内容:

      spring:
        datasource:
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&useSSL=false
          username: root
          password: root
      
  3. 将application.yml文件,改名为bootstrap.yml,并配置

    使用Nacos配置中心时,建议用bootstrap.yml配置文件(引导文件)

    server:
      port: ${PORT:6001} # 通过参数获取端口号
    
    spring:
      application:
        name: user-provider # 服务名称
      profiles:
        active: dev
      cloud:
        nacos:
          discovery: # 服务注册中心
            server-addr: localhost:8848
          config: # 配置中心
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            namespace: shop-dev # 命名空间ID
            group: SHOP_GROUP # 组
            file-extension: yml # 指定配置文件的格式
    
  4. 测试

    @RestController
    @RefreshScope // nacos配置自动刷新
    public class ConfigController {
    
        @Value("${spring.datasource.url}")
        private String url;
    
        @RequestMapping("/config")
        public String config(){
            return url;
        }
    }
    

    访问:http://localhost:6001/config,查看是否可以获取到远程配置中心的配置信息,且当配置中心发生变更时会自动刷新

3. 共享配置

当不同模块需要共享使用相同的配置信息时,可以在Nacos中定义共享配置,然后在每个模块中读取共享配置。

步骤:

  1. 在Nacos中添加共享配置信息

    在nacos主页——>配置管理——>配置列表——>创建配置

    • 命名空间:shop-dev

    • Group:SHOP_GROUP

    • Data ID:application-dev.yml

    • 配置格式:YAML

    • 配置内容:

      spring:
        mvc:
          pathmatch:
            matching-strategy: ant_path_matcher
      
      shop:
        share: 
          name: world
      
  2. 编辑bootstrap.yml文件

    server:
      port: ${PORT:6001} # 通过参数获取端口号
    
    spring:
      application:
        name: user-provider # 服务名称
      profiles:
        active: dev
      cloud:
        nacos:
          discovery: # 服务注册中心
            server-addr: localhost:8848
          config: # 配置中心
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            namespace: shop-dev # 命名空间ID
            group: SHOP_GROUP # 组
            file-extension: yml # 指定配置文件的格式
            shared-configs: # 共享配置
              - data-id: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
                group: ${spring.cloud.nacos.config.group}
                refresh: true # 自动刷新
    
  3. 测试

    @RestController
    @RefreshScope // nacos配置自动刷新
    public class ConfigController {
    
        @Value("${spring.datasource.url}")
        private String url;
    
        @Value("${shop.share.name}")
        private String name;
    
        @RequestMapping("/config")
        public String config(){
            return url + "---------" + name;
        }
    }
    

七、Gateway

1. 简介

Gateway是API网关服务,用来保护、增强和控制对API服务的访问

Gateway提供了统一的路由方式,同时还提供了客户端负载均衡、统一认证、流量监控和速率限制等功能

Gateway中存在三大核心概念:路由、断言、过滤器

  • 路由Route

    • 路由是将外部的请求转发到具体的微服务实例上,是实现外部访问统一入口的基础

    • 它由id、目标uri、一系列的断言与过滤器组成

  • 断言Predicate

    • 断言是作为匹配条件,可以匹配HTTP请求中的所有内容,例如:请求路径、请求头、请求参数、请求时间等

    • 如果请求与断言相匹配则进行路由

  • 过滤器Filter

    • 对请求的处理过程进行干预,实现请求校验、服务聚合等功能

    • 使用过滤器,可以在请求被路由前或者后进行修改

2. 用法

步骤:

  1. 创建工程cloud-gateway-5001,选择以下模块:Gateway、LoadBalancer

  2. 添加依赖

    <dependencies>
    	<dependency>
    		<groupId>org.springframework.cloud</groupId>
    		<artifactId>spring-cloud-starter-gateway</artifactId>
    	</dependency>
      <dependency>
    		<groupId>org.springframework.cloud</groupId>
    		<artifactId>spring-cloud-starter-loadbalancer</artifactId>
    	</dependency>
    	<dependency>
    		<groupId>com.alibaba.cloud</groupId>
    		<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    	</dependency>
    </dependencies>
    
  3. 编辑application.yml文件

    server:
      port: 5001
    
    spring:
      application:
        name: cloud-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          routes: # 配置路由表
            - id: users_route # 路由id,必须唯一
              uri: lb://user-provider # 当前路由要转发到的微服务,格式为:lb://微服务名
              predicates: # 配置断言
                - Path=/users/** # 指定哪些请求会进入到该路由,多个路径之间以逗号隔开
    
  4. 编辑启动类,启用Gateway

    @SpringBootApplication
    @EnableDiscoveryClient
    public class CloudGateway5001Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(CloudGateway5001Application.class, args);
    	}
    
    }
    
  5. 启动并测试

    依次启动:

    • CloudProvider6001Application

    • CloudProvider6002Application

    • CloudProvider6003Application

    • CloudGateway5001Application

    测试:

    • 在nacos管理界面——>服务管理中,能看到cloud-gateway服务已经注册

    • 直接向gateway发送请求进行测试 http://localhost:5001/users/test,默认是按轮询的方式

    • 将cloud-consumer-7001中UserClient的服务调用改为@FeignClient("cloud-gateway"),然后测试接口功能

注:配置网关后,此时的负载均衡策略是由网关来指定的,所有请求都是交由网关来转发给后台的服务器!

3. 路由配置

为请求路径添加前缀:

spring:
    gateway:
      routes: # 配置路由表
        - id: users_route # 路由id,必须唯一
          uri: lb://user-provider # 当前路由要转发到的微服务,格式为:lb://微服务名
          predicates: # 配置断言
            # - Path=/users/** # 指定哪些请求会进入到该路由,多个路径之间以逗号隔开
            - Path=/api/v2/users/** # 为请求路径添加前缀
          filters:
            - StripPrefix=2 # 请求路径的前缀过滤,表示去掉前两个路径

测试:直接向gateway发送请求进行测试 http://localhost:5001/api/v2/users/test

配置路由的两种方式:

  • 配置文件方式

    spring:
      cloud:
        gateway:
          routes: # 配置路由表
            - id: users_route # 路由id,必须唯一
              uri: lb://user-provider # 当前路由要转发到的微服务,格式为:lb://微服务名
              predicates: # 配置断言
                 # - Path=/users/** # 指定哪些请求会进入到该路由,多个路径之间以逗号隔开
                 - Path=/api/v2/users/** # 为请求路径添加前缀
              filters:
                - StripPrefix=2 # 请求路径的前缀过滤,表示去掉前两个路径
    
  • 注解方式

    创建一个配置类,在该类中通过注解进行配置

    @Configuration
    public class GatewayConfig {
    
        @Bean
        public RouteLocator routeLocator(RouteLocatorBuilder builder) {
            RouteLocatorBuilder.Builder routes = builder.routes();
    
            /**
             * 参数一:路由id
             * 参数二:访问路径及对应的远程资源地址
             */
            routes.route("users_route", p -> p.path("/users/**").uri("lb://user-provider"));
    
            return routes.build();
        }
    }
    

4. 断言

共有12种内置的断言,常用的断言类型:Path、Method、After、Before、Between等

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      # 配置路由表
      routes:
        - id: users_route # 路由id,必须唯一
          # uri: http://127.0.0.1:8001 # 当前路由要转发到的微服务地址,此处写的是固定地址,如果是动态路由的话可以使用微服务名
          uri: lb://user-provider # 使用动态路由访问集群环境,格式为:lb://微服务名
          predicates: # 配置断言
            # - Path=/users/** # 该断言必须配置,指定哪些请求会进入到该路由,多个路径之间以逗号隔开
            - Path=/api/v2/users/** # 为请求路径添加前缀
            - Method=GET,POST # 必须是指定的请求方式才能访问
          filters:
            - StripPrefix=2 # 请求路径的前缀过滤,表示去掉前两个路径

5. 过滤器

Spring Cloud Gateway 中的Filter 分为两种类型:

  • 全局过滤器 Global Filter

  • 路由过滤器 Gateway Filter

5.1 全局过滤器

全局过滤器作用于所有的路由,不需要单独配置,只需要实现GlobalFilterOrdered两个接口即可。

步骤:

  1. 自定义一个全局过滤器,实现:网关鉴权+白名单放行

    /**
     * 网关鉴权,实现统一的身份认证
     */
    @Component
    public class AuthFilter implements GlobalFilter, Ordered {
     
        @Resource
        private IgnoreWhiteProperties ignoreWhite;
    
        /**
         * 过滤器的核心方法
         * @param exchange 用于操作请求/响应的相关信息
         * @param chain 过滤器链,用于调用下一个过滤器或目标资源
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            System.out.println("AuthFilter.filter................");
    
            /**
             * 跳转不需要校验的白名单
             */
            String url = exchange.getRequest().getURI().getPath();
            if (ignoreWhite.getWhites().contains(url)){
                return chain.filter(exchange); // 放行
            }
    
            /**
             * 获取token,并判断是否为空
             */
            String token = exchange.getRequest().getHeaders().getFirst("Authorization");
            if(StringUtils.isEmpty(token)){
                ServerHttpResponse res = exchange.getResponse();
                String jsonStr = JSONUtil.toJsonStr(AjaxResult.error(HttpStatus.UNAUTHORIZED.value(), "请先登录再操作!"));
                DataBuffer buffer = res.bufferFactory().wrap( jsonStr.getBytes(StandardCharsets.UTF_8));
                res.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                return res.writeWith(Mono.just(buffer));
            }
            return chain.filter(exchange); // 放行,调用下一个过滤器或目标资源
        }
    
        /**
         * 定义过滤器的优先级:值越小,优先级越高
         */
        @Override
        public int getOrder() {
            return 0;
        }
    }
    
  2. 编辑application.yml

    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes: # 配置路由表
            - id: users_route # 路由id,必须唯一
              uri: lb://user-provider # 当前路由要转发到的微服务,格式为:lb://微服务名
              predicates: # 配置断言
               # - Path=/users/** # 指定哪些请求会进入到该路由,多个路径之间以逗号隔开
                - Path=/api/v2/users/** # 为请求路径添加前缀
                - Method=GET,POST # 必须是指定的请求方式才能访问
              filters:
                - StripPrefix=2 # 请求路径的前缀过滤,表示去掉前两个路径
                - BlackListUrlFilter
            - id: auth_route
              uri: lb://auth-provider
              predicates:
                - Path=/auth/**
              filters:
                - StripPrefix=1
    
    # 不校验白名单
    security:
      ignore:
        whites:
          - /auth/login
          - /auth/logout
          - /auth/register
    
5.2 路由过滤器

路由过滤器用于对指定路由进行过滤处理操作,默认内置了多种路由过滤器,如:StripPrefix去除前缀的过滤器。

自定义路由过滤器时,需要继承AbstractGatewayFilterFactory父类

步骤:

  1. 定义一个黑名单过滤器,用于对黑名单进行拦截,黑名单中的地址不允许访问。

    /**
     * 黑名单过滤器
     */
    @Component
    public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
    
        // 指定配置的类型
        public BlackListUrlFilter(){
            super(Config.class);
        }
    
        @Override
        public GatewayFilter apply(Config config) {
            return (exchange, chain) -> {
                System.out.println("BlackListUrlFilter.filter...........................");
    
                String url = exchange.getRequest().getURI().getPath();
                // if (url.equals("/users/list")){ // 如果是黑名单中的url,则拦截
                if(config.getBlacklistUrl().contains(url)){
                    ServerHttpResponse res = exchange.getResponse();
                    String jsonStr = JSONUtil.toJsonStr(AjaxResult.error(HttpStatus.FORBIDDEN.value(),"请求地址不允许访问"));
                    DataBuffer buffer = res.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
                    res.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                    return res.writeWith(Mono.just(buffer));
                }
                return chain.filter(exchange);
            };
        }
    
        public static class Config{
    
            private List<String> blacklistUrl = new ArrayList<>();
    
            public List<String> getBlacklistUrl() {
                return blacklistUrl;
            }
    
            public void setBlacklistUrl(List<String> blacklistUrl) {
                this.blacklistUrl = blacklistUrl;
            }
        }
    }
    
  2. 编辑application.yml

    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          # 配置路由表
          routes:
            - id: users_route # 路由id,必须唯一
              uri: lb://user-provider # 使用动态路由访问集群环境,格式为:lb://微服务名
              predicates: # 配置断言
                # - Path=/users/** # 该断言必须配置,指定哪些请求会进入到该路由,多个路径之间以逗号隔开
                - Path=/api/v2/users/** # 为请求路径添加前缀
                - Method=GET,POST # 必须是指定的请求方式才能访问
              filters:
                - StripPrefix=2 # 请求路径的前缀过滤,表示去掉前两个路径
    #            - BlackListUrlFilter  # 黑名单
                - name: BlackListUrlFilter
                  args:
                    blacklistUrl:
                      - '/users/list'
                      - '/users/delete'
                      - '/users/update'
    
  3. 测试

    访问 http://localhost:5001/api/v2/users/list 提示"请求地址不允许访问"

八、Sentinel

1. 简介

1.1 服务熔断和服务降级

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。

服务雪崩:在微服务架构中服务之间会相互调用和依赖,如果某个服务发生故障,可能会导致多个服务故障,从而导致整个系统故障

解决服务雪崩的方式:

  • 服务熔断

    当服务出现不可用或响应超时时,为了防止整个系统出现雪崩, 暂时停止对该服务的调用,直接返回一个结果,快速释放资源。

    如果检测到目标服务情况好转,则恢复对目标服务的调用。

  • 服务降级

    为了防止核心业务功能出现负荷过载或者响应慢的问题,将非核心服务进行降级,暂时性的关闭或延迟使用,保证核心服务的正常运行

1.2 Sentinel介绍

Spring Cloud Alibaba Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件。

  • 主要以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。

  • 官网 home | Sentinel

相关术语概念:

  • 资源是 Sentinel 的关键概念,它可以是 Java 应用程序中的任何内容,能够被 Sentinel 保护起来

  • 规则:围绕资源的实时状态设定的规则,包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整

  • 吞吐量(Throughput) :系统在单位时间内处理请求的数量

  • QPS每秒查询率(Query Per Second):每秒请求数据数量

  • 响应时间(RT):平均响应时间

  • 并发数: 系统同时处理的请求数量

2. 安装

步骤:

  1. 下载

    Releases · alibaba/Sentinel · GitHub

  2. 启动

执行以下命令:

java -jar sentinel-dashboard-1.8.6.jar # 默认使用的是8080端口

以指定端口运行:

java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
  1. 访问

通过浏览器访问 http://localhost:8718,打开Sentinel控制台,默认账号和密码是sentinel

3. 用法

在cloud-consumer-7001工程中,步骤:

  1. 编辑pom.xml文件,添加依赖

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  1. 编辑application.yml文件

server:
  port: 7001

spring:
  application:
    name: user-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    # sentinel配置
    sentinel:
    	eager: true # 取消控制台懒加载,项目启动即连接sentinel
      transport:
        dashboard: localhost:8718 # 配置sentinel控制台地址
  main:
    allow-circular-references: true # 允许循环引用
  1. 测试

首先发送请求,如:http://localhost:7001/users

然后在Sentinel控制台中可以看到对应的请求

4. 流控规则

4.1 简介

在Sentinel控制台中可以设置流控规则

  • 资源名

    • 当前为哪一个请求设置流控规则

    • 默认为当前的请求地址

  • 针对来源

    • Sentinel可以针对调用者进行限流

    • 填写对应的微服务名

    • 默认default(不区分来源)

  • 阈值类型/单机阈值

    • QPS:限制每秒的请求数量,当调用的请求数达到了QPS设置的阈值,进行限流

    • 并发线程数:当调用的请求的线程数达到了阈值,进行限流

  • 是否集群

    • 可以设置为不用集群

4.2 使用

首先为某个请求设置一个流控规则,如:限制1秒只能发送一次请求

然后访问对应的请求,如果一秒访问的请求数超过了1次,则会提示Blocked by Sentinel (flow limiting)

4.3 自定义限流异常处理

使用@SentinelResource注解,定义出现限流异常时的处理策略,可以自定义提示消息。

  • 使用当前类中的异常处理方法

/**
 * 使用@SentinelResource自定义限流异常处理
 *  value属性,表示资源名称,可以自定义,但是其值不能带有/
 *  blockHandlerClass属性,指定处理异常的方法所在的类,未指定时表示处理异常的方法在当前类中
 *  blockHandler属性,指定处理异常的方法名称
 */
@SentinelResource(value = "UserListResource",blockHandler = "handlerException")
@GetMapping
public AjaxResult getUserList(String username){
    return userClient.getUserList(username);
}

/**
 * 处理异常的方法
 *  1.方法返回值需要与原方法一致
 *  2.方法参数列表需要与原方法一致,同时多一个BlockException参数,表示异常对象
 */
public AjaxResult handlerException(String username, BlockException e){
    return AjaxResult.error("请求过于频繁,请稍后再试!参数为:" + username+",异常:"+e);
}
  • 使用异常处理类中的异常处理方法

public class ExceptionUtil {
    /**
     * 处理全局Sentinel异常的方法
     * 该方法必须为static的
     */
    public static AjaxResult handlerException(String username, BlockException e){
        return AjaxResult.error("Sentinel限流,请稍后再试:" + e);
    }
}

// 指定blockHandlerClass属性时,表示处理异常的方法在另一个类中,同时处理异常方法必须为static的
@SentinelResource(value = "UserListResource",blockHandlerClass = ExceptionUtil.class,blockHandler = "handlerException")
@GetMapping
public ResponseResult getUserList(){
		return userService.getUserList();
}

5. 服务熔断

通过Sentinel实现服务熔断,其实就是一种机制

  • 当某个服务不可用时,暂时停止对该服务的调用,可以阻断故障的传播,也称为断路器或熔断器

  • 当出现连续多次服务调用失败时,会进行熔断保护,直接返回一个由开发者设置的fallback(退路)信息

  • 会定期再次检查故障的服务,如果故障服务恢复,将继续使用服务

步骤:

  1. 编辑application.yml文件,启用服务熔断

    # 启用sentinel熔断
    feign:
      sentinel:
        enabled: true
    
  2. 设置fallback信息

    在@FeignClient注解中指定fallback参数

    @FeignClient(value = "cloud-gateway", fallback = UserClientFallback.class)
    public interface UserClient {
    

    创建UserClient接口的实现类,并配置返回的信息

    @Component
    public class UserClientFallback implements UserClient {
    
        @Override
        public String test() {
            return "请求失败";
        }
    
        @Override
        public AjaxResult getUserList(String username) {
            System.out.println("服务熔断开启。。。。。。");
            return AjaxResult.error("获取用户列表失败");
        }
    
        @Override
        public AjaxResult deleteUser(Integer id) {
            return AjaxResult.error("删除用户失败");
        }
    
        @Override
        public AjaxResult postUser(User user) {
            return AjaxResult.error("添加用户失败");
        }
    
        @Override
        public AjaxResult putUser(User user) {
            return AjaxResult.error("修改用户失败");
        }
    
        @Override
        public AjaxResult query(Map<String, Object> map) {
            return AjaxResult.error("查询用户失败");
        }
    }
    
  3. 测试

    当服务提供者不可用或出现异常时,会暂时停止对该服务的调用

6. 持久化

Sentinel结合Nacos实现持久化,使用Nacos作为数据源。

其实就是将Sentinel规则保存到Nacos中,实现规则的持久化。

步骤:

  1. 编辑pom.xml文件,配置依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. 编辑application.yml文件

server:
  port: 7001

spring:
  application:
    name: user-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    # sentinel配置
    sentinel:
      transport:
        dashboard: localhost:8718 # sentinel控制台地址
      datasource: # sentinel数据源
        ds1: # 该名称自定义,此处表示限流配置
          nacos:
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            namespace: sentinel-dev
            group-id: SENTINEL_GROUP
            dataId: sentinel-flow-rule
            data-type: json
            rule-type: flow # 限流规则

  main:
    allow-circular-references: true # 允许循环引用

# 启用sentinel熔断
feign:
  sentinel:
    enabled: true
  1. 在Nacos中添加配置信息

    在nacos主页——>配置管理中,新增配置信息

    • 命名空间:sentinel-dev

    • Group:SENTINEL_GROUP

    • Data ID:sentinel-flow-rule

    • 配置格式:JSON

    • 配置内容:

      [
          {
               "resource":"UserListResource",
               "limitApp":"default",
               "grade":1,
               "count":2,
               "strategy":0,
               "controlBehavior":0,
               "clusterMode":false
          }
      ]
      

    配置项说明:

    • resource:资源名称

    • limitApp:来源

    • grade:阀值类型,0---线程数,1---QPS

    • count:单机阀值

    • strategy:流控模式,0---直接,1---关联,2---链路

    • controlBehavior:流控效果,0---快速失败,1---warmUp,2---排队等待

    • clusterMode:是否集群

  2. 测试

    首先发送请求,如:http://localhost:7001/users

    然后在Sentinel控制台中可以看到对应的流控规则,并且限流生效

九、Gateway整合Sentinel

1. 网关限流

网关限流:就是通过网关层对服务进行限流,从而达到保护后端服务的作用。

在cloud-gateway-5001工程中,步骤:

  1. 编辑pom.xml文件,添加依赖

    <!-- nacos-config -->
    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <!--sentinel-->
    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- sentinel-nacos -->
    <dependency>
    	<groupId>com.alibaba.csp</groupId>
    	<artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--sentinel-gateway-->
    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    </dependency>
    
  2. 将application.yml文件,改名为bootstrap.yml,并配置

    连接配置中心、连接sentinel、sentinel规则持久化

    server:
      port: 5001
    
    spring:
      application:
        name: cloud-gateway
      profiles:
        active: dev
      cloud:
        nacos:
          discovery: # 服务中心
            server-addr: localhost:8848
          config: # 配置中心
            server-addr: ${spring.cloud.nacos.discovery.server-addr}
            file-extension: yml
        sentinel:
          eager: true # 取消控制台懒加载
          transport:
            dashboard: localhost:8718  # 控制台地址
          datasource: # nacos配置持久化
            ds1:
              nacos:
                server-addr: ${spring.cloud.nacos.discovery.server-addr}
                dataId: sentinel-gateway
                groupId: DEFAULT_GROUP
                data-type: json
                rule-type: gw-flow # 网关限流规则
    
  3. 在Nacos中添加配置信息

    添加gateway配置:

    • 命名空间和Group:使用默认的publicDEFAULT_GROUP

    • Data ID:cloud-gateway-dev.yml

    • 配置格式:YAML

    • 配置内容:

      spring:
        cloud:
          gateway:
            routes:
              # 用户模块
              - id: users_route
                uri: lb://user-provider
                predicates:
                  - Path=/api/v2/users/**
                filters:
                  - StripPrefix=2
                  - name: BlackListUrlFilter
                    args:
                      blacklistUrl:
                        - '/users/list'
                        - '/users/delete'
                        - '/users/update'
              # 认证模块
              - id: auth_route
                uri: lb://auth-provider
                predicates:
                  - Path=/auth/**
                filters:
                  - StripPrefix=1
      
      
      # 不校验白名单
      security:
        ignore:
          whites:
            - /auth/login
            - /auth/logout
            - /auth/register
      

    添加sentinel限流配置:

    • 命名空间和Group:使用默认的publicDEFAULT_GROUP

    • Data ID:sentinel-gateway

    • 配置格式:JSON

    • 配置内容:

      [
      	{
              "resource": "users_route",
              "limitApp": "default",
              "grade": 1,
              "count": 1,
              "strategy": 0,
              "controlBehavior": 0
          }
      ]
      

    注:资源名resource为Route ID,根据Route ID设置限流,一般是对某个微服务进行限流。

  4. 编辑启动类,标记该应用为API网关类型,即网关流控

    @SpringBootApplication
    @EnableDiscoveryClient
    public class CloudGateway5001Application {
    
    	public static void main(String[] args) {
    		System.setProperty("csp.sentinel.app.type", "1"); // 标记该应用为API网关类型
    		SpringApplication.run(CloudGateway5001Application.class, args);
    	}
    
    }
    
  5. 测试

在Sentinel控制台中可以看到对应的流控规则

直接向gateway发送请求,进行流控的测试:http://localhost:5001/api/v2/users

2. 限流异常处理

定义一个限流异常处理类,实现 WebExceptionHandler 接口

/**
 * 自定义限流异常处理
 */
@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
public class SentinelFallbackHandler implements WebExceptionHandler {
    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
        ServerHttpResponse res = exchange.getResponse();
        String jsonStr = JSONUtil.toJsonStr(AjaxResult.error(HttpStatus.TOO_MANY_REQUESTS.value(), "请求超过最大数,请稍候再试"));
        DataBuffer buffer = res.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
        res.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return res.writeWith(Mono.just(buffer));
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        }
        if (!BlockException.isBlockException(ex)) {
            return Mono.error(ex);
        }
        return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}

3. 网关异常处理

定义一个网关异常处理类,实现 ErrorWebExceptionHandler 接口

/**
 * 网关统一异常处理
 */
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse res = exchange.getResponse();
        String jsonStr = JSONUtil.toJsonStr(AjaxResult.error(ex.getMessage()));
        DataBuffer buffer = res.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
        res.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return res.writeWith(Mono.just(buffer));
    }
}

十、Seata

1. 简介

分布式事务:

  • 指一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

  • 参考:分布式事务 | RuoYi

Seata分布式事务:

  • Seata是阿里开发的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

  • Seata提供了四种事务模式:AT模式(推荐)、TCC模式、Saga模式、XA模式

  • 参考:Apache Seata

2. 术语

在Seata的架构中,一共有三个角色:

  • TC(Transaction Coordinator) - 事务协调者

    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM(Transaction Manager) - 事务管理器

    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM(Resource Manager) - 资源管理器

    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata架构图如下所示:

  • TM 和 RM 是作为 Seata客户端与业务系统集成在一起

  • TC 作为 Seata服务端独立部署。

3. 执行流程

在 Seata 中,分布式事务的执行流程:

  • TM 开启分布式事务(TM 向 TC 注册全局事务记录);

  • 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );

  • TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);

  • TC 汇总事务信息,决定分布式事务是提交还是回滚;

  • TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;

注:整个过程是分为两个阶段来实现的,即成功则提交,失败则回滚。

4. AT模式

AT模式(Autonomous Transaction)是Seata独有的一种模式,通过自动补偿机制实现数据回滚,也是实际项目中比较常用的一种模式。

原理:

  • 通过生成反向SQL实现数据回滚,需要在数据库中额外添加undo_log表,undo_log表中保存的是自动生成的回滚SQL。

示例:

  • 业务逻辑要执行的sql

    insert into 订单 values(1001,...)
    update 仓储 set num = 300 where gid =100;
    
  • 自动生成回滚sql

    delete from 订单 where id =1001
    update 仓储 set num=210 where gid = 100
    

特点:

  • 效率高,使用简单(Seata自动生成反向SQL并回滚)

  • 对业务无侵入

  • 需要注意的是,所有服务与数据库必须要自己拥有管理权,因为要创建undo_log表

5. 安装

Seata服务端也是一个微服务,和其他微服务一样也需要注册中心配置中心,同时需要记录事务相关信息,可以将其存储到数据库中。

步骤:

  1. 下载并解压Seata

    https://seata.io/zh-cn/blog/download.html

    seata-server-1.7.0.zip

  2. 修改Seata配置文件

    进入到seata/conf目录下,编辑application.yml文件,内容如下:

    server:
      port: 7091
     
    spring:
      application:
        name: seata-server
     
    logging:
      config: classpath:logback-spring.xml
      file:
        path: ${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服务端配置 
    seata:
      service:
        vgroupMapping:
          default-tx-group: default # 事务组名: 集群名,都可以自定义
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace:          
          group: SEATA_GROUP
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 127.0.0.1:8848
          namespace:           
          group: SEATA_GROUP
          cluster: default # 集群名
      store:
        mode: db
        db:
          datasource: druid
          db-type: mysql
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/ry-seata?useUnicode=true&characterEncoding=utf8&useSSL=false
          user: root
          password: root
          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
      security:
        secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
        tokenValidityInMilliseconds: 1800000
        ignore:
          urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
    
  3. 初始化MySQL数据库

    新建一个数据库ry-seata,数据库初始化文件script/server/db/mysql.sql

    • global_table: 全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID。

    • branch_table: 分支事务表,记录每一个分支事务的 ID,分支事务操作的哪个数据库等信息。

    • lock_table: 全局锁表。

    • distributed_lock:异步任务锁表,seata1.5之后新增的表,用于seate-server异步任务调度

  4. 启动Seata

    进入到seata/bin目录下:

    • windows:双击seata-server.bat即可启动

    • linux/mac:执行bash seata-server.sh命令

  5. 访问

    通过浏览器访问 http://localhost:7091,打开Seata控制台界面,默认端口是7091,默认账号和密码是seata

    在nacos管理界面——>服务管理中,能看到已经注册的seata-server服务

    注:Seata启动时会占用两个端口:7091 和 8091

    • 7091是客户端端口

    • 8091是注册到nacos中的服务端端口

6. 用法

步骤:

  1. 在Nacos中添加事务组的信息

    • 命名空间:使用默认的public

    • Group:SEATA_GROUP

    • Data ID:service.vgroupMapping.default-tx-group

    • 配置格式:TEXT

    • 配置内容:

      default
      

    注:service.vgroupMapping.xxx-xxx-xxx中的xxx-xxx为事务组名,配置内容为集群名

  2. 添加依赖

    <!-- 分布式事务seata -->
    <dependency>
      <groupId>com.ruoyi</groupId>
      <artifactId>ruoyi-common-seata</artifactId>
    </dependency>
    
  3. 修改各服务模块的配置

    凡是需要使用分布式事务的模块都需要修改!

    spring:
      datasource:
        dynamic:
          seata: true    # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭
    
    # seata配置
    seata:
      enabled: true
      # 事务组名
      tx-service-group: default-tx-group
      # 关闭自动代理
      enable-auto-data-source-proxy: false
      # 服务配置项
      service:
        vgroup-mapping:
          default-tx-group: default
      config:
        type: nacos
        nacos:
          serverAddr: 127.0.0.1:8848
          group: SEATA_GROUP
          namespace:
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 127.0.0.1:8848
          namespace:
    
  4. 各数据库中添加undo_log表

    凡是需要使用分布式事务的模块,对应的数据库中都需要添加该表!

    CREATE TABLE `undo_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `branch_id` bigint(20) NOT NULL,
      `xid` varchar(100) NOT NULL,
      `context` varchar(128) NOT NULL,
      `rollback_info` longblob NOT NULL,
      `log_status` int(11) NOT NULL,
      `log_created` datetime NOT NULL,
      `log_modified` datetime NOT NULL,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 
    
  5. 在主服务中添加seata全局事务注解@GlobalTransactional

    调用其他服务模块的称为主服务方,即第一个开启事务的

    /**
     * 新增spu信息
     */
    @Override
    @GlobalTransactional(rollbackFor = Exception.class) // 重点:第一个开启事务的需要添加seata全局事务注解
    public void insertSpuInfo(ProductDTO productDTO)
    {
        log.info("Seata全局事务开始..........全局事务id = " + RootContext.getXID());
     		// ..... 
    }  
    
  6. 在其他服务中添加事务注解

    被调用的服务模块都是,即非第一个开启事务的

    /**
     * 新增商品阶梯价格
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 一定要使用REQUIRES_NEW,开启新事务
    public int insertSkuLadder(SkuLadder skuLadder)
    {
      	log.info("在分支事务中获取全局事务id = " + RootContext.getXID());
        return skuLadderMapper.insertSkuLadder(skuLadder);
    }
    
    /**
     * 新增商品满减信息
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 一定要使用REQUIRES_NEW,开启新事务
    public int insertSkuFullReduction(SkuFullReduction skuFullReduction)
    {
        return skuFullReductionMapper.insertSkuFullReduction(skuFullReduction);
    }
    
    /**
     * 新增商品会员价格
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 一定要使用REQUIRES_NEW,开启新事务
    public int insertMemberPrice(MemberPrice memberPrice)
    {
        return memberPriceMapper.insertMemberPrice(memberPrice);
    }
    
    /**
     * 新增商品spu积分设置
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 一定要使用REQUIRES_NEW,开启新事务
    public int insertSpuBounds(SpuBounds spuBounds)
    {
        return spuBoundsMapper.insertSpuBounds(spuBounds);
    }
    
  7. 测试

    当出现异常时会回滚。

    设置断点,在Seata控制台界面查看事务信息。

7. 交互流程梳理

Seata涉及到三个角色之间的交互,通过流程图将AT模式下的TM、RM、TC的交互流程梳理一下。

假设有三个微服务,分别是服务A、B、C,其中服务A中调用了服务B和服务C,TM、TC、RM三者之间的交互流程如下:

  1. 服务A启动时,GlobalTransactionScanner会对有@GlobalTransaction注解的方法进行AOP增强,并生成代理,增强的代码位于GlobalTransactionalInterceptor类中,当调用@GlobalTransaction注解的方法时,增强代码首先向TC注册全局事务,表示全局事务的开始,同时TC生成XID,并返回给TM;

  2. 服务A中调用服务B时,将XID传递给服务B;

  3. 服务B得到XID后,访问TC,注册分支事务,并从TC获得分支事务ID,TC根据XID将分支事务与全局事务关联;

  4. 接下来服务B开始执行SQL语句,在执行前将表中对应的数据保存一份,执行后再保存一份,将这两份记录作为回滚记录写入到数据库中,如果执行过程中没有异常,服务B最后将事务提交,并通知TC分支事务成功,服务B也会清除本地事务数据;

  5. 服务A访问完服务B后,访问服务C;

  6. 服务C与TC之间的交互与服务B完全一致;

  7. 服务B和服务C都成功后,服务A通过TM通知TC全局事务成功,如果失败了,服务A也会通知TC全局事务失败;

  8. TC记录了全局事务下的每个分支事务,TC收到全局事务的结果后,如果结果成功,则通知RM成功,RM收到通知后清理之前在数据库中保存的回滚记录,如果失败了,则RM要查询出之前在数据库保存的回滚记录,对之前的SQL操作进行回滚。

因为TM、RM、TC之间的交互都是通过网络完成的,很容易出现网络断开的情况,因此TC提供了四个定时线程池,定时检测系统中是否有超时事务、异步提交事务、回滚重试事务、重试提交事务,如果发现了有这四类事务,则从全局事务中获取所有的分支事务,分别调用各个分支事务完成对应的操作,依次来确保事务的一致性。

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值