Spring Cloud基础

1. 创建微服务项目

Spring Cloud项目的版本要严格和Spring Boot的版本适配,并且本文章还会用到许多Spring Cloud Alibaba的产品,而且是以Spring Cloud Alibaba的版本为主,所以说更加要适配版本了,版本不能乱选。

版本对照说明

参考文档:https://sca.aliyun.com/docs/2023/overview/version-explain/?spm=5176.29160081.0.0.74805c72H5S2UL

本篇文章对应的Spring Cloud项目的大致工程结构图如下:

大致工程结果图


好了,现在我们来从0开始创建一个微服务项目,如图步骤所示:

创建一个SpringCloud项目

这里我构建了一个,由于现在SpringBoot已经来到了3.x,所以JDK的最低版本也是17了,我这里直接用21,然后打包方式选择Jar包即可。

接下来就要选择SpringBoot的版本了,参考文章开头的版本适配图:

使用3.2.4版本

版本以及依赖选择

之后直接点击创建即可。之后删掉不需要的文件以及文件夹,删除后的结果:

保留的文件以及文件夹

之后在根模块进行依赖管理,根模块暂时咋们只需要SpringCloud和SpringCloudAlibaba两个依赖即可:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>nxz.yunrain.com</groupId>
    <artifactId>SpringCloudDemo</artifactId>
    <version>0.0.1</version>
    <name>SpringCloudDemo</name>
    <description>SpringCloud学习示例</description>
    <properties>
        <java.version>21</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-cloud.version>2023.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <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>

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

</project>

根模块pom.xml文件说明

根据之前提到的大致工程结构图,我们现在依次来创建services模块以及下面的具体的一些service模块:

创建services模块

创建之后,由于该模块是所有具体业务service的父模块,不用写具体的代码,所以需要删除这个模块的src文件夹,并且该模块的pom.xml文件中,打包方式也要改为pom:

services模块信息

接下来,创建具体的业务模块,这里为就先创建service-product、service-order两个模块:

service-product模块创建

这里由于是具体的业务模块了,所以是需要写代码的,所以代码相关的文件夹就不能够删除,并且,打包方式选择jar包的方式:

具体的业务模块

好了,这样微服务的架子基本上就搭建好了,后续我们会不断的学习到新的组件来完善我们的微服务,所以,对应的组件依赖,也是在后面持续不断的添加,现在我们开始学习新的组件吧。

2. Nacos

参考文档:

https://sca.aliyun.com/docs/2023/user-guide/nacos/overview/?spm=5176.29160081.0.0.74805c72YVI6Sw

https://nacos.io/docs/v2.3/what-is-nacos/?spm=5238cd80.2ef5001f.0.0.3f613b7c7gI7dc

2.1 什么是Nacos

Nacos(Naming and Configuration Service)是阿里巴巴开源的微服务核心组件,用于解决微服务架构中的 服务发现配置管理服务治理 等核心问题。它在微服务生态中扮演着“注册中心 + 配置中心”的双重角色,是构建云原生应用的关键基础设施。

2.2 为什么需要Nacos

在微服务中,我们为什么需要Nacos呢?是不是非要用Nacos才可以呢?

其实并不是非要用他,在Nacos出来之前,还有其他组件,比如:Eureka、ZooKeeper/Consul、Spring Cloud Config等。那Nacos为什么能够替代他们呢?

  1. 功能整合:Nacos 将服务发现、配置管理、服务治理等功能整合到一个平台,减少组件数量和运维复杂度。传统方案需要组合 Eureka + Config + ZooKeeper 等多组件。
  2. 动态性更强:Nacos 支持配置的实时推送和服务状态的动态感知,而 Spring Cloud Config 需要手动刷新,Eureka 的更新延迟较高。
  3. 更高的可用性和扩展性:Nacos 集群模式简单易用,支持多数据中心和容灾策略,而 ZooKeeper 的部署和维护成本较高。
  4. 云原生友好:Nacos 支持 Kubernetes 服务模型,能与 Docker、K8s 等云原生技术无缝集成,而传统组件(如 Eureka)对云原生生态的适配较弱。
  5. 社区生态活跃:Nacos 由阿里巴巴开源并持续维护,社区支持强大,而 Eureka 等组件已逐渐停止更新。

可见Nacos的功能还是比较强大的,那么为什么在微服务中我们需要使用到Nacos呢?

  1. 服务发现与注册

    微服务架构中,服务实例频繁动态变化(扩缩容、故障迁移等),需要一种机制让服务能自动注册和发现彼此。

    Nacos 作为服务注册中心,提供服务实例的实时注册、心跳检测、健康检查等功能,确保服务间的通信可靠。

  2. 动态配置管理

    微服务通常需要动态调整配置(如数据库连接、开关参数),传统静态配置文件无法满足快速变更需求。

    Nacos 提供统一的配置中心,支持配置的实时推送、版本管理、多环境隔离,避免服务重启。

  3. 服务治理能力

    Nacos 提供负载均衡、流量路由、权重调整、服务元数据管理等能力,帮助实现精细化的服务治理。

  4. 降低架构复杂度

    将服务发现、配置管理、服务元数据管理等功能集成到单一平台,简化了技术栈和维护成本。

同时我们知道,在微服务系统中,应用往往是采用分布式方式部署到。在分布式系统中,配置信息应用非常广泛,可以通过配置来实现不同的功能。这些配置信息如数据库连接信息、日志级别、业务配置等等。在传统的开发方式中,这些配置信息通常硬编码到应用程序的代码中,与程序代码一起打包和部署。然而,这种方式有很多缺点,比如配置信息不易维护,只要修改配置就得重新构建和部署等。

service-config

采用分布式配置中心的软件架构如上图所示,其可以在分布式场景中帮助解决以下问题:

  1. 管理应用程序配置:当有大量应用程序需要管理时,手动维护配置文件会变得非常困难。分布式配置中心提供了一个集中管理和分发配置信息的解决方案。
  2. 环境隔离:在开发、测试和生产等不同环境中,应用程序的配置信息往往都会有不同。使用分布式配置中心,可以轻松地管理和分发不同环境下的配置信息。
  3. 提高程序安全性:将配置信息存储在代码库或应用程序文件中可能会导致安全风险,因为这些信息可能会被意外地泄漏或被恶意攻击者利用。使用分布式配置,可以将配置信息加密和保护,并且可以进行访问权限控制。
  4. 动态更新配置:在应用程序运行时,可能需要动态地更新配置信息,以便应用程序可以及时响应变化。使用分布式配置中心,可以在运行时动态更新配置信息,而无需重新启动应用程序。

所以,这就是我们需要Nacos的原因。

2.3 Nacos的功能

2.3.1 服务发现与注册

Nacos 作为服务注册中心,管理所有微服务实例的注册、发现和健康状态监控。

核心能力

  • 服务注册:服务实例启动时自动向 Nacos 注册,提供 IP、端口、健康状态、元数据等信息。

  • 服务发现:消费者通过服务名(而非具体 IP)查询可用实例列表,实现服务间的动态调用。

  • 健康检查

    • 主动心跳检测:客户端定期发送心跳包,Nacos 根据心跳判断实例是否存活。
    • 被动健康检查:支持 TCP、HTTP、MySQL 等协议的健康探测,自动剔除异常实例。
  • 权重管理:为服务实例配置权重,实现流量按比例分配(如灰度发布)。

  • 元数据管理:支持为服务添加自定义元数据(如版本、环境标签),用于路由和过滤。

2.3.2 动态配置管理

提供统一的配置中心,支持配置的集中管理、实时推送和多环境隔离。

核心能力

  • 配置存储:以 Key-Value 形式存储配置,支持 JSON、XML、YAML、Properties 等格式。
  • 动态更新:配置修改后,通过长轮询或 HTTP 长连接实时推送到客户端,无需重启服务。
  • 版本控制:记录配置的历史版本,支持回滚到任意版本。
  • 多环境隔离:通过 Namespace(命名空间)隔离不同环境(如开发、测试、生产)的配置。
  • 多租户支持:通过 Group(分组)隔离不同项目或团队的配置。
  • 监听机制:客户端监听配置变化,触发回调逻辑(如刷新 Spring Bean)。

2.3.3 服务治理

提供细粒度的服务流量管理和治理能力。

核心能力

  • 负载均衡策略:支持基于权重、随机、轮询等算法的客户端负载均衡。
  • 流量路由:根据元数据(如版本、环境)将请求路由到特定实例。
  • 服务熔断与降级:与 Sentinel 等组件集成,实现限流、熔断和降级。
  • 服务鉴权:通过 AccessToken 控制服务访问权限。
  • 服务依赖关系:可视化展示服务间的调用拓扑,帮助分析依赖瓶颈。

2.3.4 命名空间(Namespace)与分组(Group)

实现资源的多环境、多租户隔离。

核心能力

  • Namespace:隔离不同环境(如 dev/test/prod)或不同业务线的服务与配置。每个 Namespace 有独立的服务注册表和配置库。
  • Group:在同一个 Namespace 内,通过 Group 进一步隔离资源(如不同微服务模块)。默认分组为 DEFAULT_GROUP

2.3.5 集群管理与高可用

支持分布式集群部署,保障高可用性和数据一致性。

核心能力

  • 集群模式:通过 Raft 协议实现节点间数据一致性,支持自动选举 Leader。
  • 持久化存储
    • 内置数据库:默认使用内嵌的 Derby 数据库,适合测试环境。
    • 外置数据库:支持 MySQL 作为集群数据存储,适合生产环境。
  • 多数据中心:支持跨数据中心的集群部署,实现异地容灾。
  • 监控与告警:集成 Prometheus 和 Grafana,提供监控指标(如 QPS、健康实例数)。

2.3.6 动态 DNS 服务

将服务名解析为具体的实例 IP 列表,支持基于权重的负载均衡。

核心能力

  • 域名解析:通过 DNS 协议(如 curl http://service-name.nacos)访问服务。
  • 权重路由:根据实例权重分配 DNS 查询结果。
  • 多协议支持:兼容 Dubbo、Spring Cloud、Kubernetes 等服务模型。

2.3.7 扩展与集成能力

与主流微服务框架和云原生生态无缝集成。

核心能力

  • Spring Cloud 集成:通过 spring-cloud-starter-alibaba-nacos 实现服务注册、配置管理。
  • Dubbo 集成:作为 Dubbo 的注册中心和配置中心。
  • Kubernetes 适配:支持 K8s Service 模型,实现云原生环境下的服务治理。
  • OpenAPI:提供 RESTful API,支持与其他系统(如 CI/CD 工具)集成。

2.3.8 权限与安全

保障配置和服务资源的安全性。

核心能力

  • 身份认证:支持用户名密码、AccessToken 等认证方式。
  • 权限控制:通过 RBAC 模型(Role-Based Access Control)管理用户对资源的操作权限。
  • 配置加密:敏感配置(如数据库密码)支持加密存储。

了解完毕了Nacos的一些基本功能,现在我们来安装Nacos。

2.4 安装Nacos

参考文档:

从之前的版本对照说明可以看出,此处我们Nacos的版本应该选择2.3.2版本:

Nacos版本选择

这里安装方式有两种,一种是下载安装包的方式安装,一种是Docker的方式安装,我这里就直接安装包的方式安装,并且本文章只会单机部署,并不会集群部署。

下载对应的版本:

文件下载

  1. 下载

    wget https://github.com/alibaba/nacos/releases/download/2.3.2/nacos-server-2.3.2.tar.gz
    
  2. 解压

    tar -xvf nacos-server-2.3.2.tar.gz
    
  3. 修改配置文件

    修改数据库配置,我这里使用mysql

    使用mysql配置

    修改了之后,我们需要先把对应的数据库创建出来,并且把SQL文件导入进去,该SQL文件就在conf这个目录下:

    SQL文件

    [!warning]

    在2.2.0.1和2.2.1版本时,必须执行此变更,否则无法启动;其他版本为建议设置。

    修改conf目录下的application.properties文件。

    设置其中的nacos.core.auth.plugin.nacos.token.secret.key值,详情可查看鉴权-自定义密钥.

    [!caution]

    文档中的默认值SecretKey012345678901234567890123456789012345678901234567890123456789VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=为公开默认值,可用于临时测试,实际使用时请务必更换为自定义的其他有效值。

    当然,为了安全起见,最好还是把授权给开上,保证文件的安全:

    配置授权
  4. 启动

    上述配置都配置好了之后,我们的就可以启动Nacos了,由于我是单机启动,非集群,所以使用启动命令:

    sh startup.sh -m standalone
    

启动成功之后,通过IP:端口/nacos的方式进行访问即可:

访问Nacos

2.5 整合SpringCloud与Nacos

2.5.1 服务注册

现在Nacos安装好了,我们就可以将它与Spring Boot进行整合了。

首先我们先试试服务发现,先把最基本的一部打通:也就是确保Boot程序能够连上Nacos。

首先我们在services模块中,加入服务注册的依赖:

<!--服务发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

新增服务发现依赖

之后,我们选择一个具体的业务模块编写application.yaml文件,以及编写主程序:

server:
  port: 8080
spring:
	# 服务的名称必须填写,否则服务无法注册上
  application:
    name: service-order
  cloud:
    nacos:
      server-addr: nacos服务的IP:8848
      # 如果你的nacos配置文件开启了鉴权,那么这里你就必须要配置用户名和密码,否则就会报错
      discovery:
        username: nacos
        password: nacos

service-order模块配置文件

这里又一点需要注意一下,如果你的Nacos配置没有开启鉴权配置,那么这里是无需填写Nacos的用户名和密码的。如果开启了,则需要配置,由于之前我开启过:

配置授权

所以,这里我是需要配置的。

配置文件编写好了之后,我们就可以编写主程序之后启动项目即可。启动项目之后,如果能够在Nacos的控制台的服务列表模块发现该服务,那么就说明连通了:

服务注册成功

好了,现在对于service- product模块我们也是同样的一个配置方法,之后启动项目:

service-product服务注册成功

可见,在服务列表中,有集群数目,实例数,健康实例数,现在我们多启动几个实例,看看的数据是否有变化:

多实例启动

多实例配置

新增程序参数

指定不同的端口

之后,按照上诉步骤,咋们多启动几个不同的实例:

多实例情况

之后,我们回到Nacos的控制台,查看服务列表的情况:

多实例情况

2.5.2 服务发现

之前服务注册已经测试完成,现在我们测测服务发现。在任意具体业务模块编写测试类:

@SpringBootTest
@Slf4j
public class DiscoveryTest {

    @Resource
    private DiscoveryClient discoveryClient;
    @Resource
    private NacosDiscoveryClient nacosDiscoveryClient;

    @Test
    public void t1(){
      	//获取服务集合
        List<String> services = discoveryClient.getServices();
        for (String service : services) {
            //获取指定服务下的实例
            List<ServiceInstance> instances = discoveryClient.getInstances(service);
            for (ServiceInstance instance : instances) {
                log.info("服务名称:{};IP:{};Port:{}",service,instance.getHost(),instance.getPort());
            }
        }

    }


    @Test
    public void t2(){
        List<String> services = nacosDiscoveryClient.getServices();
        for (String service : services) {
            List<ServiceInstance> instances = nacosDiscoveryClient.getInstances(service);
            for (ServiceInstance instance : instances) {
                log.info("服务名称:{};IP:{};Port:{}",service,instance.getHost(),instance.getPort());
            }
        }

    }
}

从上述代码可以看出,两个测试代码差不多,唯一的区别就是DiscoveryClientNacosDiscoveryClient

其中DiscoveryClient是Spring家的规范,而NacosDiscoveryClient是Nacos自己的规范,从代码的角度来说,NacosDiscoveryClient实现了DiscoveryClient接口。二者都是服务发现的重要组件。

我们来看看执行结果:

服务发现

可见能够拿到不同服务的信息,可见服务发现也是OK的。

[!tip]

有些Alibaba Cloud的版本需要明确加上@EnableDiscoveryClient注解才能够发现服务,@EnableDiscoveryClient注解的功能就是用于显式的开启服务发现的功能,但是在一些高版本的Alibaba Cloud项目中,已经不需要在明确的指定这个注解了。只要你导入了依赖:spring-cloud-starter-alibaba-nacos-discovery;并且正确配置了Nacos的服务地址:spring.cloud.nacos.discovery.server-addr;那么就无需在显式的添加@EnableDiscoveryClient注解,但是如果你都加上,那也没问题。

2.5.3 远程调用之RestTemplate

现在我们来试试微服务之间的远程调用,现在我有两个微服务,一个product-service(商品微服务),一个order-service(订单微服务);其中,我们需要发送一个创建订单的请求,但是在创建订单之前,我们需要拿到对应的商品信息。

所以总结下来就是,order-service(订单微服务)需要远程调用product-service(商品微服务)。

调用示例图

现在我们分别来编写对应的服务代码:

  1. 由于待会需要远程调用,所以在订单微服务模块中,肯定会用到商品的实体类数据,所以我们需要创建一个model模块,专门用来存放各个微服务的实体类数据。

    创建model模块

  2. 创建Order与Product实体类。

    package nxz.yunrain.cn.product;
    
    import lombok.Data;
    
    import java.math.BigDecimal;
    
    /**
     * @author nianxinzhuo
     * @date 2025/5/31 22:52
     * @description 商品实体类
     */
    @Data
    public class Product {
        /**
         * 商品ID
         */
        private Long id;
    
        /**
         * 商品名称
         */
        private String productName;
    
        /**
         * 商品价格
         */
        private BigDecimal price;
    
        /**
         * 商品数量
         */
        private int num;
    }
    
    package nxz.yunrain.cn.order;
    
    import lombok.Data;
    import nxz.yunrain.cn.product.Product;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/5/31 22:53
     * @description 订单实体类
     */
    @Data
    public class Order {
        /**
         * 订单ID
         */
        private Long id;
    
        /**
         * 用户ID
         */
        private Long userId;
    
        /**
         * 用户昵称
         */
        private String nickname;
    
        /**
         * 订单总金额
         */
        private BigDecimal totalAmount;
    
        /**
         * 订单地址
         */
        private String address;
    
        /**
         * 商品列表
         */
        private List<Product> productList;
    }
    

    目录结构

  3. 编写查询商品与下单业务实现代码。

    创建新增订单请求参数:

    package cn.yunrain.nxz.order.dto;
    
    import lombok.Data;
    
    import java.io.Serial;
    import java.io.Serializable;
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 09:26
     * @description 创建订单请求参数
     */
    @Data
    public class OrderAddDto implements Serializable {
        @Serial
        private static final long serialVersionUID = 1L;
    
        /**
         * 用户ID;原则上用户ID不应该是前端传递,而应该是后端获取
         */
        private Long userId;
    
        /**
         * 商品ID集合
         */
        private List<Long> productIdList;
    }
    

    创建新增订单接口:

    package cn.yunrain.nxz.controller;
    
    import cn.yunrain.nxz.service.OrderService;
    import jakarta.annotation.Resource;
    import cn.yunrain.nxz.order.Order;
    import cn.yunrain.nxz.order.dto.OrderAddDto;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 09:21
     * @description 商品服务接口
     */
    @RestController
    @RequestMapping("order")
    public class OrderController {
    
        @Resource
        private OrderService orderService;
    
        @PostMapping("createOrder")
        public Order createOrder(@RequestBody OrderAddDto dto) {
            // 模拟创建订单
            Order order = orderService.createteOrder(dto);
            return order;
        }
    }
    

    具体的实现类:

    package cn.yunrain.nxz.service.impl;
    
    import cn.yunrain.nxz.service.OrderService;
    import lombok.extern.slf4j.Slf4j;
    import cn.yunrain.nxz.order.Order;
    import cn.yunrain.nxz.order.dto.OrderAddDto;
    import org.springframework.stereotype.Service;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 09:23
     * @description 订单业务
     */
    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
    
        /**
         * 创建订单
         *
         * @param dto 订单添加请求参数
         * @return 订单实体
         */
        @Override
        public Order createteOrder(OrderAddDto dto) {
            //模拟订单数据
            Order order = new Order();
            order.setId(System.currentTimeMillis());
            order.setUserId(dto.getUserId());
            order.setNickname("测试用户");
            order.setAddress("测试地址");
    
            //TODO 根据商品ID集合查询商品信息
            order.setProductList(null);
            //TODO 计算总金额
            order.setTotalAmount(null);
            return order;
        }
    }
    
  4. 请求结果示例

    请求结果

从上述代码以及请求结果中我们可以看见,在创建订单的时候,我们是需要调用查询查询商品信息API的,但是由于查询商品信息API是在商品微服务模块中的,所以我们就需要使用一种远程调用的方式来调用商品服务。

远程调用有多种方式:

  1. 直接发送HTTP请求
    使用HttpURLConnection或第三方库(如Apache HttpClient、OkHttp)手动构建HTTP请求。
    特点:灵活但代码量大,需自行处理序列化、异常和连接管理。
  2. RestTemplate(Spring框架)
    Spring提供的同步HTTP客户端,封装了RESTful调用。
    特点:简化HTTP操作,支持消息转换(如JSON/XML),但已逐步被WebClient取代(Spring 5+)。
  3. OpenFeign(声明式REST客户端)
    通过接口和注解定义HTTP请求,由Feign动态生成实现类。
    特点:与Spring Cloud集成度高,支持负载均衡(结合Ribbon)、熔断(Hystrix)等。
  4. WebClient(响应式非阻塞)
    Spring WebFlux提供的异步HTTP客户端,支持响应式编程。
    特点:非阻塞IO,适合高并发场景,与Reactor库深度集成。
  5. RPC框架(如gRPC、Dubbo)
    基于二进制协议(如Protocol Buffers)的高性能远程调用。
    特点:适用于跨语言服务或对性能要求严格的场景。

这里我们不过多展开,主要就讲讲RestTemplate


RestTemplate 是 Spring Framework 提供的一个同步的、阻塞式的 HTTP 客户端工具类,专为简化与 RESTful 服务(HTTP API)的交互而设计。它封装了底层 HTTP 库(如 JDK 的 HttpURLConnection, Apache HttpClient, OkHttp 等)的复杂性,提供了一套更符合 Spring 风格的、模板化的 API。

核心目标:

  1. 简化 HTTP 操作: 避免手动处理 HttpURLConnection 或第三方 HTTP 库的繁琐步骤(设置连接、管理流、处理状态码、序列化/反序列化等)。
  2. 集成 Spring 生态: 与 Spring 的消息转换器(HttpMessageConverter)无缝集成,自动将 Java 对象与 JSON/XML 等格式相互转换。
  3. 统一错误处理: 提供 ResponseErrorHandler 机制来统一处理 HTTP 错误响应(如 4xx, 5xx)。
  4. 便捷的请求构建: 提供多种方法对应不同的 HTTP 方法(GET, POST, PUT, DELETE 等)和参数传递方式。

基本使用步骤:

  1. 创建 RestTemplate 实例

    创建RestTemplate示例有两种方式,一种是直接new,还有一种是通过bean注入的方式:

    // 最简单方式 (通常注入使用,而非每次都new)
    RestTemplate restTemplate = new RestTemplate();
    
    // 配置底层 HTTP Client (推荐使用 Apache HttpClient 或 OkHttp)
    RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    
    // 在 Spring Boot 应用中,通常通过 @Bean 配置并注入
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    }
    
  2. 发起 HTTP 请求: RestTemplate 提供了多种方法对应不同的 HTTP 动词和参数类型。


