2
0

微服务基础

2026-02-17
2026-07-23

视频链接: Spring Cloud 快速通关

采用 JDK21 实现,更推荐使用 JDK17。使用 JDK17 时,需要额外调整 pom 文件配置与相关 API。

0.微服务入门

微服务就是把一个大项目拆成多个小服务,每个服务负责一块独立业务,例如订单服务、商品服务、支付服务、库存服务。每个服务可以单独开发、单独启动、单独部署,也可以有自己的数据库。微服务的好处是项目更容易扩展、维护和部署,某个服务压力大时可以单独加机器,不需要整体扩容。

微服务带来的核心问题是:服务变多以后,服务之间要互相调用,所以需要解决服务注册发现、远程调用、负载均衡、配置管理、限流熔断、网关路由、分布式事务等问题。Spring Cloud Alibaba 就是一套解决这些问题的微服务组件集合,常见组件包括 Nacos、OpenFeign、Sentinel、Gateway、Seata。

单体项目中,方法调用通常是本地调用,例如 orderService.createOrder();微服务项目中,订单服务和商品服务可能是两个独立进程,甚至部署在不同机器上,所以订单服务要通过 HTTP 请求调用商品服务接口,这就是远程调用。

  • 单体架构:所有功能打包在一个应用中,部署简单,但扩展性与维护成本随业务增长而上升。

  • 集群架构:同一应用多实例部署,通过负载均衡提升吞吐与可用性,但仍是”单体”形态。

  • 分布式 / 微服务架构:按业务拆分为多个服务独立部署,服务之间通过远程调用协作,需配套 注册中心配置中心链路追踪网关 等基础设施。

教学和实际项目中都很常见的一种微服务工程结构

 cloud-demo
├── pom.xml                      # 父工程:统一版本管理
├── services
│   ├── pom.xml                  # 管理所有业务服务的公共依赖
│   ├── service-product
│   │   ├── pom.xml
│   │   └── src/main/java        # 商品服务代码
│   ├── service-order
│   │   ├── pom.xml
│   │   └── src/main/java        # 订单服务代码
│   └── service-xxxx
│       ├── pom.xml
│       └── src/main/java

这是一种 Maven 多模块微服务工程结构。cloud-demo 是父工程,主要负责统一锁定 Spring Boot、Spring Cloud、Spring Cloud Alibaba 等版本;services 是业务服务聚合模块,用来管理多个微服务的公共依赖;service-product、service-order 等是真正的微服务模块,每个模块都是一个可以独立启动、独立注册、独立部署的 Spring Boot 应用。这样做的好处是:代码统一管理,依赖版本统一控制,各个微服务又能保持独立运行。分布式强调多个服务通过网络协作;微服务是分布式架构的一种具体实现方式,强调按业务拆分服务。图中的结构属于微服务项目的多模块工程结构。

1. Nacos

1.1 简介与下载

Nacos 是 Dynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 是 Spring Cloud Alibaba 中常用的注册中心和配置中心。注册中心负责记录“有哪些服务、每个服务在哪个 IP 和端口”;配置中心负责把配置统一放到 Nacos 中管理,让服务启动时或运行时从 Nacos 读取配置。

Nacos 解决两个问题:第一,服务地址不能写死。商品服务可能部署多台机器,端口也可能变化,订单服务不应该固定写 localhost:8001;第二,配置不应该散落在每个项目里。数据库地址、开关配置、限流参数等可以放到配置中心统一管理。

Nacos(Dynamic Naming and Configuration Service):提供 动态服务发现配置管理 与 服务管理 平台。

安装启动

  • 下载:Nacos 安装包(如 2.4.3)
  • 启动(单机):startup.cmd -m standalone
  • 控制台:http://localhost:8848/nacos

1.2 服务注册

服务注册就是微服务启动后,把自己的服务名、IP、端口等信息上报给 Nacos。例如商品服务启动后注册为 service-product,订单服务启动后注册为 service-order。Nacos 保存这些服务实例信息,其他服务需要调用时,就可以通过服务名找到可用地址。

Nacos 在这里充当 注册中心。每个微服务启动后,会把自己的服务名、IP、端口注册到 Nacos。例如商品服务启动后注册为 service-product,订单服务启动后注册为 service-order。注册中心保存的是“服务名和服务实例地址的映射关系”。

服务注册解决的问题是:调用方不需要写死目标服务的 IP 和端口。以前如果订单服务要调用商品服务,可能要写 http://localhost:8001/product/1;但微服务中商品服务可能有多个实例,端口可能变化,所以不能写死地址。注册到 Nacos 后,订单服务只需要知道服务名 service-product,具体访问哪个实例由服务发现和负载均衡完成。

基本使用步骤是:引入 spring-cloud-starter-alibaba-nacos-discovery 依赖,在配置文件中配置 Nacos 地址,然后启动服务。服务启动后会自动向 Nacos 注册。

spring:
  cloud:
    nacos:
      # 配置 Nacos 地址
      server-addr: 127.0.0.1:8848

spring.application.name 很重要,它就是当前服务注册到 Nacos 里的服务名。以后其他服务远程调用时,不直接写 IP,而是写这个服务名。 查看注册中心效果,访问 http://localhost:8848/nacos/ 测试集群模式启动:单机情况下通过改变端口号模拟微服务集群,例如添加 Program arguments 信息为 --server.port=8001

1.3 服务发现

服务发现就是调用方根据服务名,从 Nacos 获取目标服务的实例列表。例如订单服务想调用商品服务,就先通过 service-product 找到商品服务的 IP 和端口。

服务发现的核心流程是:服务提供者启动后注册到 Nacos;服务消费者调用前根据服务名查询实例列表;如果有多个实例,负载均衡组件会从中选择一个实例;最终发起 HTTP 请求。

  1. 开启服务发现,在主启动类上添加 @EnableDiscoveryClient 注解
  2. 测试两款 API 的服务发现功能:DiscoveryClientNacosServiceDiscovery。前者为 Spring 提供的服务发现标准接口,后者由 Nacos 提供。

1.4 远程调用

远程调用就是一个服务通过 HTTP 请求调用另一个服务的接口。例如订单服务需要商品信息,就请求商品服务的 /product/{id} 接口。它和普通浏览器请求接口本质一样,只是请求方从“浏览器”变成了“另一个后端服务”。

远程调用的关键不是会不会发 HTTP,而是目标地址怎么确定。如果写死 http://localhost:8001/product/1,服务扩容或换端口后就会失效;如果写 http://service-product/product/1,再配合注册中心和负载均衡,就能自动找到可用的商品服务实例。

远程调用基本流程:

远程调用基本流程 订单服务调用商品服务时,不是直接写死商品服务地址,而是先通过注册中心找到商品服务的可用地址列表。流程是:商品服务启动后先注册到 Nacos;订单服务需要调用商品服务时,根据服务名向 Nacos 查询商品服务实例列表;Nacos 返回多个可用地址;订单服务通过负载均衡选择其中一个地址;然后真正发送 HTTP 请求到商品服务;商品服务处理后返回数据。

核心要点是:注册中心不处理业务请求,只负责保存和提供服务地址。真正处理 /product/{id} 请求的是商品服务本身。Nacos 的作用类似服务地址表,订单服务先查表,再发请求。

 ①手动拼 URL(DiscoveryClient)   典型思路:

  1. DiscoveryClient.getInstances("service-product") 拿到实例列表
  2. 选一个实例拿到 host:port
  3. 拼接 URL 后用 RestTemplate.getForObject 发请求