现在我们来使用RestTemplate来发送查询商品信息的请求。

  1. 编写商品信息查询API

    商品列表查询请求参数:

    package cn.yunrain.nxz.product.dto;
    
    import lombok.Data;
    
    import java.io.Serial;
    import java.io.Serializable;
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 11:17
     * @description 商品列表查询请求参数
     */
    @Data
    public class ProductListQueryDto implements Serializable {
        @Serial
        private static final long serialVersionUID = 1L;
    
        /**
         * 商品ID集合
         */
        private List<Long> productIdList;
    }
    

    查询商品信息API:

    package cn.yunrain.nxz.controller;
    
    import cn.yunrain.nxz.product.Product;
    import cn.yunrain.nxz.product.dto.ProductListQueryDto;
    import cn.yunrain.nxz.service.ProductService;
    import jakarta.annotation.Resource;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 11:10
     * @description 商品服务接口
     */
    @RestController
    @RequestMapping("product")
    public class ProductController {
        @Resource
        private ProductService productService;
    
        @PostMapping("getProductListByIds")
        public List<Product> getProductListByIds(@RequestBody ProductListQueryDto dto) {
            // 模拟根据商品ID列表查询商品信息
            List<Product> productList = productService.getProductListByIds(dto);
            return productList;
    
        }
    }
    

    查询实现类:

    package cn.yunrain.nxz.service.impl;
    
    import cn.yunrain.nxz.product.Product;
    import cn.yunrain.nxz.product.dto.ProductListQueryDto;
    import cn.yunrain.nxz.service.ProductService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    import java.math.BigDecimal;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 10:15
     * @description 商品服务
     */
    
    @Service
    @Slf4j
    public class ProductServiceImpl implements ProductService {
        /**
         * 根据商品ID列表查询商品信息
         *
         * @param dto 商品列表查询请求参数
         * @return 商品信息列表
         */
        @Override
        public List<Product> getProductListByIds(ProductListQueryDto dto) {
            log.info("开始根据商品ID列表查询商品信息...");
            // 模拟根据商品ID列表查询商品信息
            if (CollectionUtils.isEmpty(dto.getProductIdList())){
                log.warn("商品ID列表为空,无法查询商品信息");
                return new ArrayList<>();
            }
            List<Product> productList = new ArrayList<>(dto.getProductIdList().size());
            for (Long productId : dto.getProductIdList()) {
                Product product = new Product();
                product.setId(productId);
                product.setProductName("商品" + productId);
                product.setPrice(new BigDecimal("100").add(new BigDecimal(productId))); // 模拟价格
                product.setNum(2);
                productList.add(product);
            }
            return productList;
        }
    }
    
  2. 在订单服务中使用RestTemplate来请求商品信息

    要使用RestTemplate首先要将他实例化。这里我们通过配置类的方式实例化:

    package cn.yunrain.nxz.config;
    
    import org.springframework.cloud.client.loadbalancer.LoadBalanced;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.client.RestTemplate;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 11:06
     * @description RestTemplate配置类
     */
    
    @Configuration
    public class RestTemplateConfig {
    
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    实例化之后,就可以用RestTemplate来发送远程调用请求:

    package cn.yunrain.nxz.service.impl;
    
    import cn.yunrain.nxz.product.Product;
    import cn.yunrain.nxz.product.dto.ProductListQueryDto;
    import cn.yunrain.nxz.service.OrderService;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import cn.yunrain.nxz.order.Order;
    import cn.yunrain.nxz.order.dto.OrderAddDto;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.core.ParameterizedTypeReference;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Service;
    import org.springframework.web.client.RestTemplate;
    
    import java.math.BigDecimal;
    import java.util.List;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/1 09:23
     * @description 订单业务
     */
    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
    
        @Resource
        private RestTemplate restTemplate;
        @Resource
        private DiscoveryClient discoveryClient;
    
        /**
         * 创建订单
         *
         * @param dto 订单添加请求参数
         * @return 订单实体
         */
        @Override
        public Order createteOrder(OrderAddDto dto) {
            log.info("开始创建订单....");
            //模拟订单数据
            Order order = new Order();
            order.setId(System.currentTimeMillis());
            order.setUserId(dto.getUserId());
            order.setNickname("测试用户");
            order.setAddress("测试地址");
    
            // 根据商品ID集合查询商品信息
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
    
            ProductListQueryDto requestDto = new ProductListQueryDto();
            requestDto.setProductIdList(dto.getProductIdList());
    				// 封装请求实体
            HttpEntity<ProductListQueryDto> requestEntity = new HttpEntity<>(requestDto, headers);
    
            //获取服务实例列表(商品服务实例)
            List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("service-product");
            if (serviceInstanceList.isEmpty()) {
                log.error("未找到服务实例: service-product");
                throw new RuntimeException("未找到商品服务实例");
            }
    
            // 使用RestTemplate调用商品服务的接口
            ServiceInstance serviceInstance = serviceInstanceList.get(0);
            String url = serviceInstance.getUri() + "/product/getProductListByIds";
            log.info("调用商品服务接口: {}", url);
    
            Product[] products = restTemplate.postForObject(
                    url,
                    requestEntity,
                    Product[].class
            );
    
            // 将products数组转为List集合
            List<Product> productList = List.of(products != null ? products : new Product[0]);
    
            log.info("查询商品信息: {}", productList);
            order.setProductList(productList);
    
            //计算总金额
            order.setTotalAmount(BigDecimal.ZERO);
            for (Product product : productList) {
                //金额 = 单价 * 数量
                if (product.getPrice() != null && product.getNum() != null) {
                    order.setTotalAmount(order.getTotalAmount().add(product.getPrice().multiply(new BigDecimal(product.getNum()))));
                } else {
                    log.warn("商品价格或数量为空,无法计算总金额");
                }
            }
            return order;
        }
    }
    
  3. 启动product-service和order-service,并且发送请求,看是否能够查询出商品数据。

    创建订单时查询商品信息

可见商品信息确实是查询出来了,说明RestTemplate远程调用的方式确实成功了。


虽然RestTemplate远程调用的方式成功了,但是我们每次使用RestTemplate的时候,都需要获取服务实例,然后手动拼接请求地址,这是十分麻烦的,那么有没有简单点的方式呢?

请求地址构建与发送请求

这里当然还有简单点的方式,那就是负载均衡

有了负载均衡之后,我们就可以不用手动的获取服务实例和请求地址了。 同时如果你有多个服务实例,那么系统会将接收到的流量分发给不同的服务实例,不会一直是一个服务实例在处理数据。

使用步骤:

  1. 引入负载均衡相关依赖

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

    引入负载均衡依赖

  2. 给远程调用RestTemplate加上负载均衡功能

    @Configuration
    public class RestTemplateConfig {
    
        @Bean
        @LoadBalanced
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    加上@LoadBalanced注解之后就具有了负载均衡功能

    使用@LoadBalanced开启负载均衡

  3. 远程调用地址直接写上商品服务模块的名称即可

    Product[] products = restTemplate.postForObject(
            "http://service-product/product/getProductListByIds",
            requestEntity,
            Product[].class
    );
    

    简化版请求

所以,当以前没有@LoadBalanced的时候,RestTemplate 会把 service-product 当作普通的主机名,然后尝试进行 DNS 解析,但找不到这个主机以至于连接不上。所以,没有@LoadBalanced的时候,就需要手动获取到服务实例列表,然后挑选一个出来,手动拼接请求地址,例如之前的这部分:

手动获取实例并拼接地址

现在有了@LoadBalanced之后,Spring Cloud LoadBalancer 会拦截请求,通过服务发现(如 Eureka、Consul、Nacos)找到 service-product 服务的实际实例,将服务名替换为真实的IP:Port

同时,如果你有多个服务实例,系统默认会采用轮训的方式来请求多个实例。

现在我请求5次创建订单请求,看看各个实例的商品服务被调用了几次。

各个实例轮训处理

可见确实是负载均衡的。

[!caution]

这里有一点需要注意了,如果你是使用的是手动获取服务实例然后拼接的请求地址的方式,那么你就不要用@LoadBalanced注解。因为你传入的是完整的地址,例如:http://192.168.3.123:8082/product/getProductListByIds,此时LoadBalanced会误解析192.168.3.123当作服务名,尝试通过服务发现查找实例。

所以,如果你传入的地址是真实IP:Port的这种形式的请求地址的时候,就不要用@LoadBalanced注解。

2.5.4 配置中心

2.5.4.1 配置值导入获取

如果你要使用Nacos的配置中心,那么你需要按照如下步骤配置:

  1. 引入配置中心的相关依赖

    <!--配置中心-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
  2. 引入配置

    在应用的/src/main/resources/application.yaml配置文件中配置Nacos Config地址并引入服务配置:

    spring:
     	# 省略其他配置...
      # 引入nacos上的配置文件
      config:
        import:
          - nacos:nacos-order-config.yaml
    

    前缀nacos::这是一个自定义的命名空间(类似file:classpath:),Spring Cloud Alibaba会拦截此类配置,将其解析为从Nacos服务器加载配置。

    完整格式:

    spring.config.import=nacos:${filename}[?配置项]
    

    例如:

    spring.config.import=nacos:nacos-order-config.yaml?group=DEFAULT_GROUP&namespace=dev
    

    如果未指定参数(如group/namespace),则使用spring.cloud.nacos.config下的全局配置。

    引入nacos上的nacos-order-config配置

  3. Nacos中创建配置

    在第二步的时候,我们引入了nacos-order-config.yaml配置文件,那么我们现在就需要在Nacos中将这个配置文件创建出来。

    创建配置

    创建配置


现在Nacos中的配置文件也创建好了,现在我们来测试一下,是否能够获取到创建的配置。

import cn.yunrain.nxz.OrderMainApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author nianxinzhuo
 * @date 2025/6/2 09:59
 * @description Nacos配置导入测试
 */
@SpringBootTest(classes = OrderMainApplication.class)
public class NacosConfigImportTest {

    @Value("${test.timeout}")
    private String timeout;

    @Value("${test.cache-time}")
    private String cacheTime;

    @Test
    public void testNacosConfigImport() {
        System.out.println("Timeout: " + timeout);
        System.out.println("Cache Time: " + cacheTime);
    }
}

执行测试方法,发现程序报错了:

403错误,用户没找到

这个403,用户没找到错误是怎么回事?

其实很简单,因为我的Nacos开启了权限校验的。正如之前服务发现中的配置一样,需要加上权限相关的信息,所以我们这里也要给配置中心也加上权限相关的信息。

配置中心的权限配置

[!important]

如果你的Nacos开启了权限校验,那么无论是你的注册中心还是你的配置中心,你都要配置用户名和密码。

配置完毕之后,再次启动验证:

获取成功


2.5.4.2 多配置问题

现在能够拿到Nacos上的配置了,但是我还有一个疑问:如果程序本身有的配置,在Nacos上也有,那么应该是谁生效呢?

现在我们在程序和Nacos上都写上相同的配置:

新增与nacos上的配置一样的配置项

启动程序查看:

执行结果

可见,仍然是Nacos上的配置生效了。

[!important]

所以,如果Nacos上的配置和程序本身的配置具有相同的配置项,那么是以Nacos上的配置项为准


那既然Nacos上的配置和程序本身的配置具有相同的配置项,是以Nacos上的配置项为准,那么我如果多个Nacos配置上都有相同的配置项,那么是以哪个Nacos上的配置为准呢?

新增配置

新增配置

启动程序验证:

执行结果

可见是第二个配置生效了。

[!important]

如果有多个Nacos配置被导入,且有相同的配置项,那么对于相同的配置项,后导入的配置会覆盖先导入的配置


2.5.4.3 配置动态刷新

现在配置是能够获取了,那么我们再来看看配置的动态刷新

在开始之前,我们先还原配置,只保留一个Nacos配置:

保留的配置

你想要配置的动态刷新很简单, 只需要配合一个注解@RefreshScope即可:

package cn.yunrain.nxz.controller;

import cn.yunrain.nxz.service.OrderService;
import jakarta.annotation.Resource;
import cn.yunrain.nxz.order.Order;
import cn.yunrain.nxz.order.dto.OrderAddDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

/**
 * @author nianxinzhuo
 * @date 2025/6/1 09:21
 * @description 商品服务接口
 */
@RestController
@RequestMapping("order")
@RefreshScope //启动配置动态刷新
public class OrderController {

    @Resource
    private OrderService orderService;

  	//获取配置值
    @Value("${test.timeout}")
    private String timeout;

    @Value("${test.cache-time}")
    private String cacheTime;
  	
  	//之前的代码....

    @GetMapping("/getNacosOrderConfig")
    public String getNacosOrderConfig() {
        return "timeout:"+timeout + ", cacheTime:" + cacheTime;
    }
}

执行结果:

执行结果

现在我修改配置中的值,然后不重启程序,直接请求:

修改配置值

请求结果


除了使用@Value+@RefreshScope配合的方式、还可以使用@ConfigurationProperties注解的方式实现动态刷新。

package cn.yunrain.nxz.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author nianxinzhuo
 * @date 2025/6/2 12:06
 * @description 配置值
 */
@Component
@ConfigurationProperties(prefix = "test")
@Data
public class OrderProperties {
    private String timeout;
    private String cacheTime;
}
package cn.yunrain.nxz.controller;

import cn.yunrain.nxz.order.Order;
import cn.yunrain.nxz.order.dto.OrderAddDto;
import cn.yunrain.nxz.properties.OrderProperties;
import cn.yunrain.nxz.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;

/**
 * @author nianxinzhuo
 * @date 2025/6/1 09:21
 * @description 商品服务接口
 */
@RestController
@RequestMapping("order")
public class OrderController {
  
  	//将配置值注入进来
    @Resource
    private OrderProperties orderProperties;

		//其他代码...
  
    @GetMapping("/getNacosOrderConfig")
    public String getNacosOrderConfig() {
        return "timeout:"+orderProperties.getTimeout() + ", cacheTime:" + orderProperties.getCacheTime();
    }
}

测试结果也是一样的,仍然可以动态刷新。


除了上述两种方式,还可以使用使用编码的方式来实现配置的监听和更新。

也就是通过NacosConfigManagerApplicationRunner组合方式。

ApplicationRunner是Spring Boot提供的一个接口,用于在Spring Boot应用启动后执行一些特定的代码逻辑

在SpringApplication启动完成后,即在应用上下文(ApplicationContext)准备就绪后,但在应用开始接收请求之前执行

常用于应用启动后需要立即执行的一些初始化操作,如数据加载、缓存预热、检查外部服务等。

@Bean
public ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager) {
    return args -> {
        // 这里可以添加一些初始化逻辑
        System.out.println("项目启动执行一次性逻辑....");
        ConfigService configService = nacosConfigManager.getConfigService();
        //参数:1.配置文件名 2.分组 3.监听器
        configService.addListener("nacos-order-config.yaml", "DEFAULT_GROUP", new Listener() {
            //线程池配置
            @Override
            public Executor getExecutor() {
                return Executors.newFixedThreadPool(10);
            }

            //接收配置变更监听
            @Override
            public void receiveConfigInfo(String s) {
                System.out.println("接收到配置变更: " + s);
                //TODO 监听到配置变更,可以执行其他业务嗲吗
            }
        });
    };
}

项目启动日志

可见确实在项目启动的时候执行了一次。

现在我们去变更一下Nacos上的配置,看看是否能够接收到配置的变更信息。

配置变更监听

监听成功。

2.5.5 数据隔离

数据隔离是Nacos作为配置中心和注册中心的核心能力之一,主要用于解决多环境(如开发、测试、预发布、生产)、多租户、多项目/应用之间的数据(配置和服务)相互影响、安全保密和权限控制的问题。

Nacos 主要通过以下几个关键概念来实现层次化的数据隔离:

  1. 命名空间(NameSpace)
  2. 分组(Group)
  3. 服务/配置名称(dataId)

2.5.5.1 命名空间(NameSpace)

概念: 这是 Nacos 中最高级别、最粗粒度的隔离维度。你可以将其理解为一个完全独立的工作空间或环境。

作用:

  • 环境隔离: 最常见用途。例如:创建 dev (开发环境)、test (测试环境)、fat (预发布环境)、prod (生产环境) 等命名空间。部署在不同环境的应用只会访问其对应命名空间下的配置和服务,彻底避免环境串扰。
  • 租户隔离: 在 SaaS 或平台化场景下,可以为不同租户(客户、业务线、部门)分配独立的命名空间,确保他们的配置和服务数据物理隔离。
  • 项目隔离: 大型组织内不同项目组可以使用不同的命名空间。

在Nacos中,默认会有一个public的命名空间,你可以根据环境隔离案例,来创建不同的命名空间,dev (开发环境)、test (测试环境)、prod (生产环境) 。

新建命名空间

这里我就创建dev、test、prod三个命名空间。

2.5.5.2 分组(Group)

概念: 这是在同一个命名空间内进行的次级分组。粒度比命名空间细,比具体的服务/配置名称粗。

作用:

  • 项目/应用分组: 在同一个环境(命名空间)下,为不同的项目、子系统或微服务应用划分组。例如,在 dev 命名空间下,可以有 project-a 组和 project-b 组。
  • 逻辑隔离与复用: 可以用来区分同一应用的不同版本、不同部署单元(如不同的数据中心或可用区部署的相同应用实例,但通常用集群概念更好),或者作为配置的一种分类标签。
  • 配置共享与覆盖: 结合 Data IDGroup 可以实现更灵活的配置管理(如 DEFAULT_GROUP 存放通用配置,特定组覆盖部分配置)。

同样的,在Nacos中默认会有一个DEFAULT_GROUP分组,你可以根据不同的微服务来创建不同的分组,例如这里我就要给订单微服务模块创建订单分组,给商品微服务模块创建商品分组。

将通用配置文件放到Order分组下

同理,我还要创建Product分组。当Product分组也创建好了之后,我们也要给test和prod命名空间也创建出差不多的配置,但是我们可以选择一个简单的方法给其他两个命名空间创建配置,那就是克隆。

克隆配置

2.5.5.3 测试数据隔离

之前上述的配置内容有点变动,大致格式如下:

common:
  name-space: prod
  group: Order
  url: http://127.0.0.1/common-prod?group=Order

现在的配置文件:

server:
  port: 8080
spring:
  # 服务的名称必须填写,否则服务无法注册上
  application:
    name: service-order
  profiles:
    active: prod
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      # 如果你的nacos配置文件开启了鉴权,那么这里你就必须要配置用户名和密码,否则就会报错
      discovery:
        username: nacos
        password: nacos
      #配置 配置中心的用户名和密码
      config:
        username: nacos
        password: nacos
        #命名空间配置,默认是public,注意,这里其实是命名空间的ID,不是名称!你可以在生成命名空间的时候,将名称和ID填写为一致的
        namespace: 6789e99a-61d2-4e53-a082-3e9f02bb20be

#按需加载
---
spring:
  # 引入nacos上的配置文件
  config:
    #dev环境启用此配置
    activate:
      on-profile: dev
    #导入nacos上的配置文件,格式:文件名?group=分组
    import:
      - nacos:common.yaml?group=Order

---
spring:
  # 引入nacos上的配置文件
  config:
    #test环境启用此配置
    activate:
      on-profile: test
    #导入nacos上的配置文件,格式:文件名?group=分组
    import:
      - nacos:common.yaml?group=Product

---
spring:
  # 引入nacos上的配置文件
  config:
    #prod环境启用此配置
    activate:
      on-profile: prod
    #导入nacos上的配置文件,格式:文件名?group=分组
    import:
      - nacos:common.yaml?group=Order
      - nacos:common.yaml?group=Product

这里不多讲,自行启动测试即可。

3. OpenFeign

参考文档:

3.1 OpenFeign概述

OpenFeign 是 Spring Cloud 生态系统提供的声明式 REST 客户端组件,其核心设计目标是将远程服务调用本地化。开发者只需通过 Java 接口声明服务契约并添加简单注解,即可像调用本地方法一样调用远程 HTTP 服务。这种设计显著降低了微服务间通信的复杂度,使开发者能聚焦业务逻辑而非通信细节15。

与传统 HTTP 客户端(如 RestTemplate)相比,OpenFeign 的核心优势体现在:

  • 声明式编程模型:通过接口和注解定义 HTTP 请求,避免样板代码
  • 自动服务发现集成:无缝对接 Eureka、Nacos 等注册中心
  • 客户端负载均衡:内建 Ribbon 或 Spring Cloud LoadBalancer 支持
  • 可扩展的编解码机制:支持 JSON、XML 等多种消息格式
  • 统一的熔断降级:与 Hystrix 或 Sentinel 集成提升容错能力

3.2 OpenFeign与Feign的关系

OpenFeign 是 Netflix Feign 项目的增强扩展,在保留原生 Feign 核心功能的基础上,深度整合 Spring MVC 注解体系和 Spring Cloud 基础设施。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

特性 OpenFeign Feign
注解支持 全面兼容Spring MVC注解 Feign原生注解
负载均衡 自动集成LoadBalancer或Ribbon 需手动集成Ribbon
配置方式 基于Spring Boot自动化配置 独立配置
服务发现 深度适配Spring Cloud注册中心 有限支持
活跃维护 Spring Cloud官方维护 已停止维护

3.3 工作原理

OpenFeign 通过动态代理注解解析实现声明式调用,其工作流程可分为七个关键阶段:

graph TD A[启动扫描] --> |"@EnableFeignClients"| B(接口扫描) B --> C[动态代理生成] C --> D[服务发现] D --> E[负载均衡] E --> F[请求构造] F --> G[HTTP调用] G --> H[响应处理]
  1. 启动扫描:Spring Boot 应用启动时,@EnableFeignClients 注解触发包扫描,定位所有 @FeignClient 标注的接口。
  2. 动态代理生成:JDK 动态代理机制为每个接口创建代理实例,拦截方法调用并转换为 HTTP 请求模板。
  3. 服务发现:从注册中心(如 Nacos)获取服务实例列表(例如 user-service 的 IP:Port 集合)。
  4. 负载均衡:通过集成组件(Ribbon 或 LoadBalancer)按策略(轮询、随机等)选择目标实例。
  5. 请求构造:解析方法上的 Spring MVC 注解(@GetMapping, @PathVariable),生成具体 HTTP 请求。
  6. HTTP调用:使用底层客户端(默认 JDK HttpURLConnection 或可选的 OkHttp)发送请求。
  7. 响应处理:根据返回类型,使用 HttpMessageConverter 反序列化响应体(如 JSON → Java 对象)

3.4 SpringCloud集成OpenFeign

3.4.1 依赖引入与启动配置

Maven 依赖配置:

<!--负载均衡-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!--OpenFeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

之后在启动类或配置类上添加注解@EnableFeignClients

@SpringBootApplication
@EnableFeignClients // 开启Feign客户端扫描
public class OrderMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderMainApplication.class,args);
    }

    //之前的其他配置...
}

对于@EnableFeignClients,我们来看看他的详细解释:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	// 扫描@FeignClient包地址,是basePackages属性的别名
	String[] value() default {};

	// 用户扫描Feign客户端的包,也就是@FeignClient标注的类,与value同义,并且互斥
	String[] basePackages() default {};

	// basePackages()的类型安全替代方案,用于指定要扫描带注释的组件的包。每个指定类别的包将被扫描。 考虑在每个包中创建一个特殊的无操作标记类或接口,除了被该属性引用之外没有其他用途。
	Class<?>[] basePackageClasses() default {};

	// 为所有假客户端定制@Configuration,默认配置都在FeignClientsConfiguration中,可以自己定制
	Class<?>[] defaultConfiguration() default {};

	// 可以指定@FeignClient标注的类,如果不为空,就会禁用类路径扫描
	Class<?>[] clients() default {};

}

3.4.2 声明式客户端定义

按照之前已有的代码,我们这里声明一个feign接口示例:

package cn.yunrain.nxz.feign;

import cn.yunrain.nxz.product.Product;
import cn.yunrain.nxz.product.dto.ProductListQueryDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**
 * @author nianxinzhuo
 * @date 2025/6/4 22:33
 * @description 商品服务feign客户端
 */
@FeignClient(
        name = "service-product", // 商品服务的名称,也是注册中心的服务标识
        path = "/product" // 全局路径前缀,相当于Controller中@RequestMapping中的值
)
public interface ProductFeignClient {
    // 在这里定义与商品服务相关的Feign客户端方法,需要与对应的Controller方法保持大体一致
    @PostMapping("/getProductListByIds")
    List<Product> getProductListByIds(@RequestBody ProductListQueryDto requestDto);
}

上述代码只是@FeignClient注解的简单解释,现在我们来看看@FeignClient注解的详细解释:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {

	// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性
	@AliasFor("name")
	String value() default "";

	// 该类的Bean名称 PS: 这个配置很重要后续会详细介绍
	String contextId() default "";

	// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性
	@AliasFor("value")
	String name() default "";

	// 弃用 被qualifiers()替代。
	@Deprecated
	String qualifier() default "";

	// 模拟客户端的@Qualifiers值。如果qualifier()和qualifiers()都存在,我们将使用后者,除非qualifier()返回的数组为空或只包含空值或空白值,在这种情况下,我们将首先退回到qualifier(),如果也不存在,则使用default = contextId + "FeignClient"。
	String[] qualifiers() default {};

	// 绝对URL或可解析主机名 PS: 使用这个配置后只能定型发送,没有负载均衡能力
	String url() default "";

	// 是否应该解码404而不是抛出FeignExceptions
	boolean decode404() default false;

	// 用于模拟客户端的自定义配置类。可以包含组成客户端部分的覆盖@Bean定义,默认配置都在FeignClientsConfiguration类中,可以指定FeignClientsConfiguration类中所有的配置
	Class<?>[] configuration() default {};

	// 指定失败回调类
	Class<?> fallback() default void.class;

	// 为指定的假客户端接口定义一个fallback工厂。fallback工厂必须生成fallback类的实例,这些实例实现了由FeignClient注释的接口。
	Class<?> fallbackFactory() default void.class;

	// 所有方法级映射使用的路径前缀
	String path() default "";

	// 是否将虚拟代理标记为主bean。默认为true。
	boolean primary() default true;
}

3.4.3 默认配置与替换

我们使用的很多框架都会有自己的默认配置,尤其是和SpringBoot集成的starter包,OpenFeign集成SpringBoot的starter包同样的也会给我们做很多默认配置。

FeignClientsConfiguration 类中,OpenFeign为我们做了很多默认配置,这个默认配置我们都可以自定义并且覆盖。

3.4.3.1 替换默认配置前置说明

自定义并且覆盖默认配置有两个维度,一个是使用全局配置另一个是每个FeignClient使用独立配置,这里以OpenFeign请求日志配置做介绍。

每个创建的 Feign 客户端都会创建一个logger。默认情况下,logger 的名字是用于创建Feign客户端的接口的全类名称。Feign 的日志只响应 DEBUG 级别。:

logging:
  level:
    #日志级别,这里的key是包名,你也可以是具体的某个类,根据自己的需要来即可
    cn.yunrain.nxz.feign: DEBUG

日志级别配置好了之后,现在我们要 Feign 要记录多少内容,可用的选择如下:

  • NONE, 没日志(默认)。
  • BASIC, 只记录请求方法和URL以及响应状态代码和执行时间。
  • HEADERS, 记录基本信息以及请求和响应头。
  • FULL, 记录请求和响应的header、正文和元数据。

之后我们创建一个配置类,为每个客户端配置 Logger.Level 对象:

package cn.yunrain.nxz.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author nianxinzhuo
 * @date 2025/6/6 11:20
 * @description Feign配置类
 */
@Configuration
public class FeignConfig {
    // 这里可以添加Feign相关的配置,例如超时时间、日志级别等
     @Bean
     public Logger.Level feignLoggerLevel() {
         // 设置Feign日志级别为FULL
         return Logger.Level.FULL;
     }
}

Feign日志配置参考

Feign的DEBUG日志级别输出

3.4.3.2 使用配置文件替换默认配置(推荐)

[!tip]

这种方式替换默认配置是最推荐的一种方案。

我们现在注释掉之前的配置类的方式,现在我们来试试配置文件的方式:

  1. 全局配置:

    spring:
      cloud:
        nacos:
        openfeign:
          client:
            config:
              # default是全局配置
              default:
                logger-level: full
    

    使用default就是全局的配置。

  2. 独立Feign配置

    spring:
      cloud:
        nacos:
        openfeign:
          client:
            config:
              # 这里是远程调用Feign客户端的名称
              productFeignClient:
                logger-level: full
    

    这里的productFeignClient就表示单独为哪一个Feign客户端做独立的配置。

    Feign客户端标识说明

两者分别测试,结果发现确实都是生效的。


现在我们再来看看源码,分析为什么推荐使用配置文件的方式

默认情况下,通过配置文件配置其实是最后才加载的,会将其它地方配置的信息全部顶掉有兴趣可以看看源码 FeignClientFactoryBean#configureFeign 这个方法会比其它配置加载后执行,会在这里实现替换:

配置加载顺序源码

3.4.3.3 使用独立配置替换默认配置

这里的独立配置和配置文件中的独立配置有点区别,这里的独立配置是通过编码的方式配置,且只给特定的Feign客户端配置

参考之前的配置类的方式:

@Configuration
public class FeignConfig {
    // 这里可以添加Feign相关的配置,例如超时时间、日志级别等
     @Bean
     public Logger.Level feignLoggerLevel() {
         // 设置Feign日志级别为FULL
         return Logger.Level.FULL;
     }
}

可见,上述配置类有了@Configuration注解,会使所有的Feign客户端都是用上这个配置,所以,这不叫独立配置

我们可以试验一下,新写一个Feign客户端:

package cn.yunrain.nxz.feign;

import feign.Headers;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;

/**
 * @author nianxinzhuo
 * @date 2025/6/6 13:07
 * @description 第三方API的Feign客户端
 */
@FeignClient(
        name = "service-third-party-api", // 第三方API服务的名称,也是注册中心的服务标识
        path = "v1", // 全局路径前缀,相当于Controller中@RequestMapping中的值
        contextId = "thirdPartyAPIFeignClient", // 用于区分同一服务中的不同Feign客户端,便于在多个Feign客户端中进行区分
        url = "https://api.bochaai.com"
)
public interface ThirdPartyAPIFeignClient {

    @PostMapping(value = "web-search",consumes = "application/json", produces = "application/json")
    String getWebSearch(@RequestHeader("Authorization") String Authorization,
                        @RequestBody String requestBody
    );
}

[!note]

当你在 @FeignClient 注解中配置了 url 属性后,Feign 将不会再根据微服务的服务名称进行服务发现,而是直接向指定的 URL 地址发起请求。

通常建议在 url 中包含协议和域名,而公共的请求前缀可以通过 path 属性指定,具体接口路径则由方法上的映射决定。

注释配置

现在我们编写一个测试类来调用,看看它是否打印了DEBUG的相关日志:

@Test
void testThirdPartyAPIFeignClient() {

    String webSearch = thirdPartyAPIFeignClient.getWebSearch(
            "Bearer sk-xxx",
            "{\"query\":\"openfeign的讲解\"}"
    );
    System.out.println("Web Search Response: " + webSearch);
}

执行结果

可见之前的配置类确实是生效了,并不算独立配置。

所以要想把他变成独立配置,也很简单,去掉@Configuration注解,并且在@FeignClient注解上加上configuration配置,值就指向你的没有@Configuration注解的配置类:

public class FeignConfig {
    // 这里可以添加Feign相关的配置,例如超时时间、日志级别等
     @Bean
     public Logger.Level feignLoggerLevel() {
         // 设置Feign日志级别为FULL
         return Logger.Level.FULL;
     }
}

再次执行之前的测试,执行结果如下可见确实没有在输出DEBUG相关的日志了。

此时,你再给Feign客户端配置单独的配置:

独立配置

此时你再次执行测试类,发现打印了DEBUG的相关日志,但是没有配置FeignConfig配置的客户端,则不会打印。

3.4.4 Feign对@QueryMap的支持

Spring Cloud OpenFeign 提供了一个等价的 @SpringQueryMap 注解,用于将POJO或Map参数注解为查询参数map。

例如:

package cn.yunrain.nxz.product.dto;

import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
 * @author nianxinzhuo
 * @date 2025/6/6 16:22
 * @description 根据条件查询商品列表信息
 */
@Data
public class ProductListByConditionQueryDto implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 商品ID
     */
    private Long id;
    /**
     * 商品名称
     */
    private String productName;
}
@GetMapping("/getProductListByCondition")
  public List<Product> getProductListByCondition(@SpringQueryMap ProductListByConditionQueryDto dto) {
      // 模拟根据商品名称和类型查询商品信息
      List<Product> productList = productService.getProductListByCondition(dto);
      return productList;
  }

对应的Feign客户端接口:

package cn.yunrain.nxz.feign;

import cn.yunrain.nxz.product.Product;
import cn.yunrain.nxz.product.dto.ProductListByConditionQueryDto;
import cn.yunrain.nxz.product.dto.ProductListQueryDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**
 * @author nianxinzhuo
 * @date 2025/6/4 22:33
 * @description 商品服务feign客户端
 */
@FeignClient(
        name = "service-product", // 商品服务的名称,也是注册中心的服务标识
        path = "/product", // 全局路径前缀,相当于Controller中@RequestMapping中的值
        contextId = "productFeignClient" // 用于区分同一服务中的不同Feign客户端,便于在多个Feign客户端中进行区分
)
public interface ProductFeignClient {
  	//其他接口...
    /**
     * 根据条件查询商品列表信息
     *
     * @param dto 商品列表查询条件
     * @return 商品信息列表
     */
    @GetMapping("/getProductListByCondition")
    List<Product> getProductListByCondition(@SpringQueryMap ProductListByConditionQueryDto dto);
}

官方讲解

3.4.5 OpenFeign超时配置

一般在远程调用的过程中,有两个必经阶段,第一个是建立远程连接,第二个则是等待远程接口响应数据。

无论是在远程连接建立还是等到接口响应数据,都需要耗时的,甚至有可能超时。

在OpenFeign中有两个超时参数:

  1. connectTimeout:连接建立超时。
  2. readTimeout:数据读取超时,即从连接建立时开始应用,当返回响应的时间过长时就会被触发。

这里OpenFeign超时配置也主要是对connectTimeoutreadTimeout的配置。

3.4.5.1 使用配置文件配置(推荐)

使用配置文件配置是最推荐的,也是在项目中使用最多的。

spring:
  cloud:
    openfeign:
      client:
        config:
          # default是全局配置
          default:
            # 连接超时时间
            connect-timeout: 3000
            # 读取超时时间
            read-timeout: 3000

现在我们来调整商品服务,让他休眠5秒,模拟数据响应耗时:

休眠5s

请求创建订单服务接口:

请求耗时

读取超时

可见配置确实生效了。

还记得之前通过配置文件的方式单独配置设置,这里我们也通过配置文件的方式单独配置一下商品微服务的超时时间:

商品微服务的OpenFeign独立配置

请求耗时

读取超时

可见仍然是读取超时,可见,当OpenFeign的全局配置和微服务独立配置都配置了相同的配置项之后,系统使用的是独立配置

3.4.5.2 使用@FeignClient配置超时时间

使用@FeignClient注解的configuration属性来指定配置类。

首先,创建一个配置类,继承自feign.Request.Options类:

package cn.yunrain.nxz.config;

import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * @author nianxinzhuo
 * @date 2025/6/6 11:20
 * @description Feign配置类
 */
public class FeignConfig {
    //其他配置

    /**
     * 设置Feign请求的连接超时时间和读取超时时间
     *
     * @return Feign请求选项
     */
     @Bean
    public Request.Options feignRequestOptions() {
         // 设置Feign请求的连接超时时间和读取超时时间
         return new Request.Options(2, TimeUnit.SECONDS, 2, TimeUnit.SECONDS, false);
     }
}

之后,将这个配置类指定到@FeignClient注解的configuration属性中:

指定配置

注释掉之前配置文件的超时时间配置方式之后,请求接口并查看耗时:

请求接口并查看耗时

3.4.5.3 为单独接口设置超时时间

这种方式优先级比使用配置文件更高

在feign接口里加入Request.Options这个参数就可以单独为接口单独设置超时时间了:

@PostMapping("/getProductListByIds")
List<Product> getProductListByIds(@RequestBody ProductListQueryDto requestDto, 
                                  Request.Options requestOptions);

之后在调用处传入这个值即可:

List<Product> productList = productFeignClient.getProductListByIds(
                new ProductListQueryDto().setProductIdList(dto.getProductIdList()),
                new Request.Options(1, TimeUnit.SECONDS,1,TimeUnit.SECONDS,false)
                );

现在我们把配置文件的方式也开着:

配置文件超时时间设置

结果以及耗时

可见单独接口设置的超时时间配置的优先级要高于配置文件方式。

3.4.6 OpenFeign重试配置

OpenFeign的重试机制通过Retryer接口实现,默认情况下不启用重试功能。当HTTP请求失败时,可以根据配置的重试策略自动重新发送请求。

OpenFeign提供了几种内置的重试器:

  1. Retryer.NEVER_RETRY

    // 默认重试器,不进行重试
    Retryer NEVER_RETRY = new Retryer() {
    
        @Override
        public void continueOrPropagate(RetryableException e) {
          throw e;
        }
    
        @Override
        public Retryer clone() {
          return this;
        }
      };
    

    Retryer接口中的NEVER_RETRY实现

  2. Retryer.Default

    // 默认重试器实现
    class Default implements Retryer {
    
        private final int maxAttempts;
        private final long period;
        private final long maxPeriod;
        int attempt;
        long sleptForMillis;
    
        public Default() {
          this(100, SECONDS.toMillis(1), 5);
        }
    
        public Default(long period, long maxPeriod, int maxAttempts) {
          this.period = period;
          this.maxPeriod = maxPeriod;
          this.maxAttempts = maxAttempts;
          this.attempt = 1;
        }
    
        // visible for testing;
        protected long currentTimeMillis() {
          return System.currentTimeMillis();
        }
    
        public void continueOrPropagate(RetryableException e) {
          if (attempt++ >= maxAttempts) {
            throw e;
          }
    
          long interval;
          if (e.retryAfter() != null) {
            interval = e.retryAfter() - currentTimeMillis();
            if (interval > maxPeriod) {
              interval = maxPeriod;
            }
            if (interval < 0) {
              return;
            }
          } else {
            interval = nextMaxInterval();
          }
          try {
            Thread.sleep(interval);
          } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
          }
          sleptForMillis += interval;
        }
    
        /**
         * Calculates the time interval to a retry attempt.<br>
         * The interval increases exponentially with each attempt, at a rate of nextInterval *= 1.5
         * (where 1.5 is the backoff factor), to the maximum interval.
         *
         * @return time in milliseconds from now until the next attempt.
         */
        long nextMaxInterval() {
          long interval = (long) (period * Math.pow(1.5, attempt - 1));
          return Math.min(interval, maxPeriod);
        }
    
        @Override
        public Retryer clone() {
          return new Default(period, maxPeriod, maxAttempts);
        }
      }
    

    Retryer接口中的Default重试器


里面的默认的重试器了解了之后,我们现在就来配置重试机制:

  1. 全局重试

    package cn.yunrain.nxz.config;
    
    import feign.Logger;
    import feign.Request;
    import feign.Retryer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author nianxinzhuo
     * @date 2025/6/6 11:20
     * @description Feign配置类
     */
    @Configuration
    public class FeignConfig {
        // 其他配置...
    
      	// 设置默认的重试器
        @Bean
        public Retryer feignRetryer() {
            // 初始间隔100ms,最大间隔1秒,最大重试次数5次
            return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1), 5);
        }
    }
    

    由于之前我在接口调用处添加了超时配置(1s),且远程接口也有休眠配置(5s);这里直接启动服务测试即可:

    之前模拟的读取超时配置

    结果测试

    可见当每次请求接口超时之后,确实进行了重试,且重试了5次。

  2. 如果你是针对特定Feign客户端的独立配置,那么你取消掉@Configuration注解,且在特定Feign客户端中的configuration属性指定为配置类即可。

  3. 配置文件的方式:

    spring:
      cloud:
        openfeign:
          client:
            config:
               #default是全局配置
    #          default:
    #            # 连接超时时间
    #            connect-timeout: 3000
    #            # 读取超时时间
    #            read-timeout: 3000
              productFeignClient:
                connect-timeout: 4000
                read-timeout: 4000
                # 重试配置
                retryer: feign.Retryer.Default
    

除此之外,你还可以自定义一个重试器,仅需实现Retryer接口来创建自定义重试逻辑:

public class CustomRetryer implements Retryer {
    private final int maxAttempts;    // 最大重试次数
    private final long backoff;       // 基础退避时间(毫秒)
    private int retryCount;          // 当前重试计数器