要点:

  • getForObject 常见 GET 方法:
    • 参数 1:URL
    • 参数 2:返回体映射的目标类型(例如 Product.class
private Product getProductFromRemote(Long productId) {  
    //1.获取到商品服务所在的所有机器IP+port  
    List<ServiceInstance> instances = discoveryClient.getInstances("service-product");  
    ServiceInstance instance = instances.get(0);  
    //2.拼接请求地址(远程请求地址)  
    String url = "http://"+instance.getHost()+":"+instance.getPort()+"/product/"+productId;  
    log.info("远程请求地址:{}",url);  
    //3.发送请求  
    RestTemplate 是 Spring 提供的“HTTP 客户端工具”,用来在 Java 代码中发送 HTTP 请求,调用别人写好的接口,并把返回的 JSON/XML 自动转成 Java 对象。在微服务中,它用于服务之间的远程调用。
  /*  getForObject() 是 RestTemplate 中最常用的 GET 请求方法之一,有两个核心参数:  
    第一个参数 url:要请求的接口地址(比如 http://localhost:9000/product/1);  
    第二个参数 Product.class:指定返回数据要转换成的目标类型(这里是 Product 实体类)*/  
  /*Product.class: Java 的泛型擦除机制,让程序运行时无法自动推断出要转换成的具体类型,  
    必须通过 Product.class 明确告诉 RestTemplate 要把返回的 JSON 数据转换成哪个类的对象。*/  
    Product product = restTemplate.getForObject(url, Product.class);  
    return product;  
}

② LoadBalancerClient

典型思路:

  1. loadBalancerClient.choose("service-product") 选择实例
  2. 拼接 URL 后用 RestTemplate 发请求
private Product getProductFromRemoteWithLoadBalance(Long productId) {  
    ServiceInstance choose = loadBalancerClient.choose("service-product");  
    String url = "http://" + choose.getHost() + ":" + choose.getPort() + "/product/" + productId;  
    log.info("远程请求地址:{}", url);  
    return restTemplate.getForObject(url, Product.class);  
}

1.5 负载均衡

负载均衡就是当同一个服务有多个实例时,从多个实例中选一个来处理请求。例如商品服务启动了 8001、8002、8003 三个实例,订单服务每次调用时不应该永远访问 8001,而应该按一定规则分配到不同实例上。

使用 LoadBalancerClient 实现

注入 LoadBalancerClient,调用其 choose() 方法,传入服务名,实现负载均衡。

使用 @LoadBalanced 注解实现

在配置类中向 Spring 容器添加 RestTemplate 的 Bean,在 Bean 方法上添加 @LoadBalanced 注解,使用 RestTemplate 进行远程调用时,修改传入的 URL 为服务名,服务名拼 URL,框架自动通过注册中心拿实例列表、通过负载均衡策略选择实例、发起请求完成实例选择与负载均衡。比如:

@Configuration  //告诉 Spring:这个类里定义了 Bean,启动时要处理,没它,Spring 不会扫描这个类,里面的 @Bean 不会生效
public class OrderConfig {

    @LoadBalanced // 注解式负载均衡
    @Bean //Spring 注解,表示方法的返回值要注册成 Spring 容器里的 Bean
    public RestTemplate restTemplate() {//一个普通 Java 方法,返回一个 `RestTemplate` 对象
        return new RestTemplate();//因为 @Bean 修饰后,Spring 会调用这个方法,把返回值当成 Bean 存到容器
    }
}
private Product getProductFromRemoteWithLoadBalancerAnnotation(Long productId) {
    // 给远程发送请求:service-product 会被动态替换
    String url = "http://service-product/product/" + productId;
    log.info("远程请求: {}", url);
    // 给远程发送请求
    return restTemplate.getForObject(url, Product.class);
}

此时底层会将服务名替换为负载均衡后的目标 URL。

1.6 注册中心宕机还能不能调用

经典面试题:如果注册中心宕机,远程调用是否可以成功?

远程调用步骤

远程调用不是每一次都必须实时访问注册中心。调用方通常会缓存一份服务实例列表,所以如果之前已经调用过商品服务,注册中心短时间宕机时,订单服务可能还能根据本地缓存继续调用商品服务。

但是要分情况看。第一种:从来没调用过目标服务,此时本地没有缓存,第一次调用必须去 Nacos 获取服务地址,如果注册中心宕机,就会失败。第二种:之前调用过目标服务,本地有服务地址缓存,注册中心宕机后仍可能继续调用成功。第三种:注册中心和商品服务都宕机,即使本地缓存了地址,请求也会因为目标服务不可用而失败,常见错误是连接拒绝或超时。

所以面试题可以这样答:注册中心宕机不一定导致远程调用立刻失败,关键看调用方是否已有服务实例缓存,以及目标服务本身是否还存活。

1.7配置中心

如果父模块或公共模块中引入了 spring-cloud-starter-alibaba-nacos-config,子模块 service-orderservice-product 都会继承配置中心依赖。Spring Boot 启动时会认为当前服务要从 Nacos 配置中心导入配置,因此每个独立服务都需要在自己的 application.properties 中配置 spring.config.import=nacos:...

如果 order 写了 spring.config.import,而 product 没写,那么 order 可以启动,product 会报 Add a spring.config.import=nacos: 错误。

解决方式:

  1. 当前服务要用配置中心:添加 spring.config.import=optional:nacos:service-product.properties
  2. 当前服务不用配置中心:添加 spring.cloud.nacos.config.import-check.enabled=false
  3. 更推荐:不要把 nacos-config 放到所有服务继承的公共依赖中,哪个服务需要配置中心,哪个服务单独引入。

① @Value + @RefreshScope

  • 配合 @RefreshScope 让 Bean 在配置变化后刷新,加在配置类上激活配置刷新
  • 适合少量配置项
  • 用 @Value("${xx}") 注入配置
@RefreshScope  
@RestController  
public class OrderController {  
    @Value("${order.timeout}")  
    String orderTimeout;  
    @Value("${order.auto-confirm}")  
    String orderAutoConfirm;  
  
    @GetMapping("/config")  
    public String config() {  
        return "orderTimeout:" + orderTimeout + ", orderAutoConfirm:" + orderAutoConfirm;  
    }  
}

② @ConfigurationProperties(推荐):

  • 将配置批量绑定到属性对象中
  • 使用时注入该配置类对象读取
  • 通常比零散的 @Value 更易维护无需 @RefreshScope 即可自动刷新。
  • 定义配置类:@ConfigurationProperties(prefix = "order")
@Component  
@ConfigurationProperties(prefix = "order")  
@Data  
public class OrderProperties {  
    String timeout;  
    String autoConfirm;  
    String dbUrl;  
}

③ NacosConfigManager 监听(自定义逻辑):

目的:

  • 项目启动后动态监听配置文件变化
  • 变化后拿到新值并执行自定义逻辑(例如通知、刷新缓存等)
  • 在启动类中使用 NacosConfigManager 获取 ConfigService
  • 为指定的 dataId + group 添加 Listener
  • 在回调中处理变化后的配置内容
@Bean
ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager) {

    // ApplicationRunner 是 Spring Boot 提供的启动后执行接口
    // 这个 Bean 会在项目启动完成后自动执行
    return args -> {

        // 从 NacosConfigManager 中获取 Nacos 的配置服务对象
        // ConfigService 专门用来操作 Nacos 配置中心
        ConfigService configService = nacosConfigManager.getConfigService();

        // 给 Nacos 中的某个配置文件添加监听器
        // 参数1:dataId,配置文件名
        // 参数2:group,配置分组
        // 参数3:Listener,监听器对象
        configService.addListener(
                "service-order.properties",
                "DEFAULT_GROUP",

                // 匿名内部类:现场创建一个 Listener 接口的实现类对象
                new Listener() {

                    // 指定监听器回调方法使用哪个线程池执行
                    @Override
                    public Executor getExecutor() {
                        // 创建一个固定大小为 4 的线程池
                        return Executors.newFixedThreadPool(4);
                    }

                    // 当 Nacos 配置发生变化时,会自动调用这个方法
                    // configInfo 就是变化后的最新配置内容
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        System.out.println("配置变化:" + configInfo);

                        // 这里模拟配置变化后发送邮件通知
                        System.out.println("邮件通知");
                    }
                }
        );
    };
}

面试题:配置优先级 思考:Nacos中的数据集和application.properties有相同的配置项,哪个生效? 如果存在多个相同的配置信息,那么:

配置信息优先级

  • 生效配置进入环境变量,应用从环境变量中获取最终值。

1.8 数据隔离

一个项目通常部署在多套环境上,比如 dev、test、prod。

项目中每个微服务的配置信息在每套环境上的值可能不一样,要求项目可以通过切换环境,加载本环境的配置。

如果要完成以上需求,其中的难点是如何:

  • 区分多套环境
  • 区分多种微服务
  • 区分多种配置
  • 按需加载配置

Nacos数据隔离解决方案

实际开发时,一般用 Spring Boot 的 profile 激活当前环境,然后让服务去 Nacos 中加载对应 Namespace 下的配置。例如开发环境加载 dev 命名空间,测试环境加载 test 命名空间,生产环境加载 prod 命名空间。这样同一个服务在不同环境中可以使用不同配置。