    public CustomRetryer(int maxAttempts, long backoff) {
        this.maxAttempts = maxAttempts;
        this.backoff = backoff;
        this.retryCount = 0;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        // 步骤1: 检查是否超过最大重试次数
        if (retryCount++ >= maxAttempts) {
            throw e;  // 超过限制,抛出原始异常
        }

        // 步骤2: 计算等待时间并执行退避策略
        try {
            // 指数退避:等待时间 = 基础时间 * 2^(重试次数-1)
            long waitTime = backoff * (long) Math.pow(2, retryCount - 1);
            Thread.sleep(waitTime);
        } catch (InterruptedException ignored) {
            // 恢复中断状态
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public Retryer clone() {
        return new CustomRetryer(maxAttempts, backoff);
    }
}

3.4.7 OpenFeign拦截器

这里的拦截器有两个维度,一个是RequestInterceptor(请求拦截器)、一个是ResponseInterceptor(响应拦截器)

请求拦截器会在发送请求之前,对请求进行处理,而响应拦截器则是在请求接受到之前对响应进行处理。

这里我们写一个示例看一下即可:通过请求拦截器设置token:

package cn.yunrain.nxz.config.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * @author nianxinzhuo
 * @date 2025/6/7 10:55
 * @description token请求拦截器
 */
@Slf4j
public class RequestTokenInterceptor implements RequestInterceptor {

    /**
     * 在请求发送之前执行的拦截逻辑
     *
     * @param template 请求模板
     */
    @Override
    public void apply(RequestTemplate template) {
        log.info("开始执行请求拦截器...");
        template.header("X-Token", UUID.randomUUID().toString());
    }
}

请求拦截器写好之后,你就要配置到服务中,你可以在配置文件中配置如下:

spring:
    openfeign:
      client:
        config:
           #default是全局配置
#          default:
          productFeignClient:
            # 请求拦截器
            request-interceptors:
              - cn.yunrain.nxz.config.interceptor.RequestTokenInterceptor

你也可以直接使用注解@Component,将RequestTokenInterceptor注册为一个Bean。因为官方文档提到:

官方文档

现在再在任意远程接口处从请求头中获取X-Token,看是否是成功设置:

获取请求中的X-Token

请求并查看结果:

接口调用方

接口接收方

3.4.8 OpenFeign的Fallback机制

[!warning]

这一小节需要配合Sentinel才能实现,所以本小节不做讲解。详情参考:4.5.1.3 OpenFeign远程调用异常处理

4. Sentinel

参考文档:

4.1 Sentinel简介

Sentinel(中文名:哨兵)是一款由阿里巴巴开发的开源库,用于应用程序的流量控制、熔断降级、系统负载保护和实时监控。它旨在帮助开发人员构建稳健的分布式系统,以确保高可用性、性能稳定和可靠的微服务架构。

Sentinel 提供了以下主要功能:

  1. 流量控制:Sentinel 可以限制应用程序的请求速率,以确保系统不会因过多的请求而不稳定。它支持基于 QPS(每秒查询数)的流量控制,还可以根据资源名称、访问来源等进行流量控制的细粒度配置。
  2. 熔断降级:Sentinel 提供了熔断降级机制,可以监测应用程序中的故障率和异常率。如果异常率超过了阈值,Sentinel 可以暂时关闭对资源的访问,防止故障扩散,提高系统的稳定性。当资源不再发生异常时,Sentinel 会逐渐恢复访问。
  3. 系统负载保护:Sentinel 可以监控系统的负载情况,防止系统超载。它可以根据系统负载动态调整流量控制策略,确保系统资源充足。
  4. 实时监控和统计:Sentinel 提供了实时的监控和统计信息,开发人员可以轻松地查看应用程序的运行状态,包括流量、熔断降级情况、资源访问情况等。这有助于及时发现和解决问题。
  5. 灵活的配置:Sentinel 允许开发人员通过代码或配置文件来定义流量控制和熔断降级规则,以适应不同的应用场景和需求。

Sentinel 通常用于微服务架构中,可以集成到各种应用程序中,包括Java、Go、Node.js等。它可以帮助开发人员实现服务保护和稳定性,防止不稳定的服务影响整个系统的可用性,并提供实时监控和警报功能,以便快速响应问题。因此,Sentinel 在构建大规模分布式系统时是一个有力的工具。

4.2 微服务的雪崩效应

微服务的雪崩效应是指在一个使用微服务架构的系统中,当一个微服务出现故障或不可用时,这个故障会迅速扩散到其他微服务,导致整个系统的功能受到严重影响或完全不可用的情况。这种效应通常表现为系统的性能急剧下降,请求响应时间变得极长,甚至无法正常提供服务。

微服务架构的特点是将系统拆分成多个小型的、相对独立的微服务单元,每个微服务都有自己的数据库、逻辑和接口。这种分布式的特性使得系统更加容易扩展和维护,但也带来了一些潜在问题,其中之一就是雪崩效应。

微服务的雪崩效应可能发生的原因包括:

  1. 依赖关系:微服务之间通常存在依赖关系,一个微服务的故障可能会导致依赖它的其他微服务无法正常工作。
  2. 超时和重试:当一个微服务不可用时,其他微服务可能会发起请求,但由于故障的微服务无法响应,这些请求会超时。为了处理这些超时情况,其他微服务可能会发起更多的重试请求,导致故障的微服务负载增加,最终可能导致雪崩效应。
  3. 缓存失效:如果微服务之间使用缓存来提高性能,当一个微服务故障并且缓存失效时,其他微服务可能需要大量的资源来重新生成缓存,增加了系统的负载。

为了减轻微服务的雪崩效应,可以采取以下策略

  1. 服务降级:当系统出现故障或负载过高时,可以临时降低某些微服务的功能,以确保系统的核心功能仍然可用。
  2. 断路器模式:使用断路器模式来监控微服务之间的通信,当故障率超过一定阈值时,断开请求,防止故障扩散。
  3. 限流和排队:通过限制同时处理的请求数量,可以减轻对故障微服务的压力,也可以使用队列来平滑处理请求。
  4. 自动化监控和恢复:建立监控系统,能够及时检测微服务的健康状况,并自动触发恢复措施,减少人工干预。

微服务的雪崩效应是分布式系统中需要注意的一个重要问题,合理的设计和管理可以减少这种效应对系统的影响。


断路器模式(Circuit Breaker Pattern) 是一种用于构建稳健的分布式系统的设计模式,主要用于处理微服务架构中的故障和异常情况,以防止故障在整个系统中传播,从而提高系统的可用性和鲁棒性。断路器模式的工作原理类似于电路中的断路器,当检测到故障或异常时,它可以打开或关闭某个服务的访问,以防止系统进一步受到影响。

以下是断路器模式的详细解释:

  1. 三种状态

    • 关闭状态(Closed):初始状态或者在正常情况下的状态。在这个状态下,断路器允许请求通过,并且会监测请求的响应。如果请求成功率高于某个阈值(比如95%),则断路器保持关闭状态。如果请求失败率超过阈值,断路器进入“打开状态”。
    • 打开状态(Open):在这个状态下,断路器会立即拒绝所有的请求,不再尝试去调用故障的服务。通常,断路器会设置一个计时器,在一段时间后自动进入“半开状态”。
    • 半开状态(Half-Open):在一段时间后,断路器会自动进入半开状态,它会允许部分请求通过,并检测它们的响应。如果这些请求都成功了,断路器会恢复到“关闭状态”,否则会回到“打开状态”。
  2. 故障检测:断路器模式通过监测请求的成功和失败来检测服务的故障。如果连续的请求失败超过了一定阈值,断路器就会打开,阻止进一步的请求,以避免对故障的服务施加过多压力。

  3. 故障恢复:断路器在打开状态下允许一些请求通过,以便检测服务是否已经恢复正常。如果在半开状态下的请求成功,断路器将会逐渐恢复到关闭状态,重新允许所有请求。

  4. 降级操作:在打开状态下,可以选择提供降级的备用操作或者返回缓存数据,以维持系统的一定功能,而不完全失败。

断路器模式的优势包括:

  • 防止故障的蔓延:当一个服务故障时,断路器能够快速停止对该服务的请求,防止故障扩散到整个系统。
  • 减少对故障服务的访问:通过关闭断路器,可以减轻故障服务的负担,使其有机会恢复正常。
  • 提高系统的可用性:通过快速切换状态和自动恢复,断路器可以提高系统的可用性,减少服务不可用的时间。

[!tip]

这里其他概念就不过多讲解了,有兴趣的可以直接看官方文档,文档中说的很清楚:Sentinel的快速开始

4.3 控制台部署

参考文档:Sentinel控制台

使用 SpringCloud Alibaba 家族组件,要注意一下版本兼容问题,避免出现一些奇怪的问题,基于之前我的SpringCloud Alibaba的版本,需要查看一下Sentinel应该用什么版本:

Sentinel版本选择

之后在Github上下载对应的jar包即可:

jar包下载

下载好了之后,直接使用命令启动:

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
  • -Dserver.port=8180:sentine 服务控制台端口
  • -Dsentinel.dashboard.auth.username=xxx:sentine 控制台登录账号,不设置默认sentinel
  • -Dsentinel.dashboard.auth.password=xxx:sentine 控制台登录密码,不设置默认sentinel
  • -Dcsp.sentinel.dashboard.server=localhost:8180:将控制台自身注册到server
  • -Dproject.name=sentinel-dashboard:控制台服务自己项目名称

控制台登陆

登陆成功界面

[!caution]

如果你要想本地代码连接远程的sentinel,目前是不太方便的,十分麻烦。所以,如果你要使用sentinel和本地代码测试,还是得在本地启动一下sentinel控制台的jar。

同时在Sentinel启动期间,禁止打开任意代理软件,否则会连接不上的

4.4 SpringCloud集成Sentinel

添加对应Maven依赖:

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

在配置文件中配置好sentinel的控制台连接:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8081
      #sentinel默认采用懒加载,要执行一次请求之后,sentinel控制台上才会有对应数据,所以这里我们直接设置为程序启动就加载
      eager: true

eager配置相关解释

现在基本的配置准备好了之后,我们现在就要一一来看看sentinel的特点应用:

  1. 流量控制
  2. 熔断降级
  3. 系统保护
  4. 来源访问控制
  5. 热点参数

[!warning]

在sentinel的讲解中,基本上都是采用声明式(注解)的方式编写代码,很少涉及编程式的方式。

4.4.1 流量控制

流量控制很理解,就是控制你访问资源的频率,如果请求资源的频率过高,就按照sentinel的执行逻辑进行错误返回即可。

现在我们使用注解@SentinelResource来简单的实验一下,给某个接口加上这个注解:

@GetMapping("/getNacosOrderConfig")
@SentinelResource(value = "getNacosOrderConfig")
public String getNacosOrderConfig() {
    // 返回Nacos配置的值
        return "Nacos配置 - 命名空间: " + orderProperties.getNameSpace() +
            ", 分组: " + orderProperties.getGroup() +
            ", URL: " + orderProperties.getUrl();
}

然后启动程序查看sentinel控制台:

sentinel控制台

发现此时并没有资源展示出来,这是因为你需要再请求一下对应的接口,这里才会有数据。

资源信息查看


现在我们来简单讲解@SentinelResource注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {

  	// 资源名称,用于标识受 Sentinel 保护的资源, 若不指定则会自动使用类名或方法签名作为资源名
    String value() default "";

   	// 资源调用的流量类型,默认为 EntryType.OUT,表示出站调用(对外部资源的调用),EntryType.IN,表示入站调用(被外部调用)
    EntryType entryType() default EntryType.OUT;

	// 资源类型,默认为 0,通常用户可以自定义不同的数值来区分资源类型   
    int resourceType() default 0;

    // 指定发生流控、熔断降级时的处理方法名,该方法必须与原方法在同一个类中,除非指定了 blockHandlerClass
    String blockHandler() default "";

    // 指定 blockHandler 方法所在的类;
    // 默认为空数组,表示 blockHandler 方法与原方法在同一个类中;当 blockHandler 方法位于外部类时,需要指定此属性
    Class<?>[] blockHandlerClass() default {};

    // 指定接口出现异常时的降级处理方法名;
    // 默认为空,表示不指定特殊处理方法; 用于处理业务异常,而非流控或熔断引起的异常
    String fallback() default "";

    // 指定默认的降级处理方法名;
    // 当未指定 fallback 或 fallback 方法不可用时使用
    String defaultFallback() default "";

   	// 指定 fallback 方法所在的类;
    // 默认为空数组,表示 fallback 方法与原方法在同一个类中; 当 fallback 方法位于外部类时,需要指定此属性
    Class<?>[] fallbackClass() default {};

    // 指定需要追踪的异常类型
    // 默认为 {Throwable.class},表示追踪所有异常; 只有这些指定的异常类型会触发 fallback 处理逻辑
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
    
    // 指定需要忽略的异常类型
    // 默认为空数组,表示不忽略任何异常; 这些异常发生时不会触发 fallback 处理逻辑,会直接抛出
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

了解完@SentinelResource注解之后,现在我们来给之前的资源设置流控规则。

给指定资源添加流控规则

新增流控规则

新增流控规则的时候,有很多名称概念需要简单讲解一下:

  1. 资源名:唯一名称,默认请求路径(如:http://localhost:8080/order/getNacosOrderConfig)
  2. 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)
  3. 阈值类型/单机阈值
    • QPS(每秒钟的请求数量):当调用该接口的QPS达到了阈值的时候,进行限流;
    • 线程数:当调用该接口的线程数达到阈值时,进行限流
  4. 是否集群:我不需要集群,实际项目中看自己的需求
  5. 流控模式
    • 直接:接口达到限流条件时,直接限流
    • 关联:当关联的资源达到阈值时,就限流自己
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)[api级别的针对来源]
  6. 流控效果
    • 快速失败:直接失败
    • Warm Up:即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值
    • 排队等待:选择一个等待超时时间

上述分别对每个大规则进行了简单讲解,现在我们再讲讲其中的具体的一些小规则


4.4.1.1 阈值类型讲解

单机阈值比较好理解,就是每秒对QPS/线程数的控制。

QPS与线程数的区别在于

  • QPS单纯的代表每秒的访问次数,只要访问次数到达一定的阈值,这进行限流操作
  • 线程数,代表的是每秒内访问改api接口的线程数,如果该接口的操作比较长,当排队的线程数到达阈值的时候,进行限流操作,相反的如果接口的操作很快,即是没秒内的操作很快,同样不会进行限流操作
  • QPS可以简单的理解为访问次数,但是线程数是和接口处理的快慢有关的。

4.4.1.2 流控模式

流控模式分为直接、关联和链路。

  1. 直连模式:直接模式理解起来比较简单,就是单纯的对当前资源的流量控制,这里不再做过多的解释。

  2. 关联模式:当关联的资源达到阈值时,限流自己

    如下图的配置,当A的QPS的阈值到达10的时候,B就会被限流。

    关联模式

  3. 链路模式:链路模式关注的是调用链路上下文,可以精确控制从特定入口资源到当前资源的调用量。

    例如我有一个createOrder资源被保护:

    @SentinelResource(value = "createOrderImpl",blockHandler = "createOrderBlockHandler")
    @Override
    public Order createOrder(OrderAddDto dto) {
        // 创建订单的具体实现
    }
    

    这里我指定了触发流控时的方法处理createOrderBlockHandler,具体实现:

    /**
     * 限流降级处理方法
     *
     * @param dto 订单添加请求参数
     * @param e   异常信息
     * @return 降级后的订单实体
     */
    public Order createOrderBlockHandler(OrderAddDto dto, BlockException e) {
        log.error("限流降级处理: {}", e.getMessage());
        // 返回一个默认的订单对象或抛出自定义异常
        Order order = new Order();
        order.setId(-1L); // 设置一个特殊的ID表示降级
        order.setUserId(dto.getUserId());
        order.setNickname("降级用户");
        order.setAddress("降级地址");
        order.setTotalAmount(BigDecimal.ZERO);
        return order;
    }
    

    [!important]

    @SentinelResource注解中blockHandler属性指定的方法的参数以及返回值必须要与调用处的一致,且方法参数要多一个BlockException类型的参数

    之后,我们修改一下配置问卷,添加上如下配置:

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: 127.0.0.1:8081
          eager: true
          # 关闭 Web Context 的统一入口,这对链路模式至关重要
          web-context-unify: false
    

    [!important]

    这里的web-context-unify必须设置为false,否则链路模式不会生效的。

    我的接口是/order/createOrder,其中createOrder是具体需要保护的实现类资源,如图:

    接口调用链

    现在的调用链如下:资源/order/createOrder---->资源createOrderImpl。

    所以现在我们设置链路限流的模式:

    链路模式资源流控规则设置

    最后直接使用压测工具压测,保证QPS大于你设置的值即可:

    执行结果

4.4.1.3 流控效果

流控效果分为快速失败、预热(Warm Up)和排队等待。

  1. 快速失败:快速失败理解起来比较简单,就是限流的时候直接提示。

  2. 预热模式(Warm Up):根据codeFactor(冷加载因子,默认为3)的值,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。这个是与你设置的QPS强相关的。例如我设置如图

    流控效果设置

    因此上图的配置就是系统初始化的默认QPS阈值为10 / 3,即为3,也就是刚开始的时候阈值只有3,当经过5s后,阈值才慢慢提高到10;这种情况主要是为了保护系统,例如在秒杀系统的开启瞬间,会有很多流量上来,很可能会把系统打挂,预热方式就是为了保护系统,可以慢慢的把流量放进来,慢慢的把阈值增长到设定值。

    测试结果

  3. 排队等待模式:排队等待模式也很好理解,比如你的QPS阈值设置为10,超时等待时间设置为10,那么系统就每100ms处理一个请求。因为你的QPS是10,相当于1000ms/10=100ms;

    排队处理流程:

    • 第 1 秒内到达的前 10 个请求立即处理
    • 第 1 秒内到达的第 11 个请求需等待 100ms
    • 第 1 秒内到达的第 12 个请求需等待 200ms
    • ...以此类推
    • 如果某个请求计算出的等待时间超过 10 秒,则直接拒绝。

4.4.2 熔断降级

参考文档:Sentinel熔断降级官方文档

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

各个服务之间的调用链

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩

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

[!note]

总结:熔断降级是通过在客户端快速阻断对不稳定服务的调用,防止局部故障扩散引发系统雪崩,保障整体高可用性的关键措施。


新增熔断规则

新增熔断规则的时候,有很多名称概念需要简单讲解一下:

  1. 资源名:唯一名称,默认请求路径(如:http://localhost:8080/order/getNacosOrderConfig)

  2. 熔断策略

    • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
    • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
    • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

    注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

  3. 熔断时长:熔断器开启的持续时间,单位为秒。在此期间,所有对该资源的请求会被自动拒绝,时间结束后,熔断器会进入"半开"状态,尝试恢复服务调用。

  4. 最小请求数:触发熔断的最小请求数阈值。在统计时长内,只有请求数量达到或超过此值才会触发熔断,这是为了防止少量请求的偶发问题导致熔断,默认值为5,表示至少需要5个请求才可能触发熔断。

  5. 统计时长:统计的时间窗口长度,单位为毫秒。在这个时间窗口内收集的数据用于熔断判断。默认值为1000ms。

上述分别对每个大规则进行了简单讲解,现在我们对每一个熔断策略进行具体的讲解。

4.4.2.1 慢比例调用熔断策略

使用之前流量控制的相关代码,现在我们来设置一下熔断降级中的慢比例调用熔断策略,我的配置如下:

慢比例调用熔断策略配置

规则解读:在任意2秒时间窗口内(统计时长),系统监控createOrderImpl的调用,假设在这2秒内有50个请求调用了createOrderImpl资源,如果其中有5个(最小请求数)或更多请求的响应时间(最大RT)超过15ms,即10%(比例阈值)或更多,系统会立即触发熔断,接下来的2秒(熔断时长)内所有对createOrderImpl的调用都会被拒绝。

执行结果

4.4.2.2 异常比例熔断策略

当一个资源在指定时间窗口内的异常比例超过设定阈值时,Sentinel 会自动触发熔断,暂时切断对该资源的访问,以防止错误的扩散和系统资源的浪费。

例如我有如图配置:

异常比例熔断策略配置

在统计时长1000ms的时间窗口中,如果请求的数量至少大于5,比如有50个请求,那么如果此时系统抛出的业务异常大于或等于5个[请求数量(50)*比例阈值(0.1)],那么就会触发熔断。

4.4.2.3 异常数熔断策略

异常数熔断策略和异常比例熔断策略很类似,只不过一个是比例,一个是绝对的数,这里就不过多展开了。

4.4.3 系统规则

参考文档:系统自适应规则官方文档

Sentinel 系统自适应保护从整体维度应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口 QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则支持以下的阈值类型:

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

系统规则创建

[!caution]

这里的系统规则设置之后,如果超过阈值,系统并不会简单地拒绝所有请求,而是采用自适应流控算法,根据系统负载情况动态调整允许通过的请求量。

4.4.4 热点规则

参考文档:Sentinel热点参数限流官方文档

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

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

例如我现在有一个接口需要进行热点规则控制:

@GetMapping("/getNacosOrderConfig")
@SentinelResource(value = "getNacosOrderConfigController")
public String getNacosOrderConfig(@RequestParam(value = "yamlName", required = false) String yamlName) {
    // 返回Nacos配置的值
    return "Nacos配置-" + yamlName + "- 命名空间: " + orderProperties.getNameSpace() +
            ", 分组: " + orderProperties.getGroup() +
            ", URL: " + orderProperties.getUrl();
}

热点规则创建

新增热点规则

基本参数项解释:

  • 资源名:唯一名称,默认请求路径(如:http://localhost:8080/order/getNacosOrderConfig)。

  • 限流模式:目前Sentinel只支持QPS模式。

  • 参数索引:也就是你接口代码中的请求参数的位置,默认从0开始。

    例如我有接口:

    @GetMapping("/getNacosOrderConfig")
    @SentinelResource(value = "getNacosOrderConfigController")
    public String getNacosOrderConfig(@RequestParam(value = "yamlName", required = false) String yamlName) {}
    

    如果我要给yamlName参数配置热点规则,那么参数索引这里就是0。

  • 单机阈值:即QPS的值。

  • 统计窗口时长:统计的时间窗口长度,单位为秒。在这个时间窗口内收集的请求数用于热点判断。

  • 是否集群:这里我选择否,根据自己实际需要选择即可。

参数额外项解释:

  • 参数类型:指定的热点参数的类型,目前只能是基本数据类型和String。
  • 参数值:指定热点参数的值。
  • 限流阈值:即指定的热点参数的值的QPS的阈值。

例如对于之前的接口,我设置的热点规则如图:

热点规则

规则解读:针对"getNacosOrderConfigController"资源,使用QPS模式限制yamlName参数(索引0),每1秒内同一个yamlName值(同索引参数的相同的值)最多允许2次调用,超出将被限流。

例如我现在请求地址如下:

http://localhost:8080/order/getNacosOrderConfig?yamlName=sentinel.yaml

对于同索引参数的相同的值,也就是yamlName=sentinel.yaml,当我在1秒中,一直用yamlName=sentinel.yaml请求的时候,如果超过了2次,那么我就会被限流。

限流了

参数流量异常

如果我此时换个yamlName的值,那么能够正常,但是也要保证在规定时间窗口中,不能超过其QPS阈值。

如果我压根就不用这个yamlName参数,那么这个热点配置是不会生效的。这一个可以自行测试。

下面我们来设置一下参数额外项。

比如我想要指定一个特定参数的值不被限流,但是其他的都被限流,那么应该怎样设置呢?

此时你就要用到参数额外项了。

额外参数设置

此时,我将参数索引为0的参数,也就是yamlName这个参数的指定了一个特殊值为vip.yaml,当为这个值的时候,他的限流阈值为100W,已经很大了,基本上可以说是不限流了。之后访问地址:

http://localhost:8080/order/getNacosOrderConfig?yamlName=vip.yaml

结果可自行测试,确实约等于不限流。

同时,如果你还想设置特定值时直接无法访问,那么你完全就可以把阈值设置0就可以了。

4.4.5 授权规则

参考文档:Sentinel来源访问控制(黑白名单)官网文档

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的黑白名单控制的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过。

若配置白名单则只有请求来源位于白名单内时才可通过。

若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:

  • resource:资源名,即限流规则的作用对象
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式

[!warning]

一般我们会将这个与网关进行配合处理,这里就不多展开,请参考Gateway与Sentinel授权规则

4.5 Sentinel异常

在Sentinel体系中,有自己的异常定义,异常结构如图:

Sentinel相关异常

异常类名 功能 触发场景
BlockException 阻塞异常基类 所有Sentinel限流、降级、系统保护的基础异常。
FlowException 流量控制异常 当系统访问的流量超过你的流控规则设置的阈值时触发
ParamFlowException 热点参数限流异常 当系统访问热点参数的QPS超过你的热点规则设置的阈值时触发
DegradeException 熔断降级异常 当系统超过你的熔断规则设置的阈值时触发
AuthorityException 权限控制异常 当系统来源不满足你的授权规则时触发
SystemBlockException 系统保护异常 当系统不满足你的系统规则时触发

4.5.1 Sentinel异常处理机制

graph TD A["抛出异常"] --> B["BlockException"] B --> C["Web接口"] B --> D["@SentinelResource"] B --> E["OpenFeign调用"] B --> F["SphU 硬编码"] C --> G["SentinelWebInterceptor"] D --> H["SentinelResourceAspect"] E --> I["SentinelFeign.builder()"] F --> J["try-catch"] G --> K["默认BlockExceptionHandler"] H --> L["blockHandler"] H --> M["fallback"] I --> N["fallback"] K --> O["自定义 BEH"] L --> P["兜底回调"] M --> P N --> P P --> Q["SpringBoot异常处理"] style A fill:#E74C3C,stroke:#E74C3C,color:white style B fill:#E74C3C,stroke:#E74C3C,color:white style C fill:#2ECC71,stroke:#2ECC71,color:white style D fill:#2ECC71,stroke:#2ECC71,color:white style E fill:#2ECC71,stroke:#2ECC71,color:white style F fill:#2ECC71,stroke:#2ECC71,color:white style G fill:#34495E,stroke:#34495E,color:white style H fill:#34495E,stroke:#34495E,color:white style I fill:#34495E,stroke:#34495E,color:white style J fill:#34495E,stroke:#34495E,color:white style K fill:#8E44AD,stroke:#8E44AD,color:white style L fill:#3498DB,stroke:#3498DB,color:white style M fill:#3498DB,stroke:#3498DB,color:white style N fill:#3498DB,stroke:#3498DB,color:white style O fill:#8E44AD,stroke:#8E44AD,color:white style P fill:#8E44AD,stroke:#8E44AD,color:white style Q fill:#8E44AD,stroke:#8E44AD,color:white

4.5.1.1 普通web接口异常处理流程

当时给普通web接口设置了流控、熔断等规则的时候,发送请求时,会被SentinelWebInterceptor拦截器给拦截,进行规则判断,如果SentinelWebInterceptor拦截器出现了BlockException异常,则会走DefaultBlockExceptionHandler(默认)来处理异常。开发者可以实现自己的 BlockExceptionHandler 来自定义异常响应格式。

例如我现在有一个普通的Web接口:

@GetMapping("/getNacosOrderConfig")
public String getNacosOrderConfig(@RequestParam(value = "yamlName", required = false) String yamlName) {
    // 返回Nacos配置的值
    return "Nacos配置-" + yamlName + "- 命名空间: " + orderProperties.getNameSpace() +
            ", 分组: " + orderProperties.getGroup() +
            ", URL: " + orderProperties.getUrl();
}

设置好对应的流控规则以后,我们访问这个接口使其触发流控限制。

触发流控

如图所示,这里打印出来的内容其实就是默认BlockExceptionHandler中来的:

DefaultBlockExceptionHandler源码


这个处理器整套的加载流程如下:

  1. 系统启动时加载配置了SentinelWebAutoConfiguration配置类,将SentinelWebInterceptor拦截器注册为Bean

    SentinelWebInterceptor注册为bean

  2. SentinelWebInterceptor拦截器又继承了AbstractSentinelInterceptor拦截器,请求发送前的处理请求都在AbstractSentinelInterceptor抽象类中

    SentinelWebInterceptor继承AbstractSentinelInterceptor

  3. AbstractSentinelInterceptor拦截器中的preHandle方法中,进行规则校验,判断是否需要进行限流、熔断等操作。

    请求前的处理逻辑

  4. 在进行BlockException处理的时候,会进行判断到底该用那个处理器来处理。

    选择处理器进行处理

  5. 这里处理器选择的时候,是通过baseWebMvcConfig中拿的,这个baseWebMvcConfig是注入进来的一个Bean(BaseWebMvcConfig)。这个Bean其实是在最开始SentinelWebAutoConfiguration配置类加载的时候创建的。

    注入baseWebMvcConfig

    BaseWebMvcConfig类与SentinelWebMvcConfig类的关系

    DefaultBlockExceptionHandler设置的时机

所以这就是为什么当触发各个规则的时候,普通的web接口看到内容是Blocked by Sentinel (flow limiting)的原因了。

4.5.1.2 @SentinelResource 注解异常处理流程

在使用@SentinelResource注解的时候,会通过AOP切面拦截所有标注了@SentinelResource的方法调用。

如果出现了BlockException异常,就会被捕获到,然后执行handleBlockException方法。这个方法中,如果你的@SentinelResource注解指定了blockHandler或者blockHandlerClass中的handler,反正只要是blockHandler不为空,就会调用对应的处理逻辑,否则就走fallback的处理逻辑。

如果你的@SentinelResource注解中的fallback相关设置也不存在,那么就会继续抛出异常。

如果没人处理继续抛出的异常,那么就是500错误了。


源码分析如下:

给接口加上@SentinelResource注解

当使用了@SentinelResource注解时候,代码执行之前会被AOP切面类SentinelResourceAspect拦截:

切面方法解读

handleBlockException方法解读

handleFallback方法解读

handleDefaultFallback方法解读

切面方法解读

4.5.1.3 OpenFeign远程调用异常处理

当使用OpenFeign远程调用时,SentinelFeign.builder()会拦截所有Feign客户端调用。

SentinelFeign.builder()是Sentinel与OpenFeign集成的核心组件。

它会自动为每个Feign接口方法创建Sentinel资源保护点,当BlockException被抛出时,自动捕获异常并进入降级处理流程,无需开发者手动编写try-catch代码。

当你在使用@FeignClient注解时,你可以指定fallback相关配置,它是专门为远程调用提供降级处理,当远程服务不可用或被限流时触发

当需要降级处理时,首先尝试执行fallbackFactory配置的降级逻辑,如果fallbackFactory不存在或执行失败,则尝试执行fallback类的方法,如果fallback也执行失败,则进入下一层兜底处理。

现在我们来整合Feign与Sentinel,首先在配置文件新增配置:

feign:
  sentinel:
    enabled: true #为feign整合sentinel

这里是谁要使用远程调用,就在谁的配置文件中添加

添加之后,只需要发送有关feign的请求,就可以在Sentinel控制台上的簇点链路的地方看到你的请求路径。

例如:POST:http://service-product/product/getProductListByIds

远程调用地址查看

那么在Feign中,降级发生时,如何自定义自己的逻辑呢?


对于fallback来说,找到我们之前的FeignClient,为@FeignClient注解添加上一个属性:fallback,这个属性的值要填写一个类。例如:

@FeignClient(
        name = "service-product", // 商品服务的名称,也是注册中心的服务标识
        path = "/product", // 全局路径前缀,相当于Controller中@RequestMapping中的值
        contextId = "productFeignClient", // 用于区分同一服务中的不同Feign客户端,便于在多个Feign客户端中进行区分
        fallback = ProductFeignFallback.class //指定fallback属性
)
public interface ProductFeignClient {
    // 在这里定义与商品服务相关的Feign客户端方法,需要与对应的Controller方法保持大体一致

    /**
     * 根据商品ID列表查询商品信息
     *
     * @param requestDto 商品列表查询请求参数
     * @return 商品信息列表
     */
    @PostMapping("/getProductListByIds")
    List<Product> getProductListByIds(@RequestBody ProductListQueryDto requestDto, Request.Options requestOptions);


    /**
     * 根据条件查询商品列表信息
     *
     * @param dto 商品列表查询条件
     * @return 商品信息列表
     */
    @GetMapping("/getProductListByCondition")
    List<Product> getProductListByCondition(@SpringQueryMap ProductListByConditionQueryDto dto);
}

具体的ProductFeignFallback这个类:

//该类必须交给spring管理,且要实现对应的Feign接口
@Component
@Slf4j
public class ProductFeignFallback implements ProductFeignClient {

    /**
     * 根据商品ID列表查询商品信息
     * @param requestDto 商品列表查询请求参数
     * @param requestOptions 请求选项
     * @return 商品信息列表
     */
    @Override
    public List<Product> getProductListByIds(ProductListQueryDto requestDto, Request.Options requestOptions) {
        log.info("根据商品ID列表查询商品信息失败...");
        return Collections.emptyList();
    }

    @Override
    public List<Product> getProductListByCondition(ProductListByConditionQueryDto dto) {
        return null;
    }
}

[!caution]

注意:对于fallback来说,自定义处理的这个类必须要实现你自己某个FeignClient接口,同时还要被Spring所管理,否则调用出错。

所以,当调用getProductListByIds出现异常或者被限流而降级的时候,就会走指定的ProductFeignFallback类中的getProductListByIds方法的逻辑。例如我现在设置流控规则如下:

远程调用接口流控规则

请求结果

请求结果

可见fallback回调成功。


对于fallbackFactory来说,找到我们之前的FeignClient,为@FeignClient注解添加上一个属性:fallbackFactory,这个属性的值要填写一个类。例如:

@FeignClient(
        name = "service-product", // 商品服务的名称,也是注册中心的服务标识
        path = "/product", // 全局路径前缀,相当于Controller中@RequestMapping中的值
        contextId = "productFeignClient", // 用于区分同一服务中的不同Feign客户端,便于在多个Feign客户端中进行区分
        fallbackFactory = ProductFeignFallbackFactory.class //指定fallbackFactory工厂类
)
public interface ProductFeignClient {
    //接口方法和之前一样,此处省略...
}

编写ProductFeignFallbackFactory工厂类:

//该类必须交给spring管理,且要实现FallbackFactory泛型接口,泛型为你的Feign接口
@Component
@Slf4j
public class ProductFeignFallbackFactory implements FallbackFactory<ProductFeignClient> {
    @Override
    public ProductFeignClient create(Throwable cause) {
        return new ProductFeignClient() {
            @Override
            public List<Product> getProductListByIds(ProductListQueryDto requestDto, Request.Options requestOptions) {
                log.info("根据商品ID列表查询商品信息失败(ProductFeignFallbackFactory)...");
                return Collections.emptyList();
            }

            @Override
            public List<Product> getProductListByCondition(ProductListByConditionQueryDto dto) {
                return null;
            }
        };
    }
}

该类必须交给spring管理,且要实现FallbackFactory泛型接口,泛型为你的Feign接口。

流控规则仍然沿用之前的,现在请求看一下什么效果:

执行结果

[!tip]

如果你在@FeignClient注解中,同时指定了fallbackfallbackFactory两个属性,在我的Spring Cloud版本中,是fallback的配置生效了,如果你的版本与我的不同,可能会导致fallbackFactory的配置生效,不同版本的优先级可能会有不一样。所以,为了避免混淆,一般就只指定一个:

  • 如果需要获取异常信息,只配置 fallbackFactory
  • 如果只需要简单降级,只配置 fallback

fallbackFactory和fallback的区别:

在使用 Spring Cloud 的 Feign 客户端时,您可以定义降级处理逻辑,以应对远程服务不可用或出现错误的情况。两种常见的降级处理方式是使用 fallbackfallbackFactory

  1. fallback

    fallback 是一种基本的降级处理方式,您可以在 Feign 客户端接口中使用 fallback 属性来指定一个降级处理类(通常是一个实现了接口的类)。

    降级处理类中实现了接口中的方法,用于定义当远程服务调用失败时应采取的降级策略。这个类应该能够处理一种特定类型的降级,通常是针对单个 Feign 客户端接口的。

    使用 fallback 时,您可以为每个 Feign 客户端接口定义不同的降级处理逻辑。

    @FeignClient(name = "example-service", fallback = MyFallback.class)
    public interface MyFeignClient {
        // ...
    }
    
  2. fallbackFactory

    fallbackFactory 提供了更灵活的降级处理方式,它允许您动态生成降级处理类的实例,根据不同的异常情况采取不同的降级策略。

    您可以通过实现 FallbackFactory 接口来创建一个工厂类,该工厂类会生成降级处理类的实例。

    fallback 不同,fallbackFactory 可以更精细地处理不同的异常情况,因为它可以访问异常信息,从而更好地控制降级逻辑。

    @FeignClient(name = "example-service", fallbackFactory = MyFallbackFactory.class)
    public interface MyFeignClient {
        // ...
    }
    
    public class MyFallbackFactory implements FallbackFactory<MyFeignClient> {
        @Override
        public MyFeignClient create(Throwable throwable) {
            if (throwable instanceof FeignException) {
                FeignException feignException = (FeignException) throwable;
                if (feignException.status() == 404) {
                    return new MyFallbackFor404();
                }
            }
            return new MyFallbackForOtherErrors();
        }
    }
    

总结:

  • 使用 fallback 时,您为每个 Feign 客户端接口指定一个具体的降级处理类,适合简单的降级逻辑。
  • 使用 fallbackFactory 时,您可以更灵活地生成降级处理类的实例,可以根据不同的异常情况采取不同的降级策略,适合复杂的降级逻辑。

4.5.1.4 SphU硬编码异常处理流程

使用SphU.entry()进行硬编码方式的资源保护时,需要通过try-catch手动捕获并处理BlockException,之后在catch块中可以实现自定义的异常处理逻辑,最后必须在finally块中手动释放Entry资源

@GetMapping("sphuTest")
public R<String> sphuTest(){
    Entry entry = null;
    try {
        entry = SphU.entry("sphuTest");
        return R.success("Sentinel限流测试通过");
    } catch (BlockException e) {
        log.error("Sentinel限流异常: {}", e.getMessage(),e);
      	// 
        return R.error("请求被限流,请稍后再试");
    }finally {
        if (entry != null){
            entry.exit(); // 释放资源
        }
    }
}

[!caution]

SphU.entry(xxx) 需要与 entry.exit() 方法成对出现,匹配调用,否则会导致调用链记录异常,抛出 ErrorEntryFreeException 异常。

现在我们给资源sphuTest加上流控规则如下:

新建流控规则

请求结果

可见通过使用SphU硬编码的方式也能够实现资源的流控,异常的处理主要就是在catch块中进行处理。

4.5.2 Sentinel自定义BlockExceptionHandler

以前我们可以使用@SentinelResource中的blockHandlerfallback,来处理,但是对于每一个资源都加上@SentinelResource显然是十分冗余和不优雅的,所以,我们要使用一种更加灵活的方式,通过实现BlockExceptionHandler接口来统一处理各个异常情况,同时也能够区分出到底是触发的是那种异常:

/**
 * @author nianxinzhuo
 * @date 2025/6/14 11:43
 * @description 自定义BlockExceptionHandler
 */
//此类需要交给Spring容器管理
@Component
public class CustomBlockExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        String msg = null;
        if (e instanceof FlowException) {
            msg = "请求被限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级";
        } else if (e instanceof SystemBlockException) {
            msg = "系统规则限流";
        } else if (e instanceof AuthorityException) {
            msg = "授权规则不通过";
        }

        httpServletResponse.setStatus(429);
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(R.error(msg)));

    }
}

这种自定义的BlockExceptionHandler只能用在普通的Web接口,因为BlockExceptionHandler接口是全局的Web层异常处理器。所以不用使用@SentinelResource注解了,直接在对应的接口处上设置流控规则,如果触发了流控规则,那么就会执行自定义BlockExceptionHandler类中的handle方法。

[!important]

这里只能是是URL资源的异常处理,基于注解@SentinelResource定义的资源是不会走自定义的BlockExceptionHandler的。同时,基于URL资源的异常,基本上就只有流控与熔断异常。

例如我现在有代码如下:

@GetMapping("blockExceptionHandlerTest")
public R<String> blockExceptionHandlerTest(@RequestParam(value = "name", required = false) String name) {
    return R.success("Sentinel限流测试通过,参数name: " + name);
}

[!caution]

注意,我这里是没有用@SentinelResource注解来定义资源的,而是直接把他作为一个URL资源。

现在设置流控规则如下

流控规则

响应结果

同时我们再来看看熔断规则:

熔断规则

请求结果

接下来你可以在试试热点参数规则,你会发现热点参数是绝不会生效的,因为热点参数异常是必须和注解@SentinelResource一起使用的,同时系统规则也不会生效,因为系统规则是自适应的。

4.5.3 Sentinel全局异常处理

这个全局异常处理和以前Spring Boot的全局异常处理一样,只是把各个异常更加细化了,例如:

/**
 * @author nianxinzhuo
 * @date 2025/6/14 15:59
 * @description 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(FlowException.class)
    public R<?> handleFlowException(FlowException ex) {
        return R.error("触发流控异常");
    }

    @ExceptionHandler(DegradeException.class)
    public R<?> handleDegradeException(DegradeException ex) {
        return R.error("触发降级异常");
    }

    @ExceptionHandler(ParamFlowException.class)
    public R<?> handleParamFlowException(ParamFlowException ex) {
        return R.error("触发热点参数限流异常");
    }
    @ExceptionHandler(SystemBlockException.class)
    public R<?> handleSystemBlockException(SystemBlockException ex) {
        return R.error("触发系统规则限流异常");
    }

    @ExceptionHandler(AuthorityException.class)
    public R<?> handleAuthorityException(AuthorityException ex) {
        return R.error("触发授权规则不通过异常");
    }

  	// 其他异常处理...
}

这里就不过多演示了。

4.6 Sentinel规则的持久化

参考文档:

在进行了这么多次Sentinel操作以后,相比你已经发现,每次配置好Sentinel的相关规则之后,如果应用重启,那么对应的规则也会消失。因为规则默认存储在客户端的内存当中,所以,为了保证客户端重启配置不丢失,我们需要将Sentinel的配置持久化。

从官方文档Sentinel动态规则扩展可知:

Sentinel推荐方式

可见,Sentinel最推荐的方式为在Sentinel控制台配置好规则之后,由Sentinel将规则推送到统一的配置中心进行存储(Nacos、Zookeeper等),之后,客户端再从配置中心读取规则配置

但是翻遍文档,也没看见看见Sentinel如何将规则推送到配置中心的,所以,这里我们先看后面一段,客户端从配置中心读取Sentinel配置

4.6.1 各种规则文件讲解

4.6.1.1 流控规则

[
  {
    // 资源名
    "resource": "/test",
    // 针对来源,若为 default 则不区分调用来源
    "limitApp": "default",
    // 限流阈值类型(1:QPS;0:并发线程数)
    "grade": 1,
    // 阈值
    "count": 1,
    // 是否是集群模式
    "clusterMode": false,
    // 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待)
    "controlBehavior": 0,
    // 流控模式(0:直接;1:关联;2:链路)
    "strategy": 0,
    // 预热时间(秒,预热模式需要此参数)
    "warmUpPeriodSec": 10,
    // 超时时间(排队等待模式需要此参数)
    "maxQueueingTimeMs": 500,
    // 关联资源、入口资源(关联、链路模式)
    "refResource": "rrr"
  }
]

4.6.1.2 熔断规则

[
  {
      // 资源名
    "resource": "/test1",
    "limitApp": "default",
    // 熔断策略(0:慢调用比例,1:异常比率,2:异常计数)
    "grade": 0,
    // 最大RT、比例阈值、异常数
    "count": 200,
    // 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)
    "slowRatioThreshold": 0.2,
    // 最小请求数
    "minRequestAmount": 5,
    // 当单位统计时长(类中默认1000)
    "statIntervalMs": 1000,
    // 熔断时长
    "timeWindow": 10
  }
]

4.6.1.3 热点规则

[
  {
    // 资源名
    "resource": "/test1",
    // 限流模式(QPS 模式,不可更改)
    "grade": 1,
    // 参数索引
    "paramIdx": 0,
    // 单机阈值
    "count": 13,
    // 统计窗口时长
    "durationInSec": 6,
    // 是否集群 默认false
    "clusterMode": false,
    //
    "burstCount": 0,
    // 集群模式配置
    "clusterConfig": {
      //
      "fallbackToLocalWhenFail": true,
      //
      "flowId": 2,
      //
      "sampleCount": 10,
      //
      "thresholdType": 0,
      //
      "windowIntervalMs": 1000
    },
    // 流控效果(支持快速失败和匀速排队模式)
    "controlBehavior": 0,
    //
    "limitApp": "default",
    //
    "maxQueueingTimeMs": 0,
    // 高级选项
    "paramFlowItemList": [
      {
        // 参数类型
        "classType": "int",
        // 限流阈值
        "count": 222,
        // 参数值
        "object": "2"
      }
    ]
  }
]

4.6.1.4 系统规则

[
    {
      // RT
      "avgRt": 1,
      // CPU 使用率
      "highestCpuUsage": -1,
      // LOAD
      "highestSystemLoad": -1,
      // 线程数
      "maxThread": -1,
      // 入口 QPS
      "qps": -1
    }
  ]

4.6.1.5 授权规则

[
  {
    // 资源名
    "resource": "sentinel_spring_web_context",
    // 流控应用
    "limitApp": "/test",
    // 授权类型(0代表白名单;1代表黑名单。)
    "strategy": 0
  }
]

4.6.2 客户端读取配置中心Sentinel规则配置

这里的配置中心我选用的是Nacos。

事先创建好规则,比如我这里创建了流控规则如下:

流控规则配置

[
   {
      // 资源名
      "resource": "blockExceptionHandlerTest",

      // 针对来源,若为 default 则不区分调用来源
      "limitApp": "default",

      // 限流阈值类型(1:QPS;0:并发线程数)
      "grade": 1,

      // 阈值
      "count": 2,

      // 是否是集群模式
      "clusterMode": false,

      // 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待)
      "controlBehavior": 0,

      // 流控模式(0:直接;1:关联;2:链路)
      "strategy": 0
   }
]

流控规则配置好了之后,参考Sentinel的官方文档,从配置中心读取出规则文件:

读取配置中心的Sentinel规则文件实现

读取配置中心的Sentinel规则文件实现

可见要想实现从客户端读取配置中心上Sentinel的规则文件,步骤如下:

  1. 导入依赖

    <!-- Sentinel 数据源 -->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    
  2. 编写数据源初始化SPI

    package cn.yunrain.nxz.config;
    
    import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
    import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
    import com.alibaba.csp.sentinel.init.InitFunc;
    import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
    import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
    import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
    import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.TypeReference;
    import com.alibaba.nacos.api.PropertyKeyConst;
    
    import java.util.List;
    import java.util.Properties;
    
    public class DataSourceInitFunc implements InitFunc {
    
        @Override
        public void init() throws Exception {
            final String remoteAddress = "127.0.0.1:8848";
            final String username = "nacos";
            final String password = "nacos";
            final String groupId = "SENTINEL_GROUP";
            final String flowRuleDataId = "service-order-read-only-flow-rules";
            final String degradeRuleDataId = "service-order-degrade-rules";
    
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, remoteAddress);
            properties.put(PropertyKeyConst.USERNAME,username);
            properties.put(PropertyKeyConst.PASSWORD,password);
    
            // 流控规则
            ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(properties, groupId, flowRuleDataId,
                source -> JSON.parseObject(source, new TypeReference<>() {}));
            FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    
            //熔断规则
            ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new NacosDataSource<>(properties, groupId, degradeRuleDataId,
                    source -> JSON.parseObject(source, new TypeReference<>() {}));
            DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
        }
    }
    
  3. 使用编写的SPI

    将对应的类名添加到位于资源目录(通常是 resource 目录)下的 META-INF/services 目录下的 com.alibaba.csp.sentinel.init.InitFunc 文件中:

    SPI配置

之后就可以启动程序访问资源,来看看是是否配置成功,如果成功触发流控规则,那么就说明客户端读取配置中心Sentinel规则配置文件成功。

4.6.3 Sentinel与配置中心双向通信

在上一小节中,我们成功从配置中心读取了Sentinel的规则文件,但是Sentinel如如何将配置文件持久化到配置中心呢?

不可能我们每次都是手动的编写规则JSON文件配置到配置中心吧,这样是十分麻烦的。

如果能够通过Sentinel的控制台配置好规则,然后再将规则自动推送到配置中心中,同时,客户端也能够读取到配置中心的Sentinel规则配置,那么就很好了。

默认情况下Sentinel只能接收到Nacos推送的消息,但不能将自己控制台修改的信息同步给Nacos:

Nacos与Sentinel单向通信

但是在生成环境下,我们为了更方便的操作,是需要将 Sentinel 控制台修改的规则也同步到 Nacos 的,所以在这种情况下我们就需要修改Sentinel控制台的源码,让其可以实现和 Nacos 的双向通讯:

Nacos与Sentinel双向通信

改造之后,就能如Sentinel官方文档所示一样:

Sentinel持久化完整方案


现在开始改造Sentinel控制台源码。

先去根据自己的版本下载源码文件:

源码文件下载

下载之后找到控制台源码改造:

控制台源码

修改pom.xml文件。由于我的配置中心是用的Nacos,而官方提供的 Nacos 持久化实例,是在 test 目录下进行单元测试的,而我们是用于生产环境,所以需要将 scope 中的 test 去掉

修改pom文件

Sentinel官方写的Nacos持久化配置是在test/com.alibaba.csp.sentinel.dashboard.rule.nacos包下的,所以我们需要将这部分内容复制到src/main/java/com.alibaba.csp.sentinel.dashboard.rule目录下,如图所示:

复制Nacos的限流规则持久化配置

由于是与Nacos整合,所以需要添加Nacos的基本配置,修改配置文件:

sentinel.nacos.serverAddr=127.0.0.1:8848
sentinel.nacos.username=nacos
sentinel.nacos.password=nacos

nacos相关配置

之后在com.alibaba.csp.sentinel.dashboard.rule.nacos下创建 Nacos 配置文件的读取类,实现代码如下:

package com.alibaba.csp.sentinel.dashboard.rule.nacos;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author nianxinzhuo
 * @date 2025/6/14 17:06
 * @description
 */
@Configuration
@ConfigurationProperties(prefix = "sentinel.nacos")
public class NacosPropertiesConfiguration {
    private String serverAddr;
    private String dataId;
    private String groupId;
    private String namespace;
    private String username;
    private String password;
		//getter and setter...
}

NacosPropertiesConfiguration配置类位置

修改NacosConfig中的nacosConfigService方法,将Nacos相关配置加载到Spring容器中:

修改NacosConfig中的nacosConfigService方法


前期准备配置就搞好了,现在我针对流控规则和熔断(降级)规则,来配置持久化。

为什么只示范两个规则呢,因为对于流控规则来说,配置持久化稍微麻烦点,与其他规则持久化配置有点不同。所以流控规则需要单独讲解。

对于熔断规则的持久化配置,则是和热点、授权、等规则配置方式有异曲同工之妙,很多思想是可以复用的,你理解了一种,后面几种规则都是相同的方法配置,所以这里熔断也单独讲一下。

4.6.3.1 流控规则持久化

找到com.alibaba.csp.sentinel.dashboard.controller.v2目录下的FlowControllerV2文件,这个V2其实就是官方写的一个流控持久化Demo。修改FlowControllerV2文件,修改后的代码:

@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

修改FlowControllerV2文件

之后修改webapp/resources/app/scripts/directives/sidebar/sidebar.html 文件,搜索dashboard.flowV1改为dashboard.flow,如下图所示:

修改sidebar.html文件

之后再修改identity.js文件,它位于webapp/resources/app/scripts/controllers/identity.js目录,将FlowServiceV1修改为FlowServiceV2,如下图所示:

identity.js文件的第一处修改

identity.js文件的第二处修改


对于Sentinel流控规则的持久化就改造好了,现在我们回到客户端,进行客户端的相关配置:

  1. 引入Sentinel数据源对应依赖:

    <!-- Sentinel 数据源 -->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    
  2. 修改客户端配置文件

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: 127.0.0.1:8082
          datasource:
            #流控规则与nacos持久化配置
            flow:
              nacos:
                username: ${spring.cloud.nacos.config.username}
                password: ${spring.cloud.nacos.config.password}
                server-addr: ${spring.cloud.nacos.server-addr}
                dataId: ${spring.application.name}-flow-rules
                groupId: SENTINEL_GROUP
                data-type: json
                rule-type: flow
    

    流控规则配置

    其中这里面的dataId以及groupId是不能随意乱写的,需要与Sentinel控制台中的源码匹配上

    FlowRuleNacosPublisher中dataId以及groupId名称

    可见推送到Nacos配置中心的时候,设置的dataId的名称是app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX;groupId是NacosConfigUtil.GROUP_ID

    这里的app其实就是客户端微服务的名称,而NacosConfigUtil的源码如下:

    NacosConfigUtil源码

    这下你应该明白了为啥我的客户端中流控规则的持久化配置文件的要如此写了。

现在先启动Sentinel控制台的代码,再启动客户端。之后给资源设置流控规则,设置之后观察Nacos:

Nacos中的流控规则配置

流控规则详情

现在你通过控制台更改流控规则:

通过Sentinel控制台修改流控规则

观察Nacos上的流控规则详情:

Nacos中流控规则详情

可见改文件的MD5变化了,同时流控规则中的count也变化了,可见更新成功。

[!tip]

当然,如果你在Nacos中更新或者新增流控规则,在Sentinel也会同步更新的。

4.6.3.2 熔断规则持久化

[!tip]

上面讲了流控规则持久化,可见是很麻烦的,还更改了前端代码,但是后面的一些规则,比如熔断、热点、授权等规则都不必改前端代码,相对来说比较简单,而且配置方式都是一致的。

  1. 提供XxxRuleNacosProviderXxxRuleNacosPublisher

    参考之前的官方写的FlowRuleNacosProviderFlowRuleNacosPublisher写出DegradeRuleNacosProviderDegradeRuleNacosPublisher

    参考FlowRule相关代码创建出Degrade相关代码

    在NacosConfigUtil中新建熔断降级的dataId常量

  2. 修改NacosConfig文件,参考之前flowRuleEntityEncoderflowRuleEntityDecoder的Bean,提供xxxRuleEntityEncoderxxxRuleEntityDecoder

    提供degradeRuleEntityEncoder与degradeRuleEntityDecoder

  3. 创建新的熔断降级Controller,注释掉原来的接口,并且改造新创建出来的熔断降级Controller,我这里和官方取名保持一致,取名DegradeControllerV2

    创建DegradeControllerV2

  4. 改造新创建的Controller(DegradeControllerV2)。其中的改造细节参考FlowControllerV2的改造时的方法。

    DegradeControllerV2接口第一个改造点

    DegradeControllerV2接口第二个改造点

    DegradeControllerV2接口第三个改造点

    DegradeControllerV2接口第四个改造点

  5. 客户端配置文件中,新增熔断降级到配置

    spring:
      cloud:
        sentinel:
          transport:
            dashboard: 127.0.0.1:8082
          datasource:
            #熔断规则与nacos持久化配置
            degrade:
              nacos:
                username: ${spring.cloud.nacos.config.username}
                password: ${spring.cloud.nacos.config.password}
                server-addr: ${spring.cloud.nacos.server-addr}
                dataId: ${spring.application.name}-degrade-rules
                groupId: SENTINEL_GROUP
                data-type: json
                rule-type: degrade
    

    客户端降级规则配置

这就改造好了,先启动改造的Sentinel控制台,之后再启动客户端,最后在控制台中创建熔断降级规则,观察Nacos控制台是否有对应数据:

Sentinel控制台创建熔断规则

熔断规则已持久化到Nacos中

熔断规则详情

现在数据已经推送到了Nacos中,当你重启客户端之后,你会发现之前配置的规则依然存储。

5. Gateway

参考文档:

在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。

这种客户端直接请求服务的方式存在以下问题:

  • 当服务数量众多时,客户端需要维护大量的服务地址,这对于客户端来说,是非常繁琐复杂的。
  • 在某些场景下可能会存在跨域请求的问题。
  • 身份认证的难度大,每个微服务需要独立认证。

我们可以通过 API 网关来解决这些问题,API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。

API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。

对于服务数量众多、复杂度较高、规模比较大的系统来说,使用 API 网关具有以下好处:

  • 客户端通过 API 网关与微服务交互时,客户端只需要知道 API 网关地址即可,而不需要维护大量的服务地址,简化了客户端的开发。
  • 客户端直接与 API 网关通信,能够减少客户端与各个服务的交互次数。
  • 客户端与后端的服务耦合度降低。
  • 节省流量,提高性能,提升用户体验。
  • API 网关还提供了安全、流控、过滤、缓存、计费以及监控等 API 管理功能。

这里我们主要讲解Spring Cloud Gateway这种API网关。

5.1 Gateway的术语与工作流程

在Gateway中,有3个核心的概念:

  1. Route(路由): 网关的基本构件。它由一个ID、一个目的地URI、一个谓词(Predicate)集合和一个过滤器(Filter)集合定义。如果集合谓词为真,则路由被匹配。
  2. Predicate(谓词):: 这是一个 Java 8 Function Predicate。输入类型是 Spring Framework ServerWebExchange。这让你可以在HTTP请求中的任何内容上进行匹配,比如header或查询参数。
  3. Filter过滤器):: 这些是 GatewayFilter 的实例,已经用特定工厂构建。在这里,你可以在发送下游请求之前或之后修改请求和响应。

了解了核心概念之后,再来看看Gateway的工作流程图:

Gateway工作流程图

客户端向 Spring Cloud Gateway 发出请求。如果Gateway处理程序映射确定一个请求与路由相匹配,它将被发送到Gateway Web处理程序。这个处理程序通过一个特定于该请求的过滤器链来运行该请求。过滤器被虚线分割的原因是,过滤器可以在代理请求发送之前和之后运行逻辑。所有的 "pre" (前)过滤器逻辑都被执行。然后发出代理请求。在代理请求发出后,"post" (后)过滤器逻辑被运行。

5.2 SpringCloud集成Gateway

一般在实践中,我们把Gateway作为一个独立的模块,所以,我们需要创建一个Gateway网关模块,并且引入对应的依赖:

<!--网关依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!--nacos服务发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!--负载均衡依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

一般我们的Gateway网关服务都是需要和注册中心通信的,而且还会使用到负载均衡,所以这里nacos的服务发现以及负载均衡的依赖我也一并加上。

之后,我们编写好启动类和基本的配置文件:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos

启动程序,查看nacos控制台:

配置成功

5.3 动态路由(Route)

有了Gateway网关之后,我们一般不会直接去访问对应的各个微服务,而是统一的访问网关,让网关去找到对应的微服务。

这样做的好处:

  • 流量有了统一的入口,便于控制。
  • 不会将内部服务给暴露出去,保证了系统的安全。
  • 可以不用去记忆众多的微服务地址,只需要记住网关的服务地址即可。

那么网关接收到请求,是如何知道该将请求转发到那个微服务上门去呢?

这里就要和动态路由想关联起来。

我们来看看路由(Route)的配置:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:

这里有一个routes的配置项,我们来点击进去看看:

routes配置

这里的routes的类型是一个RouteDefinition的集合,我们再来看看RouteDefinition这个类提供的信息:

RouteDefinition类属性

RouteDefinition类中,提供了上图的一些属性,我们来一一讲解一下。

5.3.1 id

这个是路由的唯一标识,不做过多讲解,要求唯一就行。

5.3.2 URI

这个配置路由的目标地址,表示请求将要被转发到哪里。这里也不做过多讲解。

5.3.3 路由谓词(Predicate)

RouteDefinition这个类中,提供了类型为PredicateDefinition的集合属性。这个属性其实就是Gateway中的路由谓词(Predicate)。

路由谓词(Route Predicate) 是定义网关路由规则的核心组件之一。它决定了 哪些传入请求(HTTP 请求)应该匹配到哪个特定的路由(Route),进而决定请求被转发到哪个下游服务(目标 URI) 以及应用哪些过滤器(Filter)。

PredicateDefinition类中,有如下属性:

@Validated
public class PredicateDefinition {

	@NotNull
	private String name;

	private Map<String, String> args = new LinkedHashMap<>();
  
  // 省略其他无关信息...
}

PredicateDefinition类属性

Spring Cloud Gateway 提供了丰富的内置谓词工厂(PredicateFactory)。配置时通常使用工厂名称及其参数。