Nacos 的解决方案:

  • Namespace:用名称空间区分多套环境(dev / test / prod)
  • Group:用 Group 区分多种微服务
  • Data ID:用 Data-id 区分多种具体配置(如 common.propertiesdatabase.properties
  • 使用 SpringBoot 激活对应环境的配置
spring.cloud.nacos.config.namespace=xxx

用来指定 Nacos 配置中心使用的命名空间 ID
如果不设置,默认加载的是 public 命名空间下的配置。 spring.config.import 是 Spring 官方提供的加载外部配置源的标准方式。
Spring Cloud Alibaba 遵循这套官方规范,未来会统一收口到只支持这一种方式。

application.yml 多环境配置:


# 当前服务公共配置:无论 dev/test/prod 都会生效
spring:
  profiles:
    # 当前启动环境:dev / test / prod
    # 改这里,就会切换下面不同环境的配置段
    active: dev

  application:
    # 当前服务名,也会注册到 Nacos
    name: service-order

  cloud:
    nacos:
      # Nacos 地址:注册中心、配置中心都连这个
      server-addr: 127.0.0.1:8848
      config:
        # 当前环境对应的 Nacos 命名空间
        # active=dev 时,相当于 namespace=dev
        # 注意:实际项目里 namespace 常填 Nacos 生成的命名空间 ID
        namespace: ${spring.profiles.active:public}

---
# dev 环境生效:active=dev 时才加载这一段
spring:
  config:
    import:
      # 启动时去 Nacos 读取 order 分组下的 common.properties
      - optional:nacos:common.properties?group=order

      # 启动时去 Nacos 读取 order 分组下的 application.properties
      - optional:nacos:application.properties?group=order

    activate:
      on-profile: dev

---
# test 环境生效:active=test 时才加载这一段
spring:
  config:
    import:
      - optional:nacos:common.properties?group=order
      - optional:nacos:haha.properties?group=order

    activate:
      on-profile: test

---
# prod 环境生效:active=prod 时才加载这一段
spring:
  config:
    import:
      - optional:nacos:common.properties?group=order
      - optional:nacos:hehe.properties?group=order

    activate:
      on-profile: prod

2. OpenFeign

2.1 简介与使用

OpenFeign,是一种 Declarative REST Client,即声明式 Rest 客户端,与之对应的是编程式 Rest 客户端,比如 RestTemplate。 OpenFeign 是声明式 HTTP 客户端,用来简化微服务之间的远程调用。以前使用 RestTemplate 需要手动拼 URL、手动发送请求;使用 OpenFeign 后,只需要写一个接口,用注解描述要调用哪个服务、哪个路径、什么请求方式,Spring 会自动生成代理对象帮你发送 HTTP 请求。它的核心作用是:把远程 HTTP 调用写得像本地方法调用一样。

OpenFeign 会根据接口上的 @FeignClient@GetMapping@PostMapping 等注解生成代理对象。调用接口方法时,代理对象不会执行普通 Java 方法逻辑,而是根据注解信息组装 HTTP 请求,然后通过服务发现和负载均衡找到目标服务,最后发送请求并把响应结果转换成 Java 对象。 简单流程是:调用 Feign 接口方法;Feign 代理对象解析注解;根据服务名找实例;负载均衡选择实例;发送 HTTP 请求;反序列化响应结果。

注意:有些第三方 API 不需要注册中心,例如查询天气(墨迹天气等)。此时 @FeignClient 的 url 属性可直接指定第三方地址。

2.2 接入步骤

  1. 引入依赖:spring-cloud-starter-openfeign
  2. 启动类添加 @EnableFeignClients(开启 Feign 客户端扫描)
  3. 创建 Feign 接口,使用 @FeignClient(value = "service-product") 指定目标服务
  4. 在接口中使用 Spring MVC 注解定义请求方式与路径
  5. 注入 Feign 接口即可发起远程调用

核心注解:

  • 指定远程地址@FeignClient(value = 目标微服务名称)
  • 指定请求方式@GetMapping@PostMapping@DeleteMapping …
  • 指定携带数据@RequestHeader@RequestParam@RequestBody …
  • 指定结果返回:响应模型类

MVC 两套使用逻辑:

  1. 放到 Controller 上是接受这样的请求
  2. 放到 FeignClient 上是发送这样的请求

OpenFeign的远程调用

使用 OpenFeign 会自动连上注册中心,然后自动负载均衡地挑一个实例发送请求。

2.2 小技巧

如何编写好 OpenFeign 声明式的远程调用接口:

  • 针对业务 API:直接复制对方的 Controller 签名即可;
  • 第三方 API:根据接口文档确定请求如何发

2.3 一道面试题

面试题:客户端负载均衡与服务端负载均衡区别:

  • 客户端负载均衡(如 OpenFeign + LoadBalancer):客户端从注册中心获取实例列表 → 自己选择一个 → 发起调用
  • 服务端负载均衡(如 Nginx):客户端直接请求负载均衡服务器 → 由服务端决定转发给哪个实例

客户端负载均衡与服务端负载均衡

2.4 进阶用法

2.4.1日志

在配置文件中指定 feign 接口所在包的日志级别:

logging:
  level:
    # 指定 feign 接口所在的包的日志级别为 debug 级别
    indi.mofan.order.feign: debug

向 Spring 容器中注册 feign.Logger.Level 对象:

@Bean
public Logger.Level feignlogLevel() {
    // 指定 OpenFeign 发请求时,日志级别为 FULL
    return Logger.Level.FULL;
}

2.4.2超时控制

主要控制两个超时:

  • 连接超时(connectTimeout):商品服务器宕机,连接不上
  • 读取超时(readTimeout):API 速度慢不返回,读取不到

流程:OpenFeign 发起远程调用 → 限时等待 → 未超时返回正确结果 / 超时则中断调用 → 返回错误信息或兜底数据 如果需要修改默认超时时间,在配置文件中进行如下配置:

spring:
  cloud:
    openfeign:
      client:
        config:
          # 默认配置
          default:
            logger-level: full
            connect-timeout: 1000
            read-timeout: 2000
          # 具体 feign 客户端的超时配置
          service-product:
            logger-level: full
            # 连接超时,3000 毫秒
            connect-timeout: 3000
            # 读取超时,5000 毫秒
            read-timeout: 5000

2.4.3重试机制

远程调用超时失败后,还可以进行多次尝试,如果某次成功则返回 ok,如果多次尝试后依然失败则结束调用,返回错误。

OpenFeign 底层默认使用 NEVER_RETRY,即从不重试策略。

向 Spring 容器中添加 Retryer 类型的 Bean:

@Bean
public Retryer retryer() {
    return new Retryer.Default();
    // 参数:period(初始间隔100ms), maxPeriod(最大间隔1s), maxAttempts(最大重试次数5)  
    // 间隔递增:100ms → ×1.5 → ×1.5 → ×1.5 ...  
    // 每次间隔要乘以一个因子
}

这里使用 OpenFeign 的默认实现 Retryer.Default,在这种默认实现下:

public Default() {
    this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}

OpenFeign 的重试规则是:

  • 重试间隔 100ms
  • 最大重试间隔 1s。新一次重试间隔是上一次重试间隔的 1.5 倍,但不能超过最大重试间隔。
  • 最多重试 5 次

2.4.4 拦截器

目的:在每次 Feign 请求发出前,统一添加 Header 或其他处理逻辑(如添加 Token)。 这张图说明 Feign 请求在真正发出去之前,可以经过请求拦截器;响应返回后,也可以经过响应拦截器。请求拦截器常用来统一添加请求头,例如 token、traceId、用户信息、来源标识等。

OpenFeign的拦截器

以请求拦截器为例,自定义的请求拦截器需要实现 RequestInterceptor 接口,并重写 apply() 方法: 通过 @Component 注解 ,实现 RequestInterceptor 接口(全局生效):

package indi.mofan.order.interceptor;

public class XTokenRequestInterceptor implements RequestInterceptor {
    /**
     * 请求拦截器
     *
     * @param template 封装本次请求的详细信息
     */
    @Override
    public void apply(RequestTemplate template) {
        System.out.println("XTokenRequestInterceptor ...");
        template.header("X-Token", UUID.randomUUID().toString());
    }
}

如果把拦截器注册成 Spring Bean,它通常会对当前服务内所有 Feign 客户端生效;如果只在配置文件中给某个 Feign Client 配置,则只对指定客户端生效。开发时要注意作用范围,避免把某个接口专用的请求头加到所有远程调用上。

要想要该拦截器生效有两种方法:

  1. 在配置文件中配置对应 Feign 客户端的请求拦截器,此时该拦截器只对指定的 Feign 客户端生效 ① 不使用@Component 注解,通过配置文件注册拦截器数组(可以在 default 下全局配置,也可以针对特定服务单独配置(更灵活)):

    spring:
      cloud:
        openfeign:
          client:
            config:
              # 具体 feign 客户端
              service-product:
                # 该请求拦截器仅对当前客户端有效
                request-interceptors:
                  - indi.mofan.order.interceptor.XTokenRequestInterceptor
    
  2. 还可以直接将自定义的请求拦截器添加到 Spring 容器中,此时该拦截器对服务内的所有 Feign 客户端生效 ②通过 @Component 注解 ,实现 RequestInterceptor 接口(全局生效):

    @Component
    public class XTokenRequestInterceptor implements RequestInterceptor {
        // --snip--
    }
    

2.4.5Fallback兜底回调

OpenFeign的Fallback

Fallback,即兜底返回。 目的:在远程调用失败(超时/错误)后返回默认兜底数据,让业务继续往下走。 注意,此功能需要整合 Sentinel 才能实现。

因此需要先导入 Sentinel 依赖:

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

并在需要进行 Fallback 的服务的配置文件中开启配置: ① 开启 Sentinel 支持:

feign:
  sentinel:
    enabled: true

现在需要对 Feign 客户端 ProductFeignClient 配置 Fallback,那么需要先实现 ProductFeignClient 编写兜底返回逻辑,并将其交由 Spring 管理: ③ 编写 Fallback 实现类,实现 FeignClient 接口,返回兜底数据。

@Component
public class ProductFeignClientFallback implements ProductFeignClient {
    @Override
    public Product getProductById(Long id) {
        System.out.println("Fallback...");
        Product product = new Product();
        product.setId(id);
        product.setPrice(new BigDecimal("0"));
        product.setProductName("未知商品");
        product.setNum(0);
        return product;
    }
}

之后回到对应的 Feign 客户端,配置 Fallback: ② 在 @FeignClient 中指定 fallback 类:

@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {

    @GetMapping("/product/{id}")
    Product getProductById(@PathVariable("id") Long id);
}

2.5OpenFeign 小结

  • 核心定位:声明式 HTTP 客户端,用注解代替手写 RestTemplate 调用,自动集成注册中心与负载均衡。
  • 基本使用:引入依赖 → @EnableFeignClients → 创建 @FeignClient 接口 → 注入使用。
  • 进阶配置
    • 日志Logger.Level.FULL + 包级别 debug,调试远程调用详情
    • 超时控制connectTimeout(连接超时)+ readTimeout(读取超时)
    • 重试机制:默认不重试,注册 Retryer.Default 开启(间隔递增,最大重试 5 次)
    • 拦截器:实现 RequestInterceptor,统一添加 Header 等请求前处理
    • Fallback:整合 Sentinel,远程调用失败时返回兜底数据,保障业务可用性

目的:防止服务雪崩 —— 一个服务挂了 → 调用它的服务也跟着卡住 → 再上游的服务也被拖垮 → 像雪崩一样逐层崩溃。

手段 作用
超时控制 不无限等待,到时间就断开,释放线程
重试机制 失败后有限次重试,不行就放弃
Fallback 兜底 调用失败时返回默认数据,不让整条链路崩掉

3. Sentinel

官方文档:Sentinel

Sentinel(分布式系统的流量防卫兵):阿里巴巴开源的 流量控制 与 熔断降级 组件,核心目标是保障微服务在高并发、故障等场景下的 稳定性 与 可用性

Sentinel 是微服务稳定性保护组件,主要用于流量控制、熔断降级、热点参数限流、系统保护等。它解决的问题是:当请求太多、某个服务变慢、某个接口大量报错时,系统不能无限制地继续放行请求,否则可能导致线程耗尽、服务雪崩。

Sentinel 的核心思想是:先把接口、方法或远程调用定义为资源,再给资源配置规则。请求进入资源前,Sentinel 会检查规则;如果没有违反规则就放行,如果违反规则就限流、熔断或执行兜底逻辑。

核心概念:

  • 资源(Resource):Sentinel 保护的目标,可以是一个接口、一段代码或一次远程调用
  • 规则(Rule):针对资源制定的保护策略,如流控规则、熔断规则、热点规则等

安装启动

  • 下载:Sentinel Dashboard(如 1.8.9)
  • 启动:java -jar sentinel-dashboard-1.8.9.jar
  • 控制台:http://localhost:8080(默认账号密码均为 sentinel

3.1 工作原理

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

Sentinel架构原理

定义规则:

  • 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor),所有 Web 接口均为资源。比如在 Spring Web 项目中,Controller 接口会自动被识别为资源,一般不需要手动定义。
  • 编程式:SphU API,在代码中手动用 SphU.entry("资源名") 包住一段业务逻辑,表示这段代码是一个受 Sentinel 保护的资源。适合精确保护某一小段代码。
  • 声明式:@SentinelResource,直接在方法上加注解,把这个方法声明为 Sentinel 资源。写法比 SphU 简洁,常用于保护 Service 方法。

定义资源: 流量控制 FlowRule:限制资源的访问流量,例如限制某接口每秒最多访问 10 次,超过后直接限流,防止瞬时流量把服务打挂。 熔断降级 DegradeRule:当资源出现异常比例高、响应时间过长等情况时,临时熔断该资源,快速失败,避免慢接口或异常接口拖垮整个系统。 系统保护 SystemRule:从整个系统角度保护应用,例如根据系统负载、CPU 使用率、线程数、入口 QPS 等指标进行限流,防止服务器整体过载。 来源访问控制 AuthorityRule:根据调用来源控制是否允许访问,例如只允许某些应用访问该资源,或者禁止某些来源访问。 热点参数 ParamFlowRule:针对请求参数限流,例如商品详情接口中,某个热门商品 ID 被大量访问,可以单独限制这个热点参数的访问频率。

Sentinel工作原理

3.2 整合 Sentinel

启动 Dashboard

前往 Sentinel GitHub Realease 页下载 Sentinel Dashboard,这里选择 1.8.8 版本,因此下载 sentinel-dashboard-1.8.8.jar。 在 sentinel-dashboard-1.8.8.jar 所在的目录运行以下命令,启动 Dashboard:

java -jar sentinel-dashboard-1.8.8.jar

启动完成后,浏览器访问 http://localhost:8080/,默认用户与密码均为 sentinel

服务整合 Sentinel 引入依赖:

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

配置文件中添加:

spring:
  application:
    name: service-product
  cloud:
    sentinel:
      transport:
        # 控制台地址
        dashboard: localhost:8080
      # 立即加载服务  
      eager: true

配置完成后启动对应服务,再前往 Sentinel Dashboard 查看,能够看到对应服务信息。

可以在一个方法上使用 @SentinelResource 注解,将其标记为一个「资源」,当方法被调用时,能够在 Dashboard 的「簇点链路」上找到对应的资源,之后在界面上完成对资源的流控、熔断、热点、授权等操作。

3.3异常处理机制

当请求触发 Sentinel 的保护规则(流控、熔断等)时,会抛出 BlockException。Sentinel 提供了 四种异常处理机制 来捕获和处理这个异常。 核心原则:有兜底回调就走兜底,没有则交给 Spring Boot 全局异常处理。 Sentinel异常处理

3.3.1 Web 接口级(BlockExceptionHandler)

目的:为 Controller 层的 Web 接口提供统一的限流/熔断返回格式,让前端拿到结构化的错误信息。 实现方式:自定义 BlockExceptionHandler,统一返回 JSON 格式。

当 Web 接口作为资源被流控时,默认情况下会在页面显示: <pre> Blocked by Sentinel (flow limiting) </pre>

如果需要自定义异常处理,可以实现 BlockExceptionHandler 接口,并将实现类交给 Spring 管理: 响应类R是自定义的

@Component // 注册为 Spring Bean,自动生效  
public class MyBlockExceptionHandler implements BlockExceptionHandler {  
    @Override  
    public void handle(HttpServletRequest request, HttpServletResponse response,  
                       String resourceName, // 被保护的资源名称(新版新增参数)  
                       BlockException e) throws Exception {  
        // 设置响应类型为 JSON  
        response.setContentType("application/json;charset=utf-8");  
        // 返回 429 状态码(Too Many Requests)  
        response.setStatus(429);  
        // 默认提示信息  
        R result = R.error(429, "请求被限流,请稍后重试");  
        // 根据异常类型返回不同提示  
        if (e instanceof FlowException) {  
            result = R.error(429, "触发流控规则");       // 流控异常  
        } else if (e instanceof DegradeException) {  
            result = R.error(429, "触发熔断降级");       // 熔断异常  
        }  
        // 将结果序列化为 JSON 写入响应  
        response.getWriter().write(new ObjectMapper().writeValueAsString(result));  
    }

/create 接口为例,当其被流控时,页面显示:

{
    "code": 500,
    "message": "/create 被 Sentinel 限制了, 原因: class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
    "data": null
}

3.3.2 @SentinelResource 注解级

目的:标注在 非 Controller 层(如 Service)的方法上,将该方法定义为 Sentinel 资源,并可指定 blockHandler 兜底回调。

  • 违反规则时 → 有 blockHandler 就调用兜底方法 → 没有则向上抛出异常交给全局处理
  • blockHandler 方法必须与原方法在 同一个类 中,且参数列表末尾多一个 BlockException

@SentinelResource 注解标记的资源被流控时,默认返回 500 错误页。

如果需要自定义异常处理,一般可以增加 @SentinelResource 注解的以下任意配置:

  • blockHandler
  • fallback
  • defaultFallback

blockHandler 为例:

@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId) {
    // --snip--
}

在当前类中创建名称为 blockHandler 值的方法,并且返回值类型、参数信息与 @SentinelResource 标记的方法一致(可以额外增加一个 BlockException 类型的参数): // 兜底方法:参数与原方法一致 + 末尾加 BlockException

/**
 * 指定兜底回调
 */
public Order createOrderFallback(Long productId, Long userId, BlockException e) {
    Order order = new Order();
    order.setId(0L);
    order.setTotalAmount(new BigDecimal("0"));
    order.setUserId(userId);
    order.setNickname("未知用户");
    order.setAddress("异常信息: " + e.getClass());
    return order;
}

当资源被流控时,执行 blockHandler 指定的方法:

{
    "id": 0,
    "totalAmount": 0,
    "userId": 666,
    "nickname": "未知用户",
    "address": "异常信息: class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
    "productList": null
}

3.3.3 OpenFeign 整合

与 @SentinelResource 逻辑一致:指定了 fallback 就调用兜底,否则向上抛出直到 Spring Boot 全局处理。

配置详见 3.2.5 Fallback 部分(feign.sentinel.enabled=true + @FeignClient(fallback = ...))。

3.3.4 SphU 硬编码(了解)

通过 SphU.entry("resourceName") 手动编码的方式定义资源和处理异常,底层源码中经常看到。日常业务开发中较少直接使用。

3.4 流控规则

流控,即流量控制(FlowRule),用于限制多余请求,从而保护系统资源不被耗尽。

Sentinel流控

3.4.1 阈值类型

Sentinel设置流控阈值类型

Sentinel 的流控阈值规则有两种:

  1. QPS:Queries Per Second,用于限制资源每秒的请求次数,防止突发流量,应用于高频短时接口(如 API 网关)。当每秒的请求数超过设定的阈值时,就会触发流控。比如上图设置的 QPS = 5,就表示每秒最多允许 5 个请求。
  2. 并发线程数:用于限制同时处理该资源的线程数(即并发数),保护系统资源(线程池),应用于耗时操作(如数据库查询)。当处理该资源的线程数超过阈值时,就会触发流控。比如设置并发线程数为 5,表示最多允许 5 个线程同时处理该资源。
阈值类型 含义 适用场景
QPS(推荐) 每秒查询量,超过阈值的请求被拒绝 大多数接口限流场景
并发线程数 同时处理该资源的线程数,超过阈值的请求排队或拒绝 业务使用线程池、需要保护线程资源时
当勾选「是否集群」时,有两种集群阈值模式可供选择:
  1. 单机均摊:将设置的「均摊阈值」到每个节点。以上图为例,假设集群有 3 个节点,那么每个节点的阈值都是 5;
  2. 总体阈值:整个集群共享设置的「均摊阈值」。假设集群有 3 个节点,这 3 个节点的的总阈值只有 5,比如按 2-2-1 的形式将阈值均摊到每个节点。

3.4.2 流控模式

Sentinel的流控模式

配置流控规则时,可以点击下方的「高级选项」,在这里可以配置「流控模式」,共有三种可选项:

  1. 直接:默认选项,对当前资源直接做限制。举例:给 /order/create 设置 QPS 阈值为 5,每秒超过 5 次请求就触发流控。。
  2. 关联:关联资源超阈值时,限流当前资源。
  3. 链路:根据不同的调用链路来限制同一个资源。仅对于某一路径下的资源访问生效。注意:使用时需要关闭 Sentinel 的 Web 上下文统一合并(spring.cloud.sentinel.web-context-unify=false),否则所有链路会合并成一个。

web-context-unify 是什么意思? Sentinel 默认会把所有 Spring MVC 的 Web 入口(Controller 接口)归到同一个上下文(Context)里。也就是说,不管请求是从 /order/seckill 进来的还是从 /order/normal 进来的,Sentinel 在统计链路时都认为它们是 同一个入口。 这就导致一个问题:链路模式失效举个例子

假设你有两个 Controller 接口,它们内部都调用了同一个 Service 方法 reduceStock()

/order/seckill  →  reduceStock()   ← 想限流这条链路  
/order/normal   →  reduceStock()   ← 不想限流