例如Gateway提供了一个Path工厂如下:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/order/**

[!tip]

Path谓语工厂的作用是根据请求的 路径(URI) 进行匹配。

网关按顺序遍历路由列表,当请求满足某路由的所有谓词条件时,匹配成功,那么就会将请求转发到对应route中配置的URI上。

上述URI中,存在lb,这个lb表示负载均衡,前提是你必须要在依赖中引入负载均衡的相关依赖才可使用

上诉配置就表示,当你请求网关地址的URI是/order/**的时候,请求就会转发到service-order这个微服务上的对应接口地址。

现在我先启动网关服务,之后再启动订单服务,我现在发送请求以及响应结果如下:

请求响应结果

可见请求的是网关的IP+端口(80),但是实际拿到的数据确实是订单服务的数据。

现在我们将predicates配置对应到代码中:对应到类PredicateDefinition的属性中的时候,谓语工厂名称就是name的值,值就存放在args这个LinkedHashMap中的。

PredicateDefinition类属性赋值流程

Gateway提供了很多内置的谓词工厂:

谓词工厂名称 作用描述 配置示例 使用场景
After 匹配在指定时间之后发生的请求 - After=2025-12-31T23:59:59+08:00[Asia/Shanghai] 定时开启新功能、节假日促销
Before 匹配在指定时间之前发生的请求 - Before=2025-01-01T00:00:00+08:00[Asia/Shanghai] 限时活动、旧版本服务下线
Between 匹配在两个时间点之间发生的请求 - Between=2025-06-01T00:00:00+08:00, 2025-06-30T23:59:59+08:00 活动期间特殊路由
Cookie 匹配包含特定Cookie且值符合正则表达式的请求 - Cookie=sessionId, [a-zA-Z0-9]{32} 会话验证、灰度发布
Header 匹配包含特定请求头且值符合正则表达式的请求 - Header=X-Request-Id, \d+ API版本控制、请求追踪
Host 匹配Host头符合指定模式的请求 - Host=**.example.com, api.example.org 多域名/子域名路由、SaaS多租户
Method 匹配HTTP方法符合指定的请求 - Method=GET,POST RESTful接口路由
Path 匹配请求路径符合Ant风格模式的请求 - Path=/order/** 基础路由(最常用)
Query 匹配包含特定查询参数(且值可选匹配正则)的请求 - Query=token - Query=id, \d{3} OAuth认证、临时访问令牌
RemoteAddr 匹配客户端IP地址在指定CIDR范围内的请求 - RemoteAddr=192.168.1.1/24 内部接口限制、IP白名单
Weight 权重比例将流量分配到不同路由组 - Weight=group-a, 80 金丝雀发布、AB测试
XForwardedRemoteAddr 基于X-Forwarded-For头部的真实客户端IP进行匹配 - XForwardedRemoteAddr=10.0.0.1/24 反向代理环境下的IP过滤

[!caution]

这里有几个注意点:

  1. 必须使用ISO-8601格式时间(如2025-06-28T14:30:00+08:00[Asia/Shanghai]),时区建议显式声明避免歧义。

  2. Weight需成组配置(相同分组名称),权重总和应为100。

    - id: canary_route
      predicates:
        - Path=/service/**
        - Weight=group-a, 10  # 10%流量
    - id: main_route
      predicates:
        - Path=/service/**
        - Weight=group-a, 90  # 90%流量
    
  3. 所有谓词默认是AND关系,此配置要求同时满足:

    predicates:
      - Path=/api/users/**
      - Method=GET
      - Header=X-Auth-Token, .+
      - After=2025-01-01T00:00:00Z
    

5.3.4 路由过滤器(GatewayFilter)

RouteDefinition这个类中,提供了类型为FilterDefinition的集合属性。这个属性其实就是Gateway中的路由过滤器(Filter)。

对于FilterDefinition类,内部构造和PredicateDefinition其实高度类似:

FilterDefinition类部分源码

路由过滤器也是 Spring Cloud Gateway 的核心组件之一,用于在请求被路由到下游服务修改请求和响应。

路由过滤器的范围是一个特定的路由。Spring Cloud Gateway 包括许多内置的 GatewayFilter 工厂。

下面我随便配置一个路由过滤器如下:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/order/**
          filters:
            - AddRequestHeader=X-Request-token, 123456789qwe #路由过滤器

我现在添加了一个路由过滤器,给请求头加上了参数X-Request-token,值为123456789qwe,现在我们在下游服务看看是否能够获取到X-Request-token这个参数的值:

发送请求以及响应结果

下游服务获取

获取结果

可见路由过滤器确实配置成功。

下面我们来看看Gateway提供的一些内置路由过滤器:

过滤器工厂名称 作用 配置示例 适用场景
AddRequestHeader 添加请求头 - AddRequestHeader=X-Request-Id,12345 添加追踪ID、身份令牌等
AddRequestHeadersIfNotPresent 仅当请求头不存在时添加 - AddRequestHeadersIfNotPresent=X-Auth:default 设置默认值避免覆盖
AddRequestParameter 添加请求查询参数 - AddRequestParameter=region,cn 强制添加区域参数
AddResponseHeader 添加响应头 - AddResponseHeader=X-Processed-By,Gateway 标识网关处理痕迹
CircuitBreaker 熔断保护(集成Resilience4j) yaml- name: CircuitBreaker args: name: backendA 服务降级、故障隔离
CacheRequestBody 缓存请求体供后续过滤器使用 - CacheRequestBody 需要多次读取请求体的场景(如验签+转发)
DedupeResponseHeader 去重响应头 - DedupeResponseHeader=User-Access RETAIN_FIRST 处理下游服务重复头问题
FallbackHeaders 熔断时添加异常信息到响应头 yaml- name: FallbackHeaders args: executionExceptionTypeHeaderName: Error-Type 熔断故障排查
JsonToGrpc 将JSON请求转换为gRPC协议 需配合protobuf定义 REST到gRPC协议转换
LocalResponseCache 本地响应缓存 yaml- name: LocalResponseCache args: size: 10MB timeToLive: 1h 缓存静态资源
MapRequestHeader 映射请求头(A→B) - MapRequestHeader=From, To 重命名请求头
ModifyRequestBody 修改请求体内容 需编码实现 请求体加密/解密、格式转换
ModifyResponseBody 修改响应体内容 需编码实现 响应体重写、错误消息统一格式化
PrefixPath 添加路径前缀 - PrefixPath=/api/v1 统一API版本路径
PreserveHostHeader 保留原始Host头 - PreserveHostHeader 下游服务需要原始Host信息
RedirectTo 重定向请求 - RedirectTo=302, https://new.example.com 临时跳转、HTTPS强制升级
RemoveJsonAttributesResponseBody 删除JSON响应中的属性 - RemoveJsonAttributesResponseBody=password,token 敏感数据脱敏
RemoveRequestHeader 移除请求头 - RemoveRequestHeader=Cookie 安全防护(移除敏感头)
RemoveRequestParameter 移除请求参数 - RemoveRequestParameter=debug 隐藏调试参数

5.4 全局过滤器(GlobalFilter)

路由过滤器的时候我们了解到了针对某个路由的过滤器规则,现在我们来看看针对全局的过滤器规则。

在 Gateway 中,有两类过滤器:

  • Route Filter 路由过滤器,对应 GatewayFilter 接口
  • Global Filter 全局过滤器,对应 GlobalFilter 接口

两者基本是等价的,差别在于 Route Filter 不是全局,而是可以配置到指定路由上。接口对比如下图:

GlobalFilter与GatewayFilter接口对比

绝大多数情况下,在路由过滤器能满足我们的拓展需求的情况下,优先使用它。并且如果想要作用到所有路由上,可以通过 spring.cloud.gateway.default-filters 配置项来设置。

5.4.1 全局过滤器与路由过滤器的执行顺序

无论是全局过滤器(GlobalFilter)还是路由过滤器(GatewayFilter),他们的执行顺序都跟order属性有关系

对于全局过滤器来说,内置的全局过滤器都是实现类Ordered接口的:

public interface Ordered {
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

  	// 值越小优先级越高
    int getOrder();
}

而对于路由过滤器,没有并没有实现Ordered接口和使用注解@Order注解来指定order值。而是默认从1开始,当你的路由配置配置了多个路由过滤器的时候,order的值依次递增:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
          filters:
            - AddRequestHeader=X-Request-token, 123456789qwe #order等于1
            - StripPrefix=1 # order等于2

对于过滤器的执行详细顺序,你可以通过类FilteringWebHandler查看:

加载过滤器

你可以在程序启动的时候,通过断点观察整个过滤器集合中order的值,值越小,优先级越高。

断点观察

5.4.2 内置全局过滤器

在Gateway中,官方提供了很多内置的全局过滤器。

当一个请求与路由匹配时,filter web handler将 GlobalFilter 的所有实例和 GatewayFilter 的所有路由特定实例添加到一个过滤链中。这个组合的过滤链由 org.springframework.core.Ordered 接口进行排序,你可以通过实现 getOrder() 方法来设置这个接口。

graph TD A[客户端请求] --> B{路由匹配} B -->|匹配成功| C[加载路由GatewayFilters] B -->|全局| D[加载所有GlobalFilters] C --> E[合并过滤器集合] D --> E E --> F[按order值排序] F --> G[创建过滤器链] G --> H[执行过滤器链]

由于 Spring Cloud Gateway 区分了过滤器逻辑执行的 “pre” 和 “post” 阶段,优先级最高的过滤器在 “pre” 阶段是第一个,在 “post” 阶段是最后一个

完整的执行时序如下:

sequenceDiagram participant Client participant GF1 as GlobalFilter1<br>(order=-100) participant GF2 as GlobalFilter2<br>(order=0) participant GF3 as GlobalFilter3<br>(order=50) participant RF1 as GatewayFilter1<br>(order=100) participant RF2 as GatewayFilter2<br>(order=200) participant Downstream Client->>GF1: 请求 GF1->>GF2: Pre处理 GF2->>GF3: Pre处理 GF3->>RF1: Pre处理 RF1->>RF2: Pre处理 RF2->>Downstream: 转发请求 Downstream-->>RF2: 响应 RF2-->>RF1: Post处理 RF1-->>GF3: Post处理 GF3-->>GF2: Post处理 GF2-->>GF1: Post处理 GF1-->>Client: 返回响应

现在我们来看看Gateway内置的过滤器:

内置过滤器

5.4.2.1 RouterToRequestUrlFilter

[!tip]

该过滤器的order值为10000。

RouterToRequestUrlFilter是一个前置全局过滤器,它的主要作用是将路由匹配成功后得到的目标URI转换为实际要转发的请求URL,并重新设置到请求上下文中。

简单来说就是合并路由URI与请求路径从而构造最终的下游请求URL。适用于所有路由转发前必经步骤

核心代码:

核心代码

核心功能

  1. URI重写:当网关匹配到某个路由规则(如通过Path断言)后,目标URI可能是一个静态地址(如lb://service-name)或动态生成的地址。该过滤器会根据路由配置(如uri属性)和原始请求的路径,合成最终的转发URL。
  2. 路径处理:如果路由配置了StripPrefix(路径剥离),它会先移除前缀。同时支持保留原始请求的查询参数(Query Parameters)和路径变量。
  3. 协议转换:支持将外部HTTP请求转发到内部服务(如http://lb://)。

执行时机

  • 阶段:在路由断言(Predicate)成功后、实际转发请求前执行。
  • 顺序:属于NettyRoutingFilter之前的预处理步骤(RouterToRequestUrlFilter的order值为10000)。

例如我现在有配置如下:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
          filters:
            - AddRequestHeader=X-Request-token, 123456789qwe
            - StripPrefix=1 # 移除路径前两级(如/api/order/getNacosOrderConfig → /order/getNacosOrderConfig)

现在我请求如下:http://127.0.0.1/api/order/getNacosOrderConfig

RouteToRequestUrlFilter过滤器重写URL

同时,这里的内置路由过滤器StripPrefixGatewayFilterFactory也生效了,对于上述配置:- StripPrefix=1

当我请求/api/order/getNacosOrderConfig的时候,由于我的parts的值为1,所以代表移除路径前面一级,最终请求的接口就变为了/order/getNacosOrderConfig

5.4.2.2 ReactiveLoadBalancerClientFilter

[!tip]

该过滤器的order值为10150。

ReactiveLoadBalancerClientFilter是一个前置全局过滤器。它的主要作用是将路由目标URI中的服务名(如 lb://service-name)解析为实际的服务实例地址,从而实现基于负载均衡的请求转发。

简单来说就是服务发现与负载均衡的过滤器,适用于所有微服务动态路由

核心代码:

核心代码

核心功能

  1. 服务实例发现与选择:当路由配置的 urilb:// 开头(例如 lb://user-service),该过滤器会通过 Spring Cloud LoadBalancer 从注册中心(如 Eureka、Nacos)获取服务实例列表。之后根据负载均衡策略(如轮询、随机等)选择一个可用的实例。
  2. URI 重写:将逻辑服务名(如 lb://user-service)替换为实际实例的物理地址(如 http://192.168.1.100:8080)。
  3. 故障处理:如果服务实例不可用,会触发重试逻辑(需配合 RetryFilter 使用)或返回错误响应。

执行时机

  • 阶段:在路由匹配之后、实际转发请求之前执行(顺序值为 10150,位于 RouterToRequestUrlFilter 之后)。
  • 触发条件:仅当 uri 协议为 lb:// 时生效。

例如我现在配置如下:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/order/**

现在我请求如下:http://127.0.0.1/order/getNacosOrderConfig

获取服务真正的地址

可见,我的请求先到达网关,然后路由规则中的断言匹配上之后,就给我负载均衡到了真正的接口地址:lb://service-order/order/getNacosOrderConfig;这里的lb://service-order被全局过滤器ReactiveLoadBalancerClientFilter解析为了服务真正的IP+端口。

5.4.2.3 ForwardRoutingFilter

[!tip]

该过滤器的order值为Integer.MAX_VALUE。

ForwardRoutingFilter是一个前置全局器,专门用于处理 本地转发(Forward) 请求的场景。它的核心作用是将匹配到的路由目标URI为 forward: 前缀的请求(如 forward:/local-endpoint)转发到网关应用的本地处理接口(例如Spring MVC或WebFlux控制器),而不是外部服务。

适用场景:

  • 聚合多个服务的响应:在网关内定义一个接口,通过 forward: 调用本地聚合逻辑,再返回给客户端。
  • 静态资源处理:直接转发到网关内嵌的静态资源处理器。
  • 开发调试:快速Mock接口,避免依赖真实服务。

核心功能

  1. 本地请求转发:当路由配置的 uriforward:// 开头(例如 forward:/api/local),该过滤器会将请求转发到网关应用内部的对应接口(类似于Servlet中的 RequestDispatcher.forward())。

    例如:将 /gateway-path 的请求转发给网关内部的 /local-handler 处理。

  2. 协议转换:将外部HTTP请求转换为网关内部的本地调用,保留原始请求的路径、参数和头信息。

  3. 性能优化:避免不必要的网络跳转,适合在网关层直接处理某些逻辑(如静态资源、聚合结果、Mock接口等)。

执行时机

  • 阶段:在路由匹配之后、实际处理请求之前执行(顺序值为 Integer.MAX_VALUE,即最后执行)。
  • 触发条件:仅当 uri 协议为 forward: 时生效。

例如我现在有如下配置:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-local
          uri: forward:/forward/test
          predicates:
            - Path=/local/**

我在Gateway模块新增接口如下:

package cn.yunrain.nxz.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author nianxinzhuo
 * @date 2025/6/29 10:32
 * @description 转发接口
 */
@RestController
@RequestMapping("forward")
public class ForwardController {

    @RequestMapping("test")
    public String test() {
        return "forward test";
    }

}

我现在的请求地址如下:http://127.0.0.1/local/test。如果该请求能够转到Gateway网关的/forward/test接口,那么就说明配置成功。

请求结果

可见确实是转发成功了。

5.4.2.4 NettyRoutingFilter

[!tip]

该过滤器的order值为Integer.MAX_VALUE。

NettyRoutingFilter是一个前置全局过滤器,专门用于通过 Netty 异步非阻塞客户端将请求转发到外部 HTTP 服务(如 REST API、微服务等)。它是网关实现 HTTP 代理功能的关键组件。

核心功能

  1. HTTP 请求转发:当路由目标 URI 是 http://https:// 时(例如 uri: http://backend-service),该过滤器会使用 Netty 的 HttpClient 发起异步请求,将客户端请求代理到目标服务。

    [!caution]

    如果目标 URI 是 lb://(负载均衡),需先由 ReactiveLoadBalancerClientFilter 解析为具体实例地址,再由 NettyRoutingFilter 转发。

  2. 异步非阻塞通信:基于 Reactor 和 Netty 的异步模型,避免阻塞网关线程,适合高并发场景。

  3. 请求/响应处理:自动处理原始请求的 HeadersBodyQuery Parameters,并将目标服务的响应返回给客户端。

  4. 超时与重试:支持通过配置设置连接超时、响应超时等(需配合 HttpClient 自定义配置)。

执行时机

  • 阶段:在路由匹配完成且目标 URI 确定为具体 HTTP 地址后执行(顺序值为 Integer.MAX_VALUE,即最后阶段)。
  • 触发条件:URI 协议为 http://https://(非 lb://forward://)。
sequenceDiagram NettyRoutingFilter->>NettyClient: 创建请求 NettyClient->>Downstream: 发送请求 Downstream-->>NettyClient: 接收响应 NettyClient-->>NettyRoutingFilter: 返回响应

例如我现在有如下配置:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-local
          uri: https://www.baidu.com
          predicates:
            - Path=/**

当我请求请求:http://127.0.0.1/xxx,实际会将请求转发到:https://www.baidu.com/**

例如我现在访问地址如下:http://127.0.0.1/,如果被转发到了https://www.baidu.com/,那就说明配置成功了。

请求结果

5.4.2.5 WebClientHttpRoutingFilter

[!tip]

该过滤器的order值为Integer.MAX_VALUE。

WebClientHttpRoutingFilter是一个前置全局过滤器,用于基于 Spring WebClient(响应式 HTTP 客户端)将请求转发到外部服务。它是 NettyRoutingFilter 的替代方案,适用于需要更灵活 HTTP 客户端配置的场景,或与 Spring 生态深度集成的需求。

核心功能

  1. HTTP 请求代理转发:当路由目标 URI 是 http://https:// 时,使用 WebClient(而非 Netty 原生客户端)发送请求到后端服务。

    支持所有 WebClient 特性(如自定义编解码器、拦截器等)。

  2. 响应式非阻塞通信:基于 Project Reactor 实现全异步非阻塞 IO,避免线程阻塞,适合高并发场景。

  3. 与 Spring 生态无缝集成:可直接复用应用中已有的 WebClient Bean,或自定义配置(如超时、SSL、负载均衡等)。

  4. 灵活的重试与容错:结合 RetryFilterCircuitBreakerFilter 实现复杂容错逻辑。

执行时机

  • 阶段:在路由匹配完成且目标 URI 确定为 http://https:// 后执行(顺序值 Integer.MAX_VALUE,与 NettyRoutingFilter 同级)。
  • 触发条件:需显式启用(默认情况下,Gateway 优先使用 NettyRoutingFilter)。

5.4.2.6 ForwardPathFilter

[!tip]

该过滤器的order值为0。

ForwardPathFilter是一个前置全局过滤器,主要用于处理请求路径中的 forward: 前缀,确保路径标准化,以便后续路由匹配和转发逻辑正确执行。

核心功能

  1. 路径标准化:检测请求路径是否以 forward:/ 开头(例如 forward:/api/local),并移除 forward: 前缀,保留实际路径(如 /api/local),以便后续过滤器或控制器处理。确保路径格式符合网关内部转发的要求。
  2. 协议标识处理:标记请求为“本地转发”类型,供后续 ForwardRoutingFilter 识别并执行实际转发操作。
  3. 兼容性保障:处理不同形式的路径输入(如 forward:/pathforward://path),统一转换为标准格式。

执行时机

  • 阶段:在请求进入网关的最早阶段执行(顺序值为 0,属于优先级最高的 Pre Filter)。
  • 触发条件:任何包含 forward: 前缀的请求路径。

5.4.2.7 NettyWriteResponseFilter

[!tip]

该过滤器的order值为-1。

NettyWriteResponseFilter是一个后置全局过滤器,它属于响应处理阶段的最后一个关键组件。它的核心职责是将代理请求(由 NettyRoutingFilterWebClientHttpRoutingFilter 发起)返回的后端服务响应,通过 Netty 的异步非阻塞通道写回给客户端(如浏览器或移动端)。

核心功能

  1. 响应数据回写:将后端服务(如微服务、API)的响应(包括 HTTP 状态码、Headers、Body)通过 Netty 的 HttpServerResponse 写回给原始请求的客户端。
  2. 响应式流式处理:基于 Reactor 的异步流(Flux<DataBuffer>)处理大文件或流式数据,避免内存溢出。
  3. 响应头处理:自动处理 Content-LengthTransfer-Encoding 等头信息,并支持自定义头修改(需配合其他过滤器)。
  4. 异常处理:如果响应流处理过程中发生错误(如连接中断),会触发错误回调并终止连接。

执行时机

  • 阶段:在路由转发完成且收到后端响应后执行(顺序值为 -1,即最后一个执行的过滤器)。
  • 触发条件:仅在网关成功代理请求到后端服务后生效(不适用于 forward: 本地转发)。

5.4.2.8 WebClientWriteResponseFilter

[!tip]

该过滤器的order值为-1。

WebClientWriteResponseFilter是一个后置全局过滤器,它属于响应处理阶段的核心组件。它的作用与 NettyWriteResponseFilter 类似,但专用于通过 Spring WebClient(响应式 HTTP 客户端)代理请求的场景,负责将后端服务的响应数据写回给客户端。

核心功能

  1. 响应数据回写:将 WebClient 代理请求的响应结果(如 JSON 数据、文件流)转换为 Flux<DataBuffer>,通过 ServerHttpResponse 写回客户端。
  2. 响应式流式传输:基于 Reactor 的 Flux 实现非阻塞流式传输,支持大文件或实时流(如 SSE) without 内存溢出。
  3. 响应头处理:自动处理标准头(如 Content-LengthTransfer-Encoding: chunked),并保留其他过滤器(如 AddResponseHeaderFilter)添加的自定义头。
  4. 错误处理:若响应流中断或超时,终止连接并触发错误回调(如记录日志、返回 5xx 状态码)。

5.4.2.9 GlobalLocalResponseCacheGatewayFilter

[!tip]

该过滤器的order值为-3。

GlobalLocalResponseCacheGatewayFilter是一个后置全局过滤器,它是 Spring Cloud Gateway 中的一个全局缓存过滤器,属于响应缓存层的扩展组件。它的核心作用是在网关层面缓存后端服务的响应结果,避免重复请求穿透到上游服务,从而提升性能并降低后端负载。

核心功能

  1. 响应缓存:缓存通过网关转发的后端服务响应(如 HTTP 状态码、Headers、Body),支持基于请求路径、参数或自定义键的缓存策略。适用于高频访问的静态数据(如配置信息、公共接口)。
  2. 缓存粒度控制:支持全局缓存(所有匹配路由的请求)或局部缓存(特定路由启用)。可配置缓存过期时间(TTL)、最大缓存条目等。
  3. 缓存存储:默认使用内存缓存(如 Caffeine),也可集成 Redis 等分布式缓存。
  4. 缓存失效:支持手动清除缓存或通过条件(如 Header 变更)自动失效。

执行时机

  • 阶段:属于后置过滤器(Post Filter),在收到后端响应后、返回给客户端前执行(顺序值通常位于 NettyWriteResponseFilter 之前)。
  • 触发条件:需显式配置启用缓存策略。

[!warning]

由于Gateway的版本不同,这里的配置也会不同,我的Gatew的版本的是4.1.2,所以我的配置方式不一定适合你,你要自己参考自己的版本去配置。

LocalResponseCache配置

从官方文档可知,要想时使用全局的缓存过滤器,你需要有如下步骤:

  1. 检查必要的com.github.ben-manes.caffeine:caffeinespring-boot-starter-cache依赖。

    由于我的网关模板导入了依赖spring-cloud-starter-loadbalancer,里面是有spring-boot-starter-cache依赖的

    spring-boot-starter-cache依赖

    所以我这里只需要导入com.github.ben-manes.caffeine:caffeine依赖:

    <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>3.1.8</version>
    </dependency>
    
  2. 修改配置文件,添加配置spring.cloud.gateway.filter.local-response-cache.enabled

    server:
      port: 80
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          server-addr: 132.232.142.42:8848
          username: nacos
          password: nacos
        gateway:
          filter:
          	# 本地响应缓存配置
            local-response-cache:
              enabled: true # 开启配置
              time-to-live: 10m # 设置缓存的过期时间,以 s 表示秒,m 表示分钟,h 表示小时
              size: 1MB # 设置缓存的最大大小 以 KB、MB 和 GB 为单位
          routes:
            - id: service-order
              uri: lb://service-order
              predicates:
                - Path=/api/order/**
              filters:
                - AddRequestHeader=X-Request-token, 123456789qwe
                - StripPrefix=1  # 移除路径前两级(如/api/order/getNacosOrderConfig → /order/getNacosOrderConfig)
    
  3. 请求必须满足如下3个条件

    请求必须满足的条件

之后,你直接请求测试即可。

5.4.2.10 WebsocketRoutingFilter

[!TIP]

该过滤器的order值为Integer.MAX_VALUE-1。

WebsocketRoutingFilter是一个前置全局过滤器,它是专门处理WebSocket路由的过滤器,当请求是WebSocket升级请求时,它会在路由匹配后、实际转发请求前执行。它的作用是将 WebSocket 请求(如 ws://wss://)透明地代理到后端服务,同时保持长连接的双向通信能力。

核心功能

  1. WebSocket 协议代理:拦截 Upgrade: websocket 的 HTTP 请求,将其转换为 WebSocket 长连接,并代理到后端服务(如 ws://backend-service/chat)。支持明文(ws://)和加密(wss://)连接。
  2. 双向数据转发:实时转发客户端与后端服务之间的消息帧(文本/二进制),保持全双工通信。自动处理 WebSocket 的握手协议(HTTP 101 Switching Protocols)。
  3. 负载均衡集成:兼容 lb: 前缀的服务名,可从注册中心(如 Nacos、Eureka)动态获取实例并负载均衡。
  4. 连接生命周期管理:监控连接状态,在客户端或服务端关闭连接时释放资源。

示例配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: websocket-route
          uri: ws://chat-service  # 或 lb:ws://chat-service
          predicates:
            - Path=/ws/**
          filters:
            - StripPrefix=1       # 可选:移除路径前缀

5.4.2.11 AdaptCachedBodyGlobalFilter

[!tip]

该过滤器的order值为Integer.MIN_VALUE+1000。

AdaptCachedBodyGlobalFilter是一个前置全局过滤器。它的核心作用是缓存请求体数据(如 HTTP POST/PUT 的 Body),并确保后续过滤器或路由逻辑能够多次读取请求体内容(默认情况下,请求体只能被读取一次)。

核心功能

  1. 请求体缓存:拦截 HTTP 请求的 Body 数据,将其缓存在内存或磁盘(根据配置),解决请求体只能消费一次的问题(如读取后无法在后续过滤器中重复使用)。
  2. 数据适配:支持多种数据格式(如 JSON、Form-Data、二进制流),自动适配后续处理逻辑的需要。
  3. 性能优化:通过缓存避免重复解析请求体,提升处理效率(尤其对大型请求体)。
  4. 异常处理:若请求体过大或格式错误,提前终止请求并返回错误(如 413 Payload Too Large)。

5.4.2.12 RemoveCachedBodyFilter

[!tip]

该过滤器的order值为Integer.MIN_VALUE。

RemoveCachedBodyFilter是一个后置全局过滤器。属于资源清理型过滤器,其核心作用是在请求处理完成后,清除由 AdaptCachedBodyGlobalFilter 缓存的请求体数据,释放内存或删除临时文件,避免资源泄漏。

核心功能

  1. 缓存清理:移除 AdaptCachedBodyGlobalFilter 缓存的请求体数据(包括内存中的 DataBuffer 或磁盘临时文件),防止内存溢出或磁盘空间浪费。
  2. 资源回收:确保每个请求的生命周期结束后,其关联的临时资源被及时释放,提升网关的稳定性和性能。
  3. 异常保护:即使请求处理过程中发生异常(如后端服务超时),仍会强制清理缓存。

5.4.2.13 GatewayMetricsFilter

[!tip]

该过滤器的order值为0。

GatewayMetricsFilter是一个既有前置又有后置逻辑的全局过滤器。其核心作用是通过集成 Micrometer 收集网关的关键性能指标(Metrics),帮助用户监控请求流量、延迟、错误率等关键数据。

核心功能

  1. 指标收集
    • HTTP 请求指标:记录每个请求的耗时(gateway.requests.timer)、状态码(gateway.responses)、路由ID等标签。
    • 路由级监控:区分不同路由的流量和性能(如 routeId 标签)。
    • 自定义标签:支持通过配置添加业务维度(如用户ID、服务名)。
  2. 集成 Micrometer:自动将指标导出到监控系统(如 Prometheus、InfluxDB、Datadog),无需手动埋点。
  3. 性能诊断:通过指标定位慢请求(P99 延迟)、高频错误(5xx 状态码)或流量突增。

官方说明

5.4.2.14 LoadBalancerServiceInstanceCookieFilter

[!TIP]

该过滤器的order值为10151。

LoadBalancerServiceInstanceCookieFilter是一个后置全局过滤器。它的核心作用是通过Cookie机制,将客户端的请求自动路由到上一次访问的同一服务实例,实现基于会话的负载均衡粘性。

核心功能:

  1. 会话粘性支持:在网关首次将请求路由到某个服务实例(如通过 ReactiveLoadBalancerClientFilter)时,会在响应中注入一个Cookie(默认名称为 sc-lb-instance-id),记录该实例的唯一标识(如实例ID或IP端口)。客户端后续携带此 Cookie 时,网关会优先将请求路由到同一实例。
  2. 负载均衡集成:与 Spring Cloud LoadBalancer 深度集成,兼容 lb:// 前缀的服务路由,动态解析实例标识。
  3. 灵活性配置:支持自定义 Cookie 名称、有效期(TTL)、路径等参数,适配不同业务场景。

执行时机

  • 阶段:在响应返回客户端前执行,确保在 NettyWriteResponseFilterWebClientWriteResponseFilter 之前注入 Cookie。
  • 触发条件:仅对通过 ReactiveLoadBalancerClientFilter 路由的 lb:// 服务生效。

5.4.2.15 NoLoadBalancerClientFilter

[!tip]

该过滤器的order值为10150

NoLoadBalancerClientFilter是一个前置全局过滤器。它的核心作用是显式标记某些路由不应使用负载均衡,直接跳过 ReactiveLoadBalancerClientFilter 的处理,适用于无需服务发现或需强制直连的场景。

核心功能

  1. 负载均衡强制跳过:当路由配置的 URI 包含 no-load-balancer: 前缀(如 no-load-balancer:http://backend-service)时,该过滤器会阻止 ReactiveLoadBalancerClientFilter 对请求进行负载均衡处理,直接按原始 URI 转发。
  2. 与负载均衡解耦:避免因误配置 lb:// 前缀或缺少服务注册中心导致的路由失败,提供明确的“非负载均衡”语义。
  3. 性能优化:跳过服务发现查询和实例选择逻辑,减少延迟(适用于已知固定实例的场景)。

触发条件:URI 以 no-load-balancer: 开头。

5.5 自定义路由谓语(Predicate)工厂

在之前,我们的路由谓语工厂一直使用的是内置的谓语工厂,现在我们开始来自定义。

我们再自定义之前,先观察一下内置的谓语工厂是怎么定义的,就拿AfterRoutePredicateFactory这个谓语工厂举例:

AfterRoutePredicateFactory结构

从这里可以看出,自定义谓语工厂步骤如下:

  1. 继承抽象路由谓语工厂AbstractRoutePredicateFactory
  2. 编写Config静态内部类
  3. 提供构造方法
  4. 实现apply方法
  5. 如果你的自定义谓语工厂有参数,那么还需要给出常量,然后将常量引用到shortcutFieldOrder方法中,这样你的参数就可以简写了。
  6. 将自定义谓语工厂注册到Spring容器中

官方提供的自定义路由谓语工厂方法


例如我现在自定义一个时间段路由断言工厂:

package cn.yunrain.nxz.predicate;

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * @Author nianxinzhuo
 * @Date 2025/7/1 11:35
 * @description: 自定义时间段路由断言工厂, 根据当前时间判断是否在指定的小时范围内
 */