你在 Sentinel 控制台给 reduceStock 配了链路模式的流控规则,入口资源指定为 /order/seckill

  • web-context-unify=true(默认):Sentinel 把所有 Web 请求合并成一个 context,看不到 /order/seckill 和 /order/normal 是两条不同的链路 → 链路规则不生效,两条路都会被限或都不被限。
  • web-context-unify=false:Sentinel 为每个 Web 入口创建独立的 context,能识别出两条不同链路 → 链路规则正常生效,只限制秒杀链路,普通订单不受影响。
spring:  
  cloud:  
    sentinel:  
      web-context-unify: false  # 关闭上下文合并,让每个入口独立

调用关系包括调用方、被调用方;一个方法又可能会调用其他方法,形成一个调用链路的层次关系;有了调用链路的统计信息,可以衍生出多种流量控制手段。

Sentinel流控模式

维度 直接 关联 链路
作用对象 当前资源本身 关联的其他资源 特定调用链路的入口
触发逻辑 当前资源超阈值 关联资源超阈值时,限流当前资源 从指定入口发起的请求超阈值
核心目的 保护当前资源 保护关联资源或间接限流 按入口细分流量控制
典型场景 独立接口的直接限流 资源依赖(如读操作限流写操作) 区分不同调用来源
配置依赖 无需额外配置 需指定关联资源 需指定资源访问入口

3.4.3 流控效果

打开流控规则中的高级配置后,还可以配置「流控效果」,同样有三种选项:

  1. 快速失败:默认选项。超过阈值直接拒绝,抛出 FlowException。注意,只有该选项支持「流控模式」(直接、关联、链路)的设置。
  2. Warm Up:初始阈值较低(默认是设定阈值的 $\frac{1}{3}$),随后在预热时间内逐步提升至设定阈值。例如设定阈值为 3 QPS、预热时间 3 秒,初始阈值为 1 QPS,3 秒内逐步升至 3。
  3. 排队等待:基于漏桶算法,请求进入队列后按固定间隔时间匀速处理。若请求的预期等待时间超过设定的超时时间,则拒绝请求。

Sentinel流控效果

效果 核心机制 适用场景 阈值动态变化 流量特征
快速失败 直接拒绝超出阈值的请求 明确系统处理能力并快速保护 固定阈值 突发流量
Warm Up 阈值逐步提升 冷启动或流量突增的平滑过渡 动态提升 逐步增长的流量
排队等待 匀速处理请求 服务处理均匀,避免突发压力 固定阈值 均匀的流量

3.5 熔断规则

熔断:当下游服务持续出现问题时,暂时 切断 对它的调用,避免拖垮上游服务,等一段时间后再尝试恢复。 熔断规则,即 DegradeRule。

使用熔断规则可以配置熔断降级,用于:

  • 切断不稳定调用
  • 快速返回不积压
  • 避免雪崩效应

最佳实践: 熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

  • 思考:有无熔断规则显著区别是什么?

    没有熔断:一直硬撑,陪着下游一起死

    有熔断:及时止损,保护自己,等对方恢复了再重新连接

3.5.1 断路器工作原理(三种状态)

状态 行为 转换条件
关闭(Closed) 正常放行所有请求 统计窗口内触发熔断条件 → 转为 打开
打开(Open) 所有请求直接拒绝(或走兜底) 经过熔断时长后 → 转为 半开
半开(Half-Open) 放行一个 探测请求 试探下游是否恢复 探测成功 → 回到 关闭;探测失败 → 回到 打开

熔断降级里的核心组件是「断路器」,其工作原理如下:

断路器工作原理

3.5.2 熔断策略

断路器是机制,熔断策略是触发条件

① 慢调用比例 定义:统计窗口内,响应时间超过设定阈值的请求占比

配置参数:maxSlowCallMs:慢调用阈值(如 1000ms) ratioThreshold:比例阈值(如 0.5,即 50%) statIntervalMs:统计窗口(如 10000ms) 触发条件:在 慢调用阈值 时间内内,请求数 >= 最小请求数,且慢调用比例 >= 比例阈值

配置慢调用比例的熔断规则

在 5000ms 内,有 80%(0.8 的比例阈值)的请求的最大响应时间超过 1000ms,则进行 30s 的熔断。 如果 5000ms 内,请求数不超过 5,就算达到熔断规则,也不进行熔断。

  • 设定 最大 RT(响应时间),超过该时间的调用视为”慢调用”
  • 在统计时长内,慢调用比例超过阈值 → 触发熔断

举例:最大 RT = 1000ms,比例阈值 = 0.8,统计时长 = 10s,最小请求数 = 5。
如果 10s 内有 8 个以上请求,且其中超过 80% 的调用耗时 > 5000ms,则触发熔断。

② 异常比例

定义 统计窗口内,抛出异常的请求占比
配置参数 ratioThreshold:比例阈值(如 0.3,即 30%) <br>statIntervalMs:统计窗口
触发条件 在统计窗口内,请求数 >= 最小请求数,且异常比例 >= 阈值
在远程调用的目标接口里添加 int i = 1 / 0; 模拟远程调用异常。
此时尚未配置任何熔断规则,然后远程调用存在异常的接口,此时会触发使用 OpenFeign 配置的兜底回调。
换句话说,没有配置任何熔断规则可以触发兜底回调,而配置熔断规则也是为了触发兜底回调,那岂不是配不配置熔断规则都可以?

有无熔断规则的比较

当 A 服务向 B 服务发送请求时,远程调用的 B 服务接口中存在异常,此时触发兜底回调。 在这个过程,由 A 服务发送的请求依旧会打到 B 服务上。 而配置熔断规则后,A 服务发送的请求快速失败,立即出发兜底回调,不会再把请求打到 B 服务上。**

配置异常比例的熔断规则

在 5000ms 内,有 80%(0.8 的比例阈值)的请求产生了异常,则进行 30s 的熔断。

  • 在统计时长内,异常请求占比超过阈值 → 触发熔断

举例:比例阈值 = 0.8,统计时长 = 5000ms。
5000ms 内超过 80% 的请求抛出异常,则触发熔断。

③ 异常数

定义 统计窗口内,抛出异常请求总数
配置参数 count:异常数阈值(如 5) <br>statIntervalMs:统计窗口
触发条件 在统计窗口内,异常请求数 >= count

配置异常数的熔断规则

「异常数」的熔断策略与「异常比例」很类似,只不过「异常数」是直接统计异常个数,就算统计时长内产生了一百万个请求,但只要有 10 个请求出现了异常,也会触发熔断。

  • 在统计时长内,异常请求的绝对数量超过阈值 → 触发熔断

举例:异常数阈值 = 10,统计时长 = 5000ms。
5000ms 内出现 10 次以上异常,则触发熔断。

3.6 热点规则

热点参数限流:根据方法参数的具体值来做更细粒度的流控。普通流控是对整个接口限流,热点限流可以针对某个参数的某些值单独设阈值。

所谓热点,即经常访问的数据。很多时候希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。

热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

Sentinel热点规则概述

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

在需求中学习

现有如下需求:

  • 每个用户秒杀 QPS 不得超过 1(秒杀下单时,userId 级别)
  • 6 号用户是 vvip,不限制 QPS(例外情况)
  • 666 号商品是下架商品,不允许访问

在 Sentinel GitHub Wiki 中指出:

  • 目前 Sentinel 自带的 adapter 仅 Dubbo 方法埋点带了热点参数,其它适配模块(如 Web)默认不支持热点规则,可通过自定义埋点方式指定新的资源名并传入希望的参数。注意自定义埋点的资源名不要和适配模块生成的资源名重复,否则会导致重复统计。
模块 是否支持按参数值分别统计
Dubbo 适配器 ✅ 支持(调用 Dubbo 方法时,会把参数值一起统计)
Web 适配器(Controller) ❌ 不支持(只能统计接口总调用次数,不能按参数分别统计)
既然 Web Controller 默认不支持按参数统计,那你可以手动在代码里插一个埋点,自己起个新的资源名,自己把参数传进去,这样就能实现按参数分别统计了。
Sentinel 自动给 Controller 生成了一个资源名(如 GET:/user/{id}),如果你自定义埋点时也用了同一个名字,就会导致:
  • 同一个方法的调用被统计了两次
  • 限流/熔断规则可能会重复触发

必须使用 @SentinelResource 标注资源(热点规则不支持纯 Web 路径资源)

@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", required = false) Long userId,
                     @RequestParam(value = "productId", defaultValue = "1000") Long productId) {
    Order order = orderService.createOrder(productId, userId);
    order.setId(Long.MAX_VALUE);
    return order;
}

public Order seckillFallback(Long userId,
                             Long productId,
                             // 使用 fallback,而不是 blockHandler
                             // 最后一个参数类型是 Throwable,而不是 BlockException
                             Throwable throwable) {
    System.out.println("seckillFallback...");
    Order order = new Order();
    order.setId(productId);
    order.setUserId(userId);
    order.setAddress("异常信息: " + throwable.getClass());
    return order;
}

seckill-order 资源进行如下热点规则配置:

根据需求1配置热点规则

这表示:访问 seckill-order 资源时,第一个参数(参数索引 0)在 1 秒的统计窗口时长下,其阈值为 1,也就是 QPS = 1。

需要注意:携带此参数,则参与流控;不携带不流控。

@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", defaultValue = "888") Long userId,
                     @RequestParam(value = "productId", defaultValue = "1000") Long productId) {
    // --snip--
}

上述代码中,userId 的默认值为 888,也就是以 http://localhost:8000/seckill?productId=777 的形式进行访问时,userId 的值为 888,此时依旧传入了 userId,依旧触发流控。

@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", required = false) Long userId,
                     @RequestParam(value = "productId", defaultValue = "1000") Long productId) {
    // --snip--
}

上述代码中,required = false。userId 可以不传,当以 http://localhost:8000/seckill?productId=777 的形式进行访问时,userIdnull,没有传入 userId,不会触发流控。

经过上述配置,已经完成「每个用户秒杀 QPS 不得超过 1」的需求,但「6 号用户」是个例外:

根据需求2编辑热点规则

访问 seckill-order 资源时,第一个参数(参数索引 0)的类型是 long,当其值为 6 时,限流阈值为 1000000,变相不限制「6 号用户」的 QPS。

现在还有最后一个需求「666 号商品是下架商品,不允许访问」,这其实相当于:对 666 号商品进行流控(限流阈值为 0,不允许访问),对其他商品不进行流控(或阈值非常大)。

新增热点规则:

根据需求3配置热点规则

访问 seckill-order 资源时,第二个参数(参数索引 1)在 1 秒的统计窗口时长下,其阈值为 1000000,这是一个无法达到的值,相当于不进行限流。但有一个例外:当其值为 666 时,限流阈值为 0,也就是不允许访问。

3.7 补充:fallback 与 blockHandler 的区别

属性 blockHandler fallback
触发条件 违反 Sentinel 规则(流控、熔断、热点等)抛出 BlockException 方法执行过程中抛出 业务异常(如 NullPointerException
作用 规则层面的兜底 业务层面的兜底
两者都配置时 Sentinel 规则异常 → blockHandler 优先处理;业务异常 → fallback 优先处理
  • 注意:使用fallback时抛出类型应为Throwable

    为什么 fallback 用 Throwable 而不是 Exception

    因为 Throwable 能接住一切可能抛出的东西(包括 Error 和 Exception),是最”兜底”的类型。Sentinel 的 fallback 设计就是要做到”不管出什么问题都能兜住”,所以参数类型定为 Throwable

3.8 Sentinel 小结

  • 核心定位:流量控制 + 熔断降级组件,防止服务雪崩,保障系统稳定性。
  • 异常处理四种机制
    • BlockExceptionHandler:Web 接口统一返回
    • @SentinelResource + blockHandler:非 Controller 层的方法级兜底
    • OpenFeign fallback:远程调用兜底
    • SphU 硬编码:底层方式(了解)
  • 流控规则
    • 阈值类型:QPS(推荐)/ 并发线程数
    • 流控模式:直接 / 链路 / 关联
    • 流控效果:快速失败 / Warm Up 预热 / 匀速排队
  • 熔断规则:断路器三状态(关闭 → 打开 → 半开),策略有慢调用比例、异常比例、异常数
  • 热点规则:按参数值做精细化限流,适合爆款商品等场景。⚠️ Web 适配模块默认不带热点参数,必须用 @SentinelResource 自定义埋点才能生效
  • fallback vs blockHandlerblockHandler 处理规则异常(末尾参数 BlockException),fallback 处理业务异常(末尾参数 Throwable——Java 异常体系顶级父类,能接住所有异常)
  • ⚠️ 注意:Sentinel 规则默认只存在内存中,重启后丢失,生产环境需配合持久化方案(如 Nacos)。

4. Gateway

Spring Cloud Gateway:微服务架构中的 API 网关,作为所有外部请求的统一入口,提供 路由转发请求过滤负载均衡跨域处理 等功能,是前端与后端微服务之间的”门卫”。

核心概念:

  • 路由(Route):网关的基本构建块,定义了”请求从哪来,转发到哪去”,由 ID、目标 URI、断言和过滤器组成
  • 断言(Predicate):匹配请求的条件(如路径、参数、请求头等),满足条件才会路由到目标服务
  • 过滤器(Filter):在请求转发前后对请求/响应进行修改(如重写路径、添加请求头、鉴权等)

4.1 网关功能与分类

网关是微服务的统一入口,核心功能:

  • 路由转发:根据请求路径将流量分发到不同微服务
  • 负载均衡:结合注册中心,自动在多个服务实例间分配请求
  • 统一鉴权:在网关层统一处理认证和权限校验
  • 跨域处理:集中配置 CORS,无需每个服务单独配置
  • 限流熔断:可整合 Sentinel 在网关层做流量防护 Gateway的概述

网关分类:

类型 代表 说明
响应式网关(推荐) Spring Cloud Gateway 基于 WebFlux + Netty,非阻塞异步模型,高并发性能更优
传统网关 Zuul 1.x 基于 Servlet,阻塞式模型,性能相对较低

4.2 创建网关

4.2.1 接入步骤

  1. 创建 Spring Boot 项目,引入依赖:spring-cloud-starter-gatewayspring-cloud-starter-alibaba-nacos-discovery
  2. 在 application.yml 中配置 Nacos 地址、网关端口等基础信息
  3. 使用 spring.profiles.active=router 激活路由配置文件 application-router.yml(路由规则单独管理,便于维护)
  4. 启动网关服务,通过网关端口统一访问后端微服务

4.3 路由

路由:网关的核心,每条路由定义了一组匹配规则(断言)和目标地址(URI),请求命中规则后被转发到对应微服务。

4.3.1 规则配置

路由规则在配置文件中定义,每条路由包含三个要素:

  • id:路由唯一标识
  • uri:目标服务地址(lb:// 前缀表示从注册中心负载均衡获取实例)
  • predicates:断言条件,决定哪些请求匹配该路由

4.3.2 工作原理

请求到达网关后的处理流程:

  1. 客户端发送请求到网关
  2. Gateway Handler Mapping:遍历所有路由,用断言匹配请求
  3. 匹配成功后,将请求交给 Gateway Web Handler
  4. 请求经过 过滤器链(前置过滤 → 转发到目标服务 → 后置过滤)
  5. 返回响应给客户端 Gateway路由的工作原理

需求:

配置路由规则时,可直接在配置文件中完成:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com
          predicates:
            - Path=/**
          order: 10 # 顺序,越小优先级越高  
          # id 全局唯一
        - id: order-route
          # 指定服务名称
          uri: lb://service-order
          # 指定断言规则,即路由匹配规则
          predicates:
            - Path=/api/order/**
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2

4.4 断言

断言:路由匹配的前提条件。只有请求满足断言规则,才会被路由到目标服务。Gateway 内置了多种断言工厂(如 Path、Query、Header、Method 等),也支持自定义。

断言的两种书写方式:

  • 短写法(简洁):- Path=/api/order/**
  • 长写法(可配置更多参数):使用 name + args 结构
spring:
  cloud:
    gateway:
      routes:
          # id 全局唯一
        - id: order-route
          # 指定服务名称
          uri: lb://service-order
          # 指定断言规则,即路由匹配规则
          # Fully Expanded Arguments
          predicates:
           # 短写法  
            # - Path=/api/order/**  
            # 长写法
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
        - id: product-route
          uri: lb://service-product
          # Shortcut Configuration
          predicates:
            - Path=/api/product/**

在 Spring Cloud Gateway 的实现中,断言的实现都是 RoutePredicateFactory 接口的实现。

因此除了直接查看官方文档外确定有哪些断言形式外,还可以通过查看 RoutePredicateFactory 的实现:

  • HeaderRoutePredicateFactory
  • PathRoutePredicateFactory
  • ReadBodyRoutePredicateFactory
  • BeforeRoutePredicateFactory

断言的名称可以通过去掉实现类名后的 RoutePredicateFactory 来确定,比如 HeaderRoutePredicateFactory 对应名为 Header 的断言。

4.4.2 Query 断言

根据 请求参数 匹配路由,可以指定参数名和参数值(支持正则表达式)。

Query 断言就是根据 URL 里的查询参数(就是 ? 后面那部分)来决定这个请求要不要走这条路由。

举例:请求必须携带参数 q,且值匹配 haha,才会被路由匹配。

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com
          predicates:
            - name: Path
              args:
                patterns: /search
            - name: Query
              args:
                param: q
                regexp: haha

这表示:访问网关的 /search 地址,并且使用了名为 q 的请求参数,且值为 haha,才会将请求转到 https://cn.bing.com

尽管 Gateway 内置了许多断言规则,但依旧难以满足千变万化的需求。

在上述规则的基础上,再指定一个名为 Vip 的断言规则,要求存在名为 user 的请求参数,并且值为 mofan 时才将请求跳转到 https://cn.bing.com

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com
          predicates:
            - name: Path
              args:
                patterns: /search
            - name: Query
              args:
                param: q
                regexp: haha
            - Vip=user,mofan

4.3 自定义断言工厂

内置断言不够用时,可以自定义断言工厂,实现自定义匹配逻辑。

实现三要素:

  1. 类名:必须以 RoutePredicateFactory 结尾,前缀即为配置中使用的名称(如 VipRoutePredicateFactory → 配置名 Vip
  2. 参数映射:重写 shortcutFieldOrder() 方法定义短写法的参数顺序
  3. 断言逻辑:在 apply() 方法中编写匹配规则
  • 配置示例
spring:  
  cloud:  
    gateway:  
      routes:  
        - id: bing-route  
          uri: https://cn.bing.com/  
          predicates:  
            - name: Path  
              args:  
                patterns: /search  
            - name: Query  
              args:  
                param: q  
                regexp: haha  
            # 自定义断言:短写法  
            # - Vip=user,leifengyang  
            # 自定义断言:长写法  
            - name: Vip  
              args:  
                param: user  
                value: leifengyang

自定义 AbstractRoutePredicateFactory 实现类 VipRoutePredicateFactory

// 1️⃣ 类名必须以 RoutePredicateFactory 结尾  
//    前缀 "Vip" 就是配置文件中使用的断言名称(- Vip=user,leifengyang)  
// 2️⃣ 必须加 @Component 注册为 Spring Bean,Gateway 才能自动发现  
@Component  
public class VipRoutePredicateFactory  
        extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {  
    // 继承 AbstractRoutePredicateFactory,泛型是内部配置类 Config  
  
    // 构造方法:告诉父类配置类的类型  
    public VipRoutePredicateFactory() {  
        super(Config.class);  
    }  
  
    // 3️⃣ 核心方法:编写断言匹配逻辑  
    //    返回一个 Predicate,Gateway 每次收到请求都会调用 test() 判断是否匹配  
    @Override  
    public Predicate<ServerWebExchange> apply(Config config) {  
        return new GatewayPredicate() {  
            @Override  
            public boolean test(ServerWebExchange serverWebExchange) {  
                // 从请求中获取 ServerHttpRequest 对象  
                ServerHttpRequest request = serverWebExchange.getRequest();  
  
                // 根据配置的参数名(config.param),从 URL 查询参数中取值  
                // 例如:localhost/search?user=leifengyang → 取 "user" 参数的值  
                String first = request.getQueryParams().getFirst(config.param);  
  
                // 判断:参数不为空 且 参数值等于配置的期望值(config.value)  
                // 满足条件返回 true → 路由匹配;不满足返回 false → 路由不匹配  
                return StringUtils.hasText(first) && first.equals(config.value);  
            }  
        };  
    }  
  
    // 4️⃣ 专门为短写法准备的方法  
    //    定义短写法中参数的顺序:- Vip=user,leifengyang  
    //    第1个值 "user" 映射到 Config.param  
    //    第2个值 "leifengyang" 映射到 Config.value  
    @Override  
    public List<String> shortcutFieldOrder() {  
        return Arrays.asList("param", "value");  
    }  
  
    // 5️⃣ 配置类:承载从 YAML 中读取的断言参数  
    //    Gateway 会自动把配置文件中的 args 或短写法的值注入到这个对象中  
    @Validated  
    public static class Config {  
        @NotEmpty  
        private String param;   // 要检查的请求参数名,如 "user"  
        @NotEmpty  
        private String value;   // 期望的参数值,如 "leifengyang"  
        // getter / setter 省略  
    }  
}

然后访问 http://localhost/search?q=haha&user=mofan 时,会跳转到 Bing 搜索 haha

4.5 过滤器

过滤器:在请求被路由之前或之后,对请求/响应进行处理。可以实现路径重写、添加请求头/响应头、鉴权、日志记录等功能。

Gateway 过滤器分为三个层级:

过滤器类型 作用范围 配置方式
路由过滤器filters 只对当前路由生效 配置在某条路由的 filters 下
默认过滤器default-filters 对所有路由生效 配置在 spring.cloud.gateway.default-filters 下
全局过滤器GlobalFilter 对所有路由生效 编写 Java 类实现 GlobalFilter 接口

先前在网关中配置了将 /api/order/ 开头的请求转到 service-order 服务,并要求在 service-order 服务中也存在 /api/order/ 开头的请求路径,比如 /api/order/readDb。如果该服务中原先并不存在 /api/order/ 开头的请求,比如只有 /readDb,那么在以 /api/order/readDb 进行访问就会出现 404 错误。

为了解决这个问题,可以在 service-order 服务对应的 Controller 上添加 基准路径@RequestMapping("/api/order") 注解,但这并不是最佳方案,如果能直接在网关层面解决这个问题就好了,就像把 /api/order/readDb 重写为 /readDb

Gateway 中内置了许多过滤器,其中有一个常用的过滤器名为:RewritePath,即路径重写。

RewritePath过滤器

spring:
  cloud:
    gateway:
      routes:
          # id 全局唯一
        - id: order-route
          # 指定服务名称
          uri: lb://service-order
          # 指定断言规则,即路由匹配规则
          # Fully Expanded Arguments
          predicates:
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true
          filters:
            # 类似把 /api/order/a/bc 重写为 /a/bc,移除路径前的 /api/order/
            - RewritePath=/api/order/?(?<segment>.*), /$\{segment}
          order: 1
        - id: product-route
          uri: lb://service-product
          # Shortcut Configuration
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*), /$\{segment}
          order: 2

默认过滤器

如果需要为所有路由都添加同一个过滤器,则可以使用 默认过滤器,比如:

spring:
  cloud:
    gateway:
      default-filters:
        # 为所有路由添加响应头过滤器
        - AddResponseHeader=X-Response-Abc, 123

全局过滤器

除了默认过滤器,全局过滤器也能为所有匹配的路由添加一个过滤器,全局过滤器的配置无需修改配置文件。

实现 GlobalFilter 接口,并将实现类交由 Spring 管理,即可实现全局过滤器。

还可以实现 Ordered 接口,调整多个全局过滤器的执行顺序。

/**
 * @author mofan
 * @date 2025/5/1 13:49
 */
@Slf4j
@Component
public class RtGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String uri = request.getURI().toString();
        long start = System.currentTimeMillis();
        log.info("请求 [{}] 开始,时间:{}", uri, start);
        return chain.filter(exchange)
                .doFinally(res -> {
                    long end = System.currentTimeMillis();
                    log.info("请求 [{}] 结束,时间:{},耗时:{}ms", uri, start, end - start);
                });
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

自定义过滤器工厂

尽管 Gateway 内置了许多过滤器,但仍有无法满足需求的情况,此时就需要自定义过滤器工厂。

与自定义断言类似,自定义过滤器工厂的类名也有限制,要求以 GatewayFilterFactory 结尾,而配置文件中配置的名称就是类名开头。

比如需要在配置文件中定义名为 OnceToken 的过滤器,那么需要新增 OnceTokenGatewayFilterFactory

/**
 * @author mofan
 * @date 2025/5/1 14:24
 */
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();

            String value = switch (config.getValue().toLowerCase()) {
                case "uuid" -> UUID.randomUUID().toString();
                case "jwt" -> "Test Token";
                default -> "";
            };

            HttpHeaders headers = response.getHeaders();
            headers.add(config.getName(), value);
        }));
    }
}
spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://service-order
          filters:
            # 自定义过滤器
            - OnceToken=X-Response-Token, uuid