@Component
public class TimeRangeRoutePredicateFactory extends AbstractRoutePredicateFactory<TimeRangeRoutePredicateFactory.Config> {
    // 定义配置参数的键名,必须和config静态内部类保持一致
    private static final String START_TIME = "startTime";
    private static final String END_TIME = "endTime";

    /**
     * 构造函数,传入配置类
     */
    public TimeRangeRoutePredicateFactory() {
        super(Config.class);
    }

    /**
     * 返回配置参数的键名列表
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(START_TIME, END_TIME);
    }

    /**
     * 核心方法:创建断言逻辑
     *
     * @param config 配置对象
     * @return 返回断言函数
     */
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                // 获取当前小时
                int currentHour = java.time.LocalTime.now().getHour();

                // 获取配置的开始和结束时间
                int startTime = config.getStartTime();
                int endTime = config.getEndTime();

                // 判断当前时间是否在指定范围内
                if (startTime <= endTime) {
                    // 正常时间范围,如 9-17
                    return currentHour >= startTime && currentHour <= endTime;
                } else {
                    // 跨天时间范围,如 22-6
                    return currentHour >= startTime || currentHour <= endTime;
                }
            }

            @Override
            public String toString() {
                return String.format("TimeRange: %d-%d", config.getStartTime(), config.getEndTime());
            }
        };
    }

    /**
     * 配置类,定义断言的参数
     */
    public static class Config {

        /**
         * 开始时间(小时,0-23)
         */
        private int startTime;

        /**
         * 结束时间(小时,0-23)
         */
        private int endTime;

        public int getStartTime() {
            return startTime;
        }

        public void setStartTime(int startTime) {
            this.startTime = startTime;
        }

        public int getEndTime() {
            return endTime;
        }

        public void setEndTime(int endTime) {
            this.endTime = endTime;
        }
    }
}

这就是我自定义时间段路由谓语工厂,要求给定时间段之后,符合该时间段的请求才会将请求转发到下游去。

应用自定义谓语工厂

这里有个细节,你看我的自定义路由谓语工厂再配置文件中的名称(TimeRange),这里也是沿用Gateway官方的写法,只需要取出XXXRoutePredicateFactory中的XXX就可以了。

这里自行测试即可。

5.6 自定义内置路由过滤器(GatewayFilter)

在之前,我们的路由过滤器一直使用的是内置路由过滤器,现在我们开始来自定义。

我们再自定义之前,先观察一下内置的路由过滤器是怎么定义的,就拿StripPrefixGatewayFilterFactory这个内置路由过滤器举例:

StripPrefixGatewayFilterFactory结构

从这里可以看出,自定义路由过滤器步骤如下:

  1. 继承抽象路由过滤器AbstractGatewayFilterFactory
  2. 编写Config静态内部类
  3. 提供构造方法
  4. 实现apply方法
  5. 如果你的自定义路由过滤器有参数,那么还需要用到shortcutFieldOrder方法,将你的参数写入集合中,后面你就可以在yaml配置文件使用这个参数来。
  6. 将自定义路由过滤器注册到Spring容器中

官方提供的自定义路由过滤器方法


例如我现在有一个自定义请求日志过滤器工厂类:

package cn.yunrain.nxz.gatewayFilter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;

/**
 * @author nianxinzhuo
 * @date 2025/7/1 21:17
 * @description 自定义请求日志过滤器工厂类
 */
@Component
public class RequestLogGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestLogGatewayFilterFactory.Config> {

    // 定义日志记录器
    private static final Logger logger = LoggerFactory.getLogger(RequestLogGatewayFilterFactory.class);

    // 构造函数,传入配置类的Class对象
    public RequestLogGatewayFilterFactory() {
        super(Config.class);
    }

    // 定义过滤器的配置参数列表(用于YAML配置中的参数顺序)
    @Override
    public List<String> shortcutFieldOrder() {
        // 集合中的元素要和Config类中的属性名一致
        return Arrays.asList("logRequest", "logResponse");
    }

    // 重写apply方法,这是过滤器的核心逻辑
    @Override
    public GatewayFilter apply(Config config) {
        // 返回一个GatewayFilter实例
        return (ServerWebExchange exchange, GatewayFilterChain chain) -> {

            // 获取请求对象
            ServerHttpRequest request = exchange.getRequest();

            // 记录请求开始时间(用于计算请求耗时)
            long startTime = System.currentTimeMillis();

            // 获取请求的基本信息
            String method = request.getMethod().toString(); // HTTP方法
            String path = request.getURI().getPath(); // 请求路径
            String query = request.getURI().getQuery(); // 查询参数
            String remoteAddress = getRemoteAddress(request); // 客户端IP地址

            // 构建完整的请求URL
            String fullUrl = path + (query != null ? "?" + query : "");

            // 如果配置了需要记录请求日志
            if (config.isLogRequest()) {
                logger.info("=== 请求开始 ===");
                logger.info("请求方法: {}", method);
                logger.info("请求路径: {}", fullUrl);
                logger.info("客户端IP: {}", remoteAddress);
                logger.info("请求头信息: {}", request.getHeaders());
            }

            // 执行过滤器链并处理响应
            return chain.filter(exchange)
                    .doFinally(signalType -> {
                        // 无论请求如何结束(成功、错误、取消),都会执行这里的代码
                        if (config.isLogResponse()) {
                            // 计算请求处理耗时
                            long endTime = System.currentTimeMillis();
                            long duration = endTime - startTime;

                            // 获取响应信息
                            ServerHttpResponse response = exchange.getResponse();

                            // 记录响应信息
                            logger.info("=== 请求结束 ===");
                            logger.info("响应状态码: {}", response.getStatusCode());
                            logger.info("响应头信息: {}", response.getHeaders());
                            logger.info("请求处理耗时: {} ms", duration);
                            logger.info("请求结束信号: {}", signalType); // 显示请求结束的信号类型
                            logger.info("==================");
                        }
                    });
        };
    }

    // 获取客户端真实IP地址的辅助方法
    private String getRemoteAddress(ServerHttpRequest request) {
        // 先尝试从X-Forwarded-For头获取(适用于经过代理的情况)
        String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            // X-Forwarded-For可能包含多个IP,取第一个
            return xForwardedFor.split(",")[0].trim();
        }

        // 尝试从X-Real-IP头获取
        String xRealIp = request.getHeaders().getFirst("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }

        // 如果都没有,则使用远程地址
        return request.getRemoteAddress() != null ?
                request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }

    /**
     * 配置类,用于接收YAML配置文件中的参数
     */
    public static class Config {
        // 是否记录请求日志,默认为true
        private boolean logRequest = true;

        // 是否记录响应日志,默认为true
        private boolean logResponse = true;

        public boolean isLogRequest() {
            return logRequest;
        }

        public void setLogRequest(boolean logRequest) {
            this.logRequest = logRequest;
        }

        public boolean isLogResponse() {
            return logResponse;
        }

        public void setLogResponse(boolean logResponse) {
            this.logResponse = logResponse;
        }
    }
}

这是我的自定义请求日志路由过滤器,要想使用它,你直接在路由过滤器配置项中配置即可:

应用自定义路由过滤器

这里有个细节,你看我的自定义路由过滤器在配置文件中的名称(RequestLog),这里也是沿用Gateway官方的写法,只需要取出XXXGatewayFilterFactory中的XXX就可以了。

测试结果

5.7 自定义全局过滤器(GlobalFilter)

在之前,我们的全局过滤器一直使用的是内置全局过滤器,现在我们开始来自定义。

我们再自定义之前,先观察一下内置的全局过滤器是怎么定义的,就拿RouterToRequestUrlFilter这个全局过滤器举例:

RouteToRequestUrlFilter结构

从这里可以看出,自定义全局滤器步骤如下:

  1. 实现GlobalFilterOrdered接口
  2. 将该自定义全局过滤器注册到Spring容器中
  3. 指定过滤器优先级,即Order的值
  4. 编写过滤器核心方法filter

官方提供的自定义全局过滤器方法


例如我现在有一个自定义全局请求响应日志过滤器:

package cn.yunrain.nxz.globalFilter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

/**
 * @author nianxinzhuo
 * @date 2025/7/1 22:26
 * @description 全局请求响应日志过滤器
 */
@Component
@Slf4j
public class GlobalLoggingFilter implements GlobalFilter, Ordered {
    // 定义过滤器执行顺序,数字越小优先级越高
    private static final int FILTER_ORDER = -1;

    /**
     * 过滤器核心逻辑
     * @param exchange 封装了HTTP请求和响应的对象
     * @param chain 过滤器链,用于传递到下一个过滤器
     * @return 返回Mono<Void>,表示异步处理结果
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求对象
        ServerHttpRequest request = exchange.getRequest();

        // 记录请求开始时间
        long startTime = System.currentTimeMillis();

        // 生成唯一的请求ID,用于追踪整个请求链路
        String requestId = UUID.randomUUID().toString().substring(0, 8);

        // 获取请求基本信息
        String method = request.getMethod().name(); // HTTP方法
        String uri = request.getURI().toString(); // 完整URI
        String path = request.getPath().value(); // 请求路径
        String clientIp = getClientIp(request); // 客户端IP

        // 记录请求开始日志
        log.info("[{}] 请求开始 - {} {} from {}", requestId, method, uri, clientIp);
        log.debug("[{}] 请求头: {}", requestId, request.getHeaders());

        // 在请求属性中存储请求ID和开始时间,供后续使用
        exchange.getAttributes().put("REQUEST_ID", requestId);
        exchange.getAttributes().put("START_TIME", startTime);

        // 创建修改后的请求,添加请求ID到请求头中
        ServerHttpRequest modifiedRequest = request.mutate()
                .header("X-Request-ID", requestId) // 添加请求追踪ID
                .header("X-Request-Time", String.valueOf(startTime)) // 添加请求时间
                .build();

        // 创建新的exchange对象
        ServerWebExchange modifiedExchange = exchange.mutate()
                .request(modifiedRequest)
                .build();

        // 继续执行过滤器链,并在完成后记录响应日志
        return chain.filter(modifiedExchange)
                .doOnSuccess(onSuccess -> {
                    // 请求成功完成时执行
                    logResponse(modifiedExchange, requestId, startTime, "SUCCESS");
                })
                .doOnError(onError -> {
                    // 请求出现异常时执行
                    log.error("[{}] 请求异常: {}", requestId, onError.getMessage());
                    logResponse(modifiedExchange, requestId, startTime, "ERROR");
                })
                .doFinally(onFinally -> {
                    // 无论请求如何结束都会执行(成功、异常、取消)
                    log.debug("[{}] 请求处理完成,信号类型: {}", requestId, onFinally);
                });
    }

    /**
     * 记录响应日志的私有方法
     */
    private void logResponse(ServerWebExchange exchange, String requestId, long startTime, String status) {
        // 获取响应对象
        ServerHttpResponse response = exchange.getResponse();

        // 计算请求处理耗时
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;

        // 获取响应状态码
        HttpStatusCode statusCode = response.getStatusCode();

        // 记录响应日志
        log.info("[{}] 请求结束 - 状态码: {}, 耗时: {}ms, 处理状态: {}",
                requestId, statusCode, duration, status);
        log.debug("[{}] 响应头: {}", requestId, response.getHeaders());

        // 如果耗时过长,记录警告日志
        if (duration > 1000) {
            log.warn("[{}] 请求耗时过长: {}ms", requestId, duration);
        }
    }

    /**
     * 获取客户端真实IP地址
     */
    private String getClientIp(ServerHttpRequest request) {
        // 尝试从各种代理头中获取真实IP
        String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP"};

        for (String header : headers) {
            String ip = request.getHeaders().getFirst(header);
            if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
                // X-Forwarded-For可能包含多个IP,取第一个
                return ip.split(",")[0].trim();
            }
        }

        // 如果都没有,使用远程地址
        return request.getRemoteAddress() != null ?
                request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }

    /**
     * 定义过滤器的执行顺序
     * @return 返回整数,数值越小优先级越高
     */
    @Override
    public int getOrder() {
        return FILTER_ORDER;
    }
}

测试结果

5.8 ServerWebExchange

ServerWebExchange是Spring WebFlux框架中的核心抽象,在Spring Cloud Gateway中扮演着请求上下文容器的关键角色。它封装了整个HTTP请求-响应的生命周期信息。

ServerWebExchange可以理解为一个"交换对象",它包含了处理一个HTTP请求所需的所有信息和操作接口。类似于Servlet API中的HttpServletRequestHttpServletResponse的组合体,但更加强大和灵活。

为什么需要Exchange?在传统的Servlet模型中,请求和响应是分离的对象。而在响应式编程模型中,需要一个统一的上下文来管理请求处理过程中的所有状态和数据流转。

ServerWebExchange接口源码:

/**
 * HTTP 请求-响应交互的协定。提供对 HTTP 请求和响应的访问,并公开其他服务器端处理相关属性和功能,例如请求属性。
 *
 * @author Rossen Stoyanchev
 * @since 5.0
 */
public interface ServerWebExchange {

	/**
	 * 这是一个日志关联ID的属性键,用于在日志中关联同一个请求的所有日志消息。通过getLogPrefix()方法可以获得基于此属性的格式化日志前缀。
	 * @since 5.1
	 * @see #getLogPrefix()
	 */
	String LOG_ID_ATTRIBUTE = ServerWebExchange.class.getName() + ".LOG_ID";


	/**
	 * 返回一个http请求对象
	 */
	ServerHttpRequest getRequest();

	/**
	 * 返回一个http请求响应对象
	 */
	ServerHttpResponse getResponse();

	/**
	 * 返回可变的属性映射,用于在请求处理过程中存储和传递数据。
	 */
	Map<String, Object> getAttributes();

	/**
	 * 类型安全地获取属性值,如果不存在返回null。
	 */
	@SuppressWarnings("unchecked")
	@Nullable
	default <T> T getAttribute(String name) {
		return (T) getAttributes().get(name);
	}

	/**
	 * 获取必需的属性,如果不存在会抛出IllegalArgumentException异常。
	 */
	@SuppressWarnings("unchecked")
	default <T> T getRequiredAttribute(String name) {
		T value = getAttribute(name);
		Assert.notNull(value, () -> "Required attribute '" + name + "' is missing");
		return value;
	}

	/**
	 * 获取属性值,如果不存在返回默认值。
	 */
	@SuppressWarnings("unchecked")
	default <T> T getAttributeOrDefault(String name, T defaultValue) {
		return (T) getAttributes().getOrDefault(name, defaultValue);
	}

	/**
	 * 返回当前请求的Web会话。始终保证返回一个实例,要么匹配客户端请求的会话ID,要么创建新的会话ID。注意:调用此方法不会自动创建会话。
	 */
	Mono<WebSession> getSession();

	/**
	 * 返回请求的已认证用户主体(如果有的话)。
	 */
	<T extends Principal> Mono<T> getPrincipal();

	/**
	 * 当Content-Type为application/x-www-form-urlencoded时返回表单数据,否则返回空映射。
	 * 重要:此方法会读取和解析整个请求体,结果会被缓存,因此可以安全地多次调用。
	 */
	Mono<MultiValueMap<String, String>> getFormData();

	/**
	 * 当Content-Type为multipart/form-data时返回多部分数据,否则返回空映射。
	 * 注意:每个Part的内容不会被缓存,只能读取一次。
	 */
	Mono<MultiValueMap<String, Part>> getMultipartData();

	/**
	 * 清理多部分处理使用的存储,Spring 6.0.10版本新增。
	 */
	default Mono<Void> cleanupMultipart() {
		return getMultipartData()
				.onErrorComplete()  // ignore errors reading multipart data
				.flatMapIterable(Map::values)
				.flatMapIterable(Function.identity())
				.flatMap(part -> part.delete().onErrorComplete())
				.then();
	}

	/**
	 * 使用配置的LocaleContextResolver返回LocaleContext,用于国际化支持。
	 */
	LocaleContext getLocaleContext();

	/**
	 * 返回与Web应用关联的ApplicationContext(如果通过WebHttpHandlerBuilder初始化的话)。
	 */
	@Nullable
	ApplicationContext getApplicationContext();

	/**
	 * 如果调用了checkNotModified方法且返回true,则此方法返回true。
	 */
	boolean isNotModified();

	/**
	 * 仅基于最后修改时间检查资源是否未修改。
	 */
	boolean checkNotModified(Instant lastModified);

	/**
	 * 仅基于ETag检查资源是否未修改。
	 */
	boolean checkNotModified(String etag);

	/**
	 * 同时基于ETag和最后修改时间检查资源是否未修改。这是最完整的缓存检查方法。
	 */
	boolean checkNotModified(@Nullable String etag, Instant lastModified);

	/**
	 * 根据注册的转换函数转换给定的URL。默认情况下返回原始URL。
	 */
	String transformUrl(String url);

	/**
	 * 注册额外的URL转换函数,可用于插入认证ID、CSRF防护nonce等。
	 */
	void addUrlTransformer(Function<String, String> transformer);

	/**
	 * 返回用于关联此exchange消息的日志前缀。基于LOG_ID_ATTRIBUTE属性值,并进行额外格式化,可以方便地添加到日志消息前。
	 */
	String getLogPrefix();

	/**
	 * 返回一个构建器来修改此exchange的属性,通过ServerWebExchangeDecorator包装。
	 */
	default Builder mutate() {
		return new DefaultServerWebExchangeBuilder(this);
	}


	/**
	 * Builder for mutating an existing {@link ServerWebExchange}.
	 * Removes the need
	 */
	interface Builder {

		/**
		 * 配置一个消费者来使用构建器修改当前请求,这是一个便捷方法。
		 */
		Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);

		/**
		 * 直接设置要使用的请求对象,特别是需要重写ServerHttpRequest方法时使用。
		 */
		Builder request(ServerHttpRequest request);

		/**
		 * 设置要使用的响应对象。
		 */
		Builder response(ServerHttpResponse response);