4.5 全局跨域

**> 跨域(CORS):浏览器的同源策略限制,前端页面无法直接请求不同域名/端口的接口。微服务架构下每个服务端口不同,跨域问题更突出。

  • 单体项目:在 Controller 上加 @CrossOrigin 注解
  • 微服务架构:每个服务都配置太麻烦 → 直接在 网关层统一配置跨域,一次配置全部搞定** 如果需要配置跨域,可以在 Controller 的类上添加 @CrossOrigin 注解。 如果有许多 Controller,逐一添加注解太麻烦,可以在项目的配置类中添加 CorsFilter 类型的 Bean。 上述方法只适用于单体服务,那如果在微服务中呢? 借由 Gateway 的功能,可以在配置文件中轻松完成微服务的跨域配置:
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: '*'
            allowed-headers: '*'
            allowedMethods: '*'

之后在请求的 Response Headers 中会增加一些允许跨域的信息。

4.6 面试题:微服务之间的调用经过网关吗?

默认不经过。
网关是对接前端的统一入口,后端微服务之间通过 OpenFeign / RestTemplate 直接调用即可。
如果强制走网关也可以,但多了一跳没有实际意义(了解即可)。

4.7 Gateway 小结

  • 核心定位:微服务的统一入口,基于 WebFlux 的响应式 API 网关,提供路由转发、过滤、负载均衡、跨域等能力。
  • 三大核心组件
    • 路由(Route):定义请求的匹配规则与目标地址(uri: lb://服务名 表示负载均衡转发)
    • 断言(Predicate):路由的匹配条件,内置 Path、Query、Header 等,支持自定义断言工厂
    • 过滤器(Filter):请求/响应的处理链,实现路径重写、添加 Header、鉴权、日志等
  • 过滤器三个层级
    • 路由过滤器(filters):仅对单条路由生效
    • 默认过滤器(default-filters):对所有路由生效(配置方式)
    • 全局过滤器(GlobalFilter):对所有路由生效(编码方式,可写任意逻辑)
  • 自定义扩展:自定义断言工厂(继承 AbstractRoutePredicateFactory)、自定义过滤器工厂(继承 AbstractNameValueGatewayFilterFactory
  • 全局跨域:在网关层统一配置 CORS,无需每个微服务单独处理
  • 微服务间调用不经过网关,网关只面向外部(前端)请求

5. Seata

Seata(Simple Extensible Autonomous Transaction Architecture):阿里巴巴开源的 分布式事务 解决方案,核心目标是在微服务架构下保障跨服务的数据一致性。当一次业务操作涉及多个微服务(如下单同时扣库存、扣余额),Seata 确保它们 要么全部成功,要么全部回滚

Seata 是分布式事务解决方案。它解决的问题是:一个业务操作涉及多个微服务、多个数据库时,如何保证它们要么全部成功,要么全部回滚。

原有一个事务必须基于同一个数据库连接(Connection),一个项目 → 一个数据库 → 一个 Connection → 一个事务搞定所有操作。 每个服务用自己的数据库连接,无法用本地事务统一管理。 Java Web 的本地事务确实基于一个 Connection 管理。但微服务架构中,一个业务可能涉及多个数据库(图里的场景),每个库都有自己的连接,无法用单连接事务统一管理。解决办法是用分布式事务框架(如 Seata)或消息队列保证最终一致性。 例如购买商品时,可能要生成订单、扣库存、扣账户余额。这三个操作可能分别属于订单服务、库存服务、账户服务,并且连接不同数据库。如果订单创建成功,但扣库存失败,就会出现数据不一致。Seata 就是为了解决这种跨服务、跨数据库事务一致性问题。

核心概念:

  • TC(Transaction Coordinator / 事务协调器):Seata 服务器本身,负责维护全局事务和分支事务的状态,驱动全局提交或回滚
  • TM(Transaction Manager / 事务管理器):事务的发起方(通常是调用入口的微服务),负责定义全局事务的范围——开始、提交或回滚全局事务
  • RM(Resource Manager / 资源管理器):事务的参与方(每个被调用的微服务),负责管理分支事务,向 TC 注册并汇报分支事务状态
  • 全局事务:由 TM 发起的一次完整业务操作,包含多个分支事务
  • 分支事务:全局事务中每个微服务各自执行的本地事务(例如:订单服务创建订单是一个分支,商品服务扣库存是另一个分支)

5.1 环境搭建与接入

安装启动

  • 下载:Seata Server 安装包
  • 启动:seata-server.bat
  • 控制台:http://localhost:7091(7091 是 Web 管理界面端口,8091 才是 TC 协调通信端口

下载并解压 Seata 后,进入 bin 目录,使用 seata-server.bat 命令启动 Seata。 下载的 Seata 版本保证与 pom 文件中引入的 spring-cloud-alibaba-dependencies 依赖中的 Seata 版本一致。

在需要使用分布式事务的模块中添加依赖: 引入依赖

<dependency>  
    <groupId>com.alibaba.cloud</groupId>  
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>  
    <version>2023.0.3.2</version>  
</dependency>

配置 TC 地址(Seata服务器)

在 resources 目录下创建 file.conf

service {  
  #事务服务组映射  
  vgroupMapping.default_tx_group = "default"  
  #仅当注册中心类型为file时支持,请不要配置多个地址  
  default.grouplist = "127.0.0.1:8091"  
  #降级开关,当前暂不支持  
  enableDegrade = false  
  #禁用Seata全局事务  
  disableGlobalTransaction = false  
}

开启全局事务

在事务发起方(TM)的业务方法上添加 @GlobalTransactional 注解,即可将该方法内的所有远程调用纳入分布式事务管理。各分支事务(RM)会自动向 TC 注册并汇报执行状态。

@GlobalTransactional // 开启 Seata 全局事务,分支事务会自动汇报给 TC  
public Order createOrder(Long productId, Long userId) {  
    // 远程调用:扣减库存(分支事务1)  
    productFeignClient.reduceStock(productId);  
    // 本地操作:创建订单(分支事务2)  
    Order order = new Order();  
    // ...  
    return order;  
}

注意:@EnableFeignClients(basePackages = "com.atguigu.order.feign") 仍然需要配置以启用 Feign 客户端扫描。

本地事务测试

在启动类上加上 @EnableTransactionManagement,在方法上加上 @Transactional

注解 加在哪里 作用
@EnableTransactionManagement 启动类 开启 Spring 注解式事务管理功能(”开关”),不开则 @Transactional 不生效。Spring Boot 默认已自动开启,显式加上更清晰
@Transactional 方法(或类)上 将该方法包裹在一个 本地数据库事务 中,方法内所有数据库操作要么全部成功提交,要么遇到异常全部回滚。常用参数:rollbackFor = Exception.class(指定哪些异常触发回滚,默认只回滚 RuntimeException
示例代码
@Override  
@Transactional(rollbackFor = Exception.class)  
public void deduct(String commodityCode, int count) {  
    storageTblMapper.deduct(commodityCode, count);  
    if (Objects.equals(5, count)) {  
        throw new RuntimeException("内存不足!");  
    }  
}

@Transactional 与 @GlobalTransactional 的区别:

@Transactional @GlobalTransactional
范围 单个服务内的本地数据库事务 跨多个微服务的分布式事务
管理者 Spring 事务管理器 Seata TC(事务协调器)
回滚能力 只能回滚本服务的数据库操作 能协调所有参与服务一起回滚

💡 在 Seata 场景下两者通常 配合使用:每个分支事务用 @Transactional 保证本地原子性,入口方法用 @GlobalTransactional 保证全局一致性。

  • 未使用 @GlobalTransactional 时:某个分支出错,只有出错的服务回滚,其他服务不会回滚 → 数据不一致
  • 使用 @GlobalTransactional 后:某个分支出错,TC 会通知所有分支事务回滚 → 数据一致

5.2 架构原理

整体流程:

  1. TM 向 TC 申请开启一个全局事务,TC 生成全局事务 ID(XID
  2. XID 在微服务调用链中传播(通过 Feign 请求头自动传递)
  3. 每个 RM 将本地事务作为分支事务向 TC 注册,并汇报执行结果
  4. TM 根据业务执行结果,通知 TC 发起全局 提交 或 回滚
  5. TC 驱动所有分支事务完成提交或回滚

5.3 AT 模式与二阶提交协议

AT 模式(Auto Transaction):Seata 默认推荐 的事务模式,基于 二阶段提交协议(2PC) 实现,对业务代码 无侵入。核心思想是先让各分支事务本地提交,再由 TC 统一协调最终的提交或回滚。

关键名词解释:

名词 解释
前镜像(Before Image) 执行 SQL 之前,将目标数据的当前状态快照记录下来,用于回滚时恢复数据
后镜像(After Image) 执行 SQL 之后,将目标数据的新状态快照记录下来,用于回滚前做数据校验
undo_log 回滚日志表,保存前镜像和后镜像数据,每个参与 Seata 的数据库都需要建此表
全局锁(Global Lock) 由 TC 管理的行级锁,锁定被操作的数据行,防止其他全局事务并发修改同一数据。注意:这不是 MySQL 的全局锁(锁整个数据库),而是类似 MySQL 行锁级别,只锁操作涉及的数据
XID 全局事务 ID,在整个调用链中传播,用于标识同一个全局事务
Branch ID 分支事务 ID,每个 RM 注册分支时由 TC 分配,用于标识某个分支事务

5.3.1 二阶提交协议流程

第一阶段:本地提交(各分支事务独立执行)

每个分支事务(RM)依次执行以下步骤:

  1. 生成前镜像:对即将操作的数据执行 SELECT,将当前数据状态记录为前镜像(Before Image)

  2. 执行业务 SQL

    • MySQL 自动对操作行加 行锁SELECT ... FOR UPDATE),锁定数据防止并发修改
    • 执行具体的业务 SQL(INSERT / UPDATE / DELETE)
    • SQL 执行完毕后释放 MySQL 行锁

    💡 行锁期间:普通 SELECT 可以正常读取(取决于隔离级别),但加锁读(SELECT ... FOR UPDATE)会被阻塞

  3. 生成后镜像:将操作后的数据状态记录为后镜像(After Image)

  4. 写入 undo_log:将前镜像和后镜像一起保存到 undo_log 表中

  5. 向 TC 注册分支 & 申请全局锁:RM 向 TC 注册当前分支事务,并申请全局锁锁定被操作的数据行,防止其他全局事务修改

  6. 本地事务提交:将业务数据变更和 undo_log 记录在 同一个本地事务 中一起提交(保证原子性)

  7. 向 TC 汇报:告知 TC 当前分支事务的执行状态(成功 / 失败)

⚠️ 关键点:第一阶段就 真正提交了本地事务,数据已经落库。这意味着不会长时间持有数据库锁,不会在第二阶段阻塞数据库

第二阶段:全局提交或回滚(由 TC 统一协调)

TC 能感知到所有分支事务的状态,根据整体情况决定提交或回滚:

✅ 情况一:所有分支事务都成功 → 全局提交

  1. TC 通知所有分支事务进行提交
  2. 各 RM 收到通知后,将删除 undo_log 记录的操作放入 异步任务队列
  3. 异步 + 批量 删除对应的 undo_log 记录(因为数据已经在第一阶段提交了,不需要额外操作,只需清理日志)

❌ 情况二:某个分支事务失败 → 全局回滚

  1. TC 通知 所有 分支事务进行回滚
  2. 各 RM 通过 XID + Branch ID 找到对应的 undo_log 记录
  3. 数据校验:将后镜像与数据库中的 当前数据 进行对比
    • 一致 → 说明数据没有被其他操作篡改,安全执行回滚:用前镜像的数据覆盖当前数据,恢复到操作前的状态
    • 不一致 → 说明数据已被其他操作修改(出现 脏写),需要根据配置的策略处理(如人工介入、忽略等)
  4. 回滚完成后,删除 undo_log 记录

⚠️ 全局锁的生命周期:只要还有分支事务未处理完(提交或回滚),全局锁就 一直存在。整个流程全部结束后,全局锁才会被释放。

Seata二阶提交协议

5.4 四种事务模式

模式 原理简述 适用场景
AT 模式(默认推荐) 基于二阶段提交,通过 undo_log 自动记录和回滚,对业务 无侵入 大多数业务场景,适合关系型数据库
TCC 模式 业务层面的二阶段:手动实现 Try(预留资源)→ Confirm(确认提交)/ Cancel(取消回滚)三个方法 对性能要求高、需要精细控制资源的场景(如资金操作)
Saga 模式 长事务方案:每个分支事务提交后,如需回滚则执行预先定义的 补偿操作(逆向操作) 业务流程长、参与方多、无法做资源预留的场景、请假等
XA 模式 基于数据库原生的 XA 协议,由数据库本身保证事务一致性,分支事务在第一阶段 不提交,等 TC 通知后才统一提交 对一致性要求极高、可接受较长锁定时间的场景

5.5 Seata 小结

  • 核心定位:分布式事务解决方案,保障微服务间跨服务调用的数据一致性。
  • 三大角色:TC(事务协调器 / Seata 服务器)、TM(事务发起方)、RM(事务参与方)
  • 接入方式:引入依赖 → 配置 file.conf 指向 TC → 业务方法加 @GlobalTransactional 即可
  • AT 模式(默认):基于二阶段提交协议,对业务无侵入
    • 第一阶段:生成前后镜像 → 执行业务 SQL → 写入 undo_log → 申请全局锁 → 本地提交 → 向 TC 汇报
    • 第二阶段(提交):异步批量删除 undo_log
    • 第二阶段(回滚):校验后镜像 → 用前镜像恢复数据 → 删除 undo_log
  • 四种事务模式:AT(默认无侵入)、TCC(手动三阶段)、Saga(补偿型长事务)、XA(数据库原生协议)
  • 关键机制:全局锁保护数据不被并发篡改;第一阶段即真正提交本地事务,不长时间占用数据库锁