		/**
		 * 设置此exchange返回的Principal。
		 */
		Builder principal(Mono<Principal> principalMono);

		/**
		 * 构建带有修改属性的ServerWebExchange装饰器。
		 */
		ServerWebExchange build();
	}

}

核心概念与作用:

  1. 请求-响应的统一入口: 它是 Spring WebFlux 框架(Gateway 构建于其上)定义的关键接口,代表了一次完整的、非阻塞的 HTTP 请求-响应交换。
  2. 贯穿过滤器链: ServerWebExchange 对象作为参数传递到 Gateway 的每一个 GlobalFilterRouteFilter 中。所有过滤器操作的都是同一个 ServerWebExchange 实例,这使得过滤器之间可以通过它来传递信息、修改请求或响应。
  3. 信息容器: 它持有当前请求和响应的关键对象,并提供访问它们的途径。
  4. 操作入口: 提供了修改请求、响应以及管理共享属性的方法。

[!caution]

  1. 不可变性:ServerWebExchange 及其包含的请求和响应对象都是不可变的,修改时需要使用构建器模式创建新对象。
  2. 响应式编程:所有操作都应该返回 Mono 或 Flux,避免阻塞操作。
  3. 属性传递:通过属性机制在过滤器链中传递数据,而不是使用 ThreadLocal。
  4. 内存管理:处理请求体时要注意 DataBuffer 的释放,避免内存泄漏。

5.9 GatewayFilterChain

GatewayFilterChain是一个接口,定义了过滤器链的执行方式。它采用责任链模式,将多个过滤器按照特定顺序串联起来,每个过滤器都可以对请求进行处理,然后决定是否继续传递给下一个过滤器。

GatewayFilterChain源码

核心概念与作用:

  1. 职责链模式 (Chain of Responsibility): Gateway 使用经典的责任链模式来处理请求。GatewayFilterChain 就是这个链条的抽象表示。每个过滤器 (GatewayFilter) 是链条上的一个节点。
  2. 执行流程控制: GatewayFilterChain 提供了 filter(ServerWebExchange exchange) 方法。调用这个方法意味着“将当前的 exchange 对象传递给链中的下一个过滤器(或最终目标)进行处理”。这是过滤器链向前推进的唯一方式。
  3. 组合全局过滤器和路由过滤器: Gateway 运行时会动态地将匹配到的路由配置的 GatewayFilter(路由过滤器)和所有的 GlobalFilter(全局过滤器)按照特定顺序组合成一个逻辑上的 GatewayFilterChain
  4. 短路机制: 任何过滤器在接收到 exchange 后,可以选择:
    • 继续执行链: 调用 chain.filter(exchange) 将控制权交给链中的下一个过滤器。
    • 短路链: 不调用 chain.filter(exchange),而是直接操作 exchange.getResponse() 设置响应(例如设置状态码、写入响应体)并返回一个 Mono(通常是 Mono.empty()exchange.getResponse().setComplete())。这会导致后续所有过滤器(包括代理请求到后端)都不再执行。
  5. ServerWebExchange 协作: 整个过滤器链的执行都是围绕同一个 ServerWebExchange 实例进行的。每个过滤器接收它、修改它(或基于它做决策),并通过 chain.filter(exchange) 将它传递给下一个环节。

5.10 Gateway与Sentinel授权规则

在Sentinel中,有一个授权规则之前没有讲解,这里结合Gateway讲解一下。

Sentinel的授权规则主要是控制请求的来源,即和请求头中的origin配置有关。

这里我们在网关中配置请求来源了,也就是配置请求头:

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
          filters:
            - AddRequestHeader=origin, gateway-sentinel-auth # 给请求添加请求头属性 origin
            - StripPrefix=1  # 移除路径前两级(如/api/order/getNacosOrderConfig → /order/getNacosOrderConfig)

这样Gateway当断言命中之后,就会将请求路由到service-order微服务中,且带上origin请求头属性。

现在我们来到具体的微服务(service-order)中来解析出这个请求头:

package cn.yunrain.nxz.sentinel;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

/**
 * @author 念心卓
 * @version 1.0
 * @description: 自定义Sentinel请求来源解析器
 * @date 2025/7/3 21:31
 */
@Component
public class HeaderOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1.获取请求头
        String origin = request.getHeader("origin");
        // 2.非空判断
        if (StringUtils.isEmpty(origin)) {
            origin = "blank";
        }
        return origin;
    }
}

在service-order微服务中,我示例的具体接口如下:

@GetMapping("/getNacosOrderConfig")
@SentinelResource(value = "getNacosOrderConfigController")
public String getNacosOrderConfig(@RequestParam(value = "yamlName", required = false) String yamlName, HttpServletRequest request) {
    log.info("获取到的请求头origin: {}", request.getHeader("origin"));
    return "Nacos配置-" + yamlName + "- 命名空间: " + orderProperties.getNameSpace() +
        ", 分组: " + orderProperties.getGroup() +
        ", URL: " + orderProperties.getUrl();
}

现在我们在Sentinel控制台给对应资源配置上授权规则:

白名单授权规则

现在我们将请求发送到网关,让网关给我们路由到具体的接口去:

请求成功

可见从网关出发请求成功,因为从网关出发的接口,当匹配到路由的时候,会执行我上边配置的内置网关过滤器AddRequestHeaderGatewayFilterFactory,在请求头中新增origin配置,值为gateway-sentinel-auth。

如果我们直接请求具体的某个微服务,不从网关出发:

请求失败

6. Seata

参考文档:

Seata(Simple Extensible Autonomous Transaction Architecture)是一款由阿里巴巴开源的分布式事务解决方案,致力于为微服务架构提供高性能、易集成的分布式事务服务。它通过全局事务协调机制,保障跨服务、跨数据库的数据一致性。

Seata 的出现是为了解决微服务架构下跨服务、跨数据库的数据一致性问题,它能够保证在跨数据库操作时,如果有一个微服务请求出现异常,那么所有跨库的操作全部回滚。

对于Seata,官方给出了如下示例图:

分布式事务

后续我们都将围绕这这个图进行讲解。

6.1 Seata术语表

  1. 事务协调器(TC - Transaction Coordinator):维护全局和分支事务的状态,驱动全局事务提交或回滚,是Seata的核心组件,负责协调整个分布式事务的执行。
  2. 事务管理器(TM - Transaction Manager):定义全局事务的范围,开始全局事务、提交或回滚全局事务,通常是业务服务的入口点。
  3. 资源管理器(RM - Resource Manager):管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,负责具体的资源操作,如数据库、消息队列等。

分布式事务

从上图可见RM(资源管理器)其实就是每个具体微服务中某个接口,TM(事务管理器)其实就是业务入口处,TC(事务协调器)其实就是管理各个分支事务所提交的事务。

上图大致的流程如下:

  1. TM开启全局事务并向TC注册
  2. 各个微服务的RM向TC注册分支事务
  3. 业务逻辑执行完毕后,TM决定提交或回滚
  4. TC协调所有RM执行相应的操作

6.2 Seata安装

安装包:安装包下载

安装参考文档:安装参考文档

根据自己Spring Cloud Alibaba的版本下载对应版本的Seata。

版本对照说明

我这里就下载Seata V2.0.0。

历史版本下载

二进制下载

文件下载好了之后,这里我将讲解Linux服务器方式安装。


安装步骤:

  1. 导入数据库模版文件到服务器,数据库文件存放在seata安装目录/seata/script/server/db

    数据库导入选择

  2. 修改config.txt文件,该文件位于seata安装目录/seata/script/config-center

    config.txt文件位置

    修改的地方如下:

    修改点

  3. 修改seata配置文件

    进入seata安装目录/seata/conf文件中,将原有的application.yaml重命名为application_bak.yaml,复制application.example.yml文件,命名为application.yaml

    application.yaml文件变更

    之后,将原有application.yaml,也就是现在的application_bak.yaml文件中的consolesecurity配置移动到现有application.yaml中的如图位置:

    console存放位置

    console配置存放的位置只需要保持顶级配置即可,同时,改配置代表着seata控制台的登陆账号和密码。

    security配置存放位置

    security配置存放位置位于seata配置的子配置,直接存放到文件末尾即可,注意缩进格式。

    之后修改seata配置项,我后面是使用Nacos作为Seata的配置中心和注册中心,这里我提前在Nacos把对应的namespace创建出来。

    创建seata命名空间

    之后主要更改seata配置项中nacos相关的配置。

    seata的nacos配置中心配置

    seata的nacos注册中心配置

    seata数据存储配置

  4. 将seata的配置(config.txt)推送到Nacos中存储,这一步是必须要做的。

    在目录seata安装目录/seata/script/config-center/nacos找到nacos-config.sh脚本文件。

    nacos-config.sh文件

    之后执行命令:

    sh nacos-config.sh -h nacos服务地址 -p 8848 -g SEATA_GROUP -t 刚刚注册时候的命名空间id -u nacos -w nacos
    

    对于我这里,就是如下命令:

    sh nacos-config.sh -h nacos服务地址 -p 8848 -g SEATA_GROUP -t 2705bd91-95db-4261-9179-992676316695 -u nacos -w nacos
    

    执行结果

    查看nacos数据

    可见确实是推送成功了。

  5. 启动Seata服务

    启动命令

    sh seata-server.sh -h Seata服IP -p 8091 -m db
    

    [!caution]

    如果你的Seata和Nacos都是部署到服务器上的,如果你想要本地代码连接Seata,那么你这里启动的时候就必须指明Seata服务公网IP,不能使用内网地址,不然你注册到Nacos上显示的就是内网IP,同时你本地也无法连接远程的Seata服务。

    当然,如果你本地不连接远程Seata服务,那么你就直接执行脚本命令即可。

    启动seata并查看日志


现在seata服务也就安装并且启动完成了。

[!tip]

Seata服务会用到两个端口:

  1. 控制台端口:7091
  2. 服务端口:8091

seata控制台

seata控制台

当你Seata服务启动之后,由于你的配置中心和注册中心都是用的Nacos,所以,你也能够在Nacos中看到对应的服务列表:

seata服务

seata服务详情

至此Seata服务就安装完毕。

6.3 SpringCloud集成Seata

之前Seata安装完毕了,现在将它与Spring Cloud集成:

  1. 添加依赖:

    <!-- seata -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!--服务发现-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <!--配置中心-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    

    其他依赖这里就省略了,这里主要配置最核心的依赖,即seata和nacos。

  2. 编写配置文件

    server:
      port: 8080
    spring:
      # 服务的名称必须填写,否则服务无法注册上
      application:
        name: service-order
      cloud:
        nacos:
          server-addr: 127.0.0.1:8848
          # 如果你的nacos配置文件开启了鉴权,那么这里你就必须要配置用户名和密码,否则就会报错
          discovery:
            username: nacos
            password: nacos
          #配置 配置中心的用户名和密码
          config:
            username: nacos
            password: nacos
    
    seata:
      #随便写,一般为当前微服务名称
      application-id: ${spring.application.name}
      #自定义的事务分组名 一般为服务名称 + tx-group,这个配置要与nacos上的配置一致
      tx-service-group: ${spring.application.name}-tx-group
    
      #seata的注册中心配置,这里的配置要和服务器中的 seata安装目录/seata/conf/application.yaml 中的seata.registry配置一致
      registry:
        #采用nacos作为注册中心
        type: nacos
        nacos:
          application: seata-server
          server-addr: ${spring.cloud.nacos.server-addr}
          username: ${spring.cloud.nacos.config.username}
          password: ${spring.cloud.nacos.config.password}
          group: SEATA_GROUP
          namespace: 2705bd91-95db-4261-9179-992676316695
          cluster: default
    
      #seata的配置中心配置,这里的配置要和服务器中的 seata安装目录/seata/conf/application.yaml 中的seata.config配置一致
      config:
        #采用nacos作为配置中心
        type: nacos
        nacos:
          server-addr: ${spring.cloud.nacos.server-addr}
          username: ${spring.cloud.nacos.config.username}
          password: ${spring.cloud.nacos.config.password}
          group: SEATA_GROUP
          namespace: 2705bd91-95db-4261-9179-992676316695
      service:
      	#事务分组 → Seata集群映射,value为集群名称,这里的seata集群名称为default,
      	#需与 Seata Server 注册到 Nacos 时的cluster名称一致
        vgroup-mapping:
          ${spring.application.name}-tx-group: default
    
      client:
        # 客户端中RM(资源管理器)的配置
        rm:
          # 是否上报分支事务成功状态 (默认 false)
          report-success-enable: true
          # 状态上报失败时的重试次数(默认 5)
          report-retry-count: 5
    
  3. 在Nacos上创建对应tx-service-group。

    不同微服务创建不同事务分组配置


基础的配置了解了,现在我们来看看Seata的快速运用。

参考官方文档:Seata快速开始

基础架构

参考官方的快速启动,创建4个微服务,仓储微服务、订单微服务、账户微服务、业务微服务。同时,调用的入口为业务微服务提供。

这里微服务的创建,以及对应依赖的引入为这里就省略了,主要来看看各个微服务的配置文件和使用Seata保证分布式事务一致性。

这里的配置文件我拿其中一个微服务的来说明,其他微服务的都是类似的:

server:
  port: 8084
spring:
  application:
    name: service-storage

  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
          username: nacos
          password: nacos
      config:
        username: nacos
        password: nacos

seata:
  application-id: ${spring.application.name}
  tx-service-group: ${spring.application.name}-tx-group
  service:
    vgroup-mapping:
      ${spring.application.name}-tx-group: default

  client:
    rm:
      report-success-enable: true
      report-retry-count: 5
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: ${spring.cloud.nacos.server-addr}
      username: ${spring.cloud.nacos.config.username}
      password: ${spring.cloud.nacos.config.password}
      group: SEATA_GROUP
      namespace: 2705bd91-95db-4261-9179-992676316695
      cluster: default
  config:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.server-addr}
      username: ${spring.cloud.nacos.config.username}
      password: ${spring.cloud.nacos.config.password}
      group: SEATA_GROUP
      namespace: 2705bd91-95db-4261-9179-992676316695

可见这里需要一个事务分组的一个配置${spring.application.name}-tx-group,在之前,我就提前创建好了:

不同微服务创建不同事务分组配置

接下来需要使用Seata来校验分布式事务的一致性。这里是需要操作数据库的,所以给Storage、Order、Account这三个微服务分别创建一个数据库,然后给出建表SQL:

各个微服务数据库创建

CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


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;

insert into account_tbl value (1,1,1000);
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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;
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


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;

insert into storage_tbl value (1,'P001',100);

现在给出各个微服务的核心代码:

  1. 仓储微服务(Storage):

    /**
     * 扣减库存
     *
     * @param dto 请求参数
     * @return 是否扣减成功
     */
    @Override
    public Boolean deduct(StorageDeductDto dto) {
        log.info("扣减库存,商品编码:{},数量:{}", dto.getCommodityCode(), dto.getCount());
        StorageTbl storage = this.getOne(
                new LambdaQueryWrapper<StorageTbl>()
                        .eq(StorageTbl::getCommodityCode, dto.getCommodityCode())
        );
        if (storage == null) {
            log.warn("扣减库存失败,商品编码:{} 不存在", dto.getCommodityCode());
            throw new RuntimeException("商品编码不存在");
        }
    
        if (storage.getCount() < dto.getCount()) {
            log.warn("扣减库存失败,商品编码:{},库存不足,当前库存:{},请求扣减数量:{}",
                    dto.getCommodityCode(), storage.getCount(), dto.getCount());
            throw new RuntimeException("库存不足");
        }
    
        storage.setCount(storage.getCount() - dto.getCount());
        boolean updateResult = this.updateById(storage);
        log.info("扣减库存成功,商品编码:{},剩余数量:{}", dto.getCommodityCode(), storage.getCount());
        return updateResult;
    }
    
  2. 订单微服务(Order):

    /**
     * 创建订单
     *
     * @param dto 订单创建请求参数
     * @return 是否创建成功
     */
    @Override
    public Boolean create(OrderCreateDto dto) {
        log.info("开始创建订单,订单参数: {}", dto);
        //先要扣款
        AccountDebitDto accountDebitDto = new AccountDebitDto();
        accountDebitDto.setUserId(dto.getUserId());
        accountDebitDto.setMoney((long) 10 * dto.getOrderCount());
    
        Boolean debitFlag = accountFeignClient.debit(accountDebitDto).getData();
    
        Boolean createFlag = Boolean.FALSE;
        if (debitFlag) {
            log.info("扣款成功,开始创建订单");
            //扣款成功后,创建订单
            OrderTbl order = new OrderTbl();
            order.setUserId(String.valueOf(dto.getUserId()));
            order.setCommodityCode(dto.getCommodityCode());
            order.setCount(dto.getOrderCount());
            order.setMoney(accountDebitDto.getMoney().intValue());
            createFlag = this.save(order);
    
            log.info("订单创建成功: {}", order);
            return createFlag;
        } else {
            log.error("扣款失败,无法创建订单");
            throw new RuntimeException("扣款失败,无法创建订单");
        }
    }
    
  3. 账户微服务(Account):

    /**
     * 扣款
     *
     * @param dto 扣款请求
     * @return 是否扣款成功
     */
    @Override
    public Boolean debit(AccountDebitDto dto) {
        Account account = this.getById(dto.getUserId());
    
        if (account == null) {
            log.error("账户不存在,userId: {}", dto.getUserId());
            throw new RuntimeException("账户不存在");
        }
    
        if (account.getMoney() < dto.getMoney()) {
            log.error("账户余额不足,userId: {}, 当前余额: {}, 扣款金额: {}", dto.getUserId(), account.getMoney(), dto.getMoney());
            throw new RuntimeException("账户余额不足");
        }
    
        LambdaUpdateWrapper<Account> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Account::getUserId, dto.getUserId())
                .set(Account::getMoney, account.getMoney() - dto.getMoney());
    
        boolean updateFlag = this.update(updateWrapper);
        if (updateFlag) {
            log.info("扣款成功,userId: {}, 扣款金额: {}", dto.getUserId(), dto.getMoney());
        } else {
            log.info("扣款失败,userId: {}, 扣款金额: {}", dto.getUserId(), dto.getMoney());
        }
    
        return updateFlag;
    }
    
  4. 业务微服务(Business):

    @PostMapping("purchase")
    @GlobalTransactional(rollbackFor = Exception.class)
    public R<Boolean> purchase(@RequestBody BusinessPurchaseDto dto){
        StorageDeductDto storageDeductDto = new StorageDeductDto();
        storageDeductDto.setCommodityCode(dto.getCommodityCode());
        storageDeductDto.setCount(dto.getCount());
    
        Boolean storageFlag = storageFeignClient.deduct(storageDeductDto).getData();
    
        OrderCreateDto orderCreateDto = new OrderCreateDto();
        orderCreateDto.setUserId(Long.valueOf(dto.getUserId()));
        orderCreateDto.setCommodityCode(dto.getCommodityCode());
        orderCreateDto.setOrderCount(dto.getCount());
    
        Boolean orderFlag = orderFeignClient.create(orderCreateDto).getData();
        return R.success(storageFlag && orderFlag);
    }
    

    对于业务微服务,你会发现,这里有一个注解@GlobalTransactional,这个注解就是全局事务注解,控制整个分布式微服务的提交与回滚。

现在我们给不存在的一个用户发送下单请求,看看各个数据库的数据是否回滚:

发送下单请求

从之前提供的代码可知,下单是先扣件的库存,然后才到创建订单,最后到扣款,这里我给到一个不存在的用户,扣款业务会抛出异常:

Account微服务出现异常

现在我们来看看仓储微服务的数据执行状态:

Storag执行状况

可见在第一阶段的时候,是成功扣减了库存,并且提交了本地事务,但是对于Account微服务出现了异常,TC(事务协调器)发现有一个微服务的事务出现了回滚,那么就会通知所有的微服务回滚事务,也就是上图看见的Storage微服务在第二阶段回滚事务。

现在我们在来观察一下数据库的数据是否也都回滚掉了:

各个数据库表的数据状态

可见,对于分步式事务,这里使用一个注解@GlobalTransactional即可控制分布式事务。

[!tip]

Seata中默认使用的TC事务默认,所以你使用@GlobalTransactional注解即可控制分布式事务了。


现在我们来打断点,看看程序执行过程中,Seata控制台,分布式事务的控制情况,以及数据表的状态:

断点设置位置

我将断点设置到订单创建处,会先进行仓储扣减,在订单创建的时候,会先到Account账户微服务中进行用户校验以及账户余额扣减。我们来看看Storage的数据表状态以及Seata服务状态:

Seata控制台事务信息查看

全局锁

storage表数据

storage库中的undo_log表记录

可见在事务执行过程中,如果已经提交本地事务,那么对应数据库表的数据也会变更,同时该库的undo_log表也会有回滚记录。

当TC通知各个RM回滚事务的时候,undo_log表就会派上用场。

6.4 四种事务模式

在上面我们将Seata整合到了SpringCloud中,使用了一个@GlobalTransactional注解来保证分布式事务的一致性。现在我们来简单了解一下在Seata中的四种事务模式。

6.4.1 Seata AT 模式

参考:官方AT模式讲解

AT模式(Auto Transaction)是Seata最核心和最常用的分布式事务模式。它的设计理念是在不改变业务代码的前提下(无业务侵入性),通过自动生成回滚日志实现分布式事务的最终一致性。

使用该模式的前提:

  1. 基于支持本地 ACID 事务的关系型数据库。AT模式目前支持的数据库有:MySQL、Oracle、PostgreSQL、 TiDB、MariaDB。
  2. Java 应用,通过 JDBC 访问数据库。

6.4.1.1 核心原理

AT 模式对传统 2PC(两阶段提交)进行改造:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源(高性能关键)。
  • 二阶段:根据一阶段的结果决定提交或回滚,同时异步删除回滚日志(提交)或反向补偿(回滚)。

[!caution]

这里在第一阶段提交本地事务之前,要先获取到全局锁才可以提交,否则第一阶段的本地事务是不会提交的。同时,全局锁的释放要等到第二阶段截至才会释放,也就是全局提交或全局回滚之后,全局锁才会被释放掉。

关键创新:

  1. 本地事务提交:一阶段就提交本地事务,释放数据库锁。
  2. 自动回滚:通过undo_log自动生成反向SQL实现回滚。
  3. 无业务侵入:对业务代码完全透明。

6.4.1.2 完整工作流程

  1. 阶段一:分支事务提交

    sequenceDiagram participant RM as 业务服务(RM) participant DB as 数据库 participant TC as Seata TC RM->>DB: 执行业务SQL RM->>DB: 生成前镜像(before_image)和后镜像(after_image) RM->>DB: 写入undo_log(含XID、分支ID、镜像数据) RM->>DB: 提交本地事务(业务SQL + undo_log 原子提交) RM->>TC: 注册分支事务状态(成功)

    其中undo_log 存储格式为 JSON(包含修改前后的数据快照)。提交本地事务时,业务 SQL 和 undo_log 同时提交或同时回滚

  2. 阶段二:全局事务判断

    在第二阶段的时候,如果所有的分支事务都提交成功,那么全局事务也提交成功;如果分支事务有一个提交失败,那么全局事务就会执行回滚,通知所有分支事务回滚数据。

    全局提交:

    sequenceDiagram TC->>RM: 异步通知删除undo_log RM->>DB: 删除undo_log(异步执行,快速释放资源)

    全局提交时,TC(事务协调器)会异步通知各个RM(资源管理器)删除undo_log表中的数据。


    全局回滚:

    sequenceDiagram TC->>RM: 通知回滚分支事务(携带XID) RM->>DB: 查询undo_log(根据XID) RM->>DB: 根据before_image生成反向SQL(如INSERT变DELETE) RM->>DB: 执行业务补偿(反向SQL) RM->>DB: 删除undo_log RM->>TC: 上报回滚成功

    如果有一个分支事务提交失败,那么TC(事务协调器)就会通知各个RM(资源管理器)回滚各自的事务任务。

6.4.1.3 写隔离

在AT模式下,是不会发生数据的脏写问题的。

因为在一阶段本地事务提交前,需要确保先拿到全局锁,拿不到全局锁 ,不能提交本地事务,拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。


以一个示例来说明:现在有两个全局事务tx1和tx2,分别对a表的m字段进行更新操作,m的初始值1000。

tx1先开始,开启本地事务,拿到本地锁,更新操作m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。 tx2后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁 ,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁

tx1二阶段全局提交,释放全局锁 。tx2拿到全局锁 提交本地事务。

image-20250709220954528

如果此时tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果tx2仍在等待该数据的全局锁(因为如果tx1要触发全局回滚的话,此时全局锁还在tx1事务上的,tx2仍然获取不到),同时持有本地锁,则tx1的分支回滚会失败。分支的回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。

因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题。

6.4.1.4 读隔离

在数据库本地事务隔离级别读已提交(Read Committed)或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的读已提交 ,目前Seata的方式是通过SELECT FOR UPDATE语句的代理。

SELECT FOR UPDATE语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。

出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对FOR UPDATE的SELECT语句。

6.4.2 Seata XA模式

参考文档:

6.4.3 Seata TCC模式

参考文档:

6.4.4 Seata Saga模式

参考文档: