一篇文章入门微服务3万字超长解析

核心概念介绍

微服务架构⻛格,就像是把⼀个单独的应⽤程序开发为⼀套⼩服务,每个⼩服务运⾏在⾃⼰的进程
中,并使⽤轻量级机制通信,通常是 HTTP API。这些服务围绕业务能⼒来构建, 并通过完全⾃
动化部署机制来独⽴部署。这些服务使⽤不同的编程语⾔书写,以及不同数据存储技术,并保持最
低限度的集中式管理。
简⽽⾔之:拒绝⼤型单体应⽤,基于业务边界进⾏服务微化拆分,各个服务独⽴部署运⾏。

远程调用

在分布式系统中,各个服务可能处于不同主机,但是服务之间不可避免的需要互相调⽤,我们称为
远程调⽤。 SpringCloud 中使⽤ HTTP+JSON 的⽅式完成远程调⽤

负载均衡

分布式系统中,A 服务需要调⽤ B 服务,B 服务在多台机器中都存在,A 调⽤任意⼀个服务器均
可完成功能。 为了使每⼀个服务器都不要太忙或者太闲,我们可以负载均衡的调⽤每⼀个服务
器,提升⽹站的健壮性。

常⻅的负载均衡算法:

轮询:为第⼀个请求选择健康池中的第⼀个后端服务器,然后按顺序往后依次选择,直到最后
⼀个,然后循环。
最⼩连接:优先选择连接数最少,也就是压⼒最⼩的后端服务器,在会话较⻓的情况下可以考
虑采取这种⽅式。
散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种⽅式可以⼀定程度上保
证特定⽤户能连接到相同的服务器。如果你的应⽤需要处理状态⽽要求⽤户能连接到和之前相
同的服务器,可以考虑采取这种⽅式。

服务注册/发现---注册中⼼

A 服务调⽤ B 服务,A 服务并不知道 B 服务当前在哪⼏台服务器有,哪些正常的,哪些服务已经
下线。解决这个问题可以引⼊注册中⼼;如果某些服务下线,我们其他⼈可以实时的感知到其他服务的状态,从⽽避免调⽤不可⽤的服务

配置中⼼

每⼀个服务最终都有⼤量的配置,并且每个服务都可能部署在多台机器上。我们经常需要变更配
置,我们可以让每个服务在配置中⼼获取⾃⼰的配置。

服务熔断&服务降级

在微服务架构中,微服务之间通过⽹络进⾏通信,存在相互依赖,当其中⼀个服务不可⽤时,请求积压有可 能会造成雪崩效应。要防⽌这样的情况,必须要有容错机制来保护服务。

服务熔断

设置服务的超时,当被调⽤的服务经常失败到达某个阈值,我们可以开启断路保护机制,后来的请
求不再去调⽤这个服务。本地直接返回默认的数据

服务降级

在运维期间,当系统处于⾼峰期,系统资源紧张,我们可以让⾮核⼼业务降级运⾏。降级:某些服
务不处理,或者简单处理【抛异常、返回 NULL、调⽤ Mock 数据、调⽤ Fallback 处理逻辑】

API ⽹关

在微服务架构中,API Gateway 作为整体架构的重要组件,它 抽象了微服务中都需要的公共功
,同时提供了客户端负载均衡,服务⾃动熔断,灰度发布,统⼀认证,限流流控,⽇志统计等丰
富的功能,帮助我们解决很多 API 管理难题。

接下来我们讲解以下核心内容

Nacos

Nacos 是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

安装

Nacos 的安装非常简单,推荐使用 standalone 模式进行本地开发和测试。

  1. 准备环境:确保您的机器已安装 JDK 17+ 及 Maven 3.2.x+。

  2. 下载安装包 :从 Nacos GitHub Release 页面下载最新的稳定版 zip 或 tar 包。

  3. 解压并启动

    bash 复制代码
        # 解压
        unzip nacos-server-$version.zip
        cd nacos-server-$version
        
        # Linux/Mac
        sh startup.sh -m standalone
        
        # Windows 在bin目录下
        cmd startup.cmd -m standalone
        
        #docker安装启动
        docker run --name nacos-standalone
     -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:latest
  4. 访问控制台 :启动成功后,打开浏览器访问 http://localhost:8848/nacos。默认的用户名和密码都是 nacos

注册中心

注册中心是微服务架构的"通讯录",负责管理所有服务的实例信息。

服务注册

服务注册就是将服务的信息(如服务名、IP、端口)注册到 Nacos。

  1. 创建服务提供者 :创建一个 Spring Boot 项目,例如 service-provider

  2. 添加依赖 :在 pom.xml 中引入 nacos-discovery 依赖。

    XML 复制代码
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
  3. 配置文件 :在 application.yml 中配置 Nacos 服务地址。

    bash 复制代码
        server:
          port: 8081 
        spring:
          application:
            name: service-provider # 服务名称,重要
          cloud:
            nacos:
              discovery:
                server-addr: localhost:8848 # nacos地址
        
  4. 开启服务注册 :在主启动类上添加 @EnableDiscoveryClient 注解(新版本中可省略)。

启动项目后,在 Nacos 控制台的"服务管理 -> 服务列表"中,就能看到名为 service-provider 的服务了。

服务发现

服务发现是指消费者从 Nacos 获取到服务提供者的实例列表,从而进行调用。

  • 创建服务消费者 :创建另一个 Spring Boot 项目,例如 service-consumer
  • 添加依赖和配置 :与服务提供者类似,添加 nacos-discovery 依赖,并配置 application.yml
bash 复制代码
    server:
      port: 8080 # 服务消费者端口
    spring:
      application:
        name: service-consumer
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    
编写微服务API
java 复制代码
@RestController
public class EchoController {
    
    @GetMapping("/echo/{string}")
    public String echo(@PathVariable String string) {
        return "Hello Nacos Discovery: " + string;
    }
}
DiscoveryClient
java 复制代码
@Autowired
DiscoveryClient discoveryClient;
@Test
void discoveryClientTest(){
    for (String service : discoveryClient.getServices()) {
    System.out.println("service = " + service);
    //获取ip+port
    List<ServiceInstance> instances = discoveryClient.getInstances(ser
    vice);
    for (ServiceInstance instance : instances) {
     System.out.println("ip:"+instance.getHost()+";"+"port = " +
     instance.getPort());
   }
  }
}
NacosServiceDiscovery
java 复制代码
@Autowired
NacosServiceDiscovery nacosServiceDiscovery;
@Test
void nacosServiceDiscoveryTest() throws NacosException {
   for (String service : nacosServiceDiscovery.getServices()) {
      System.out.println("service = " + service);
      List<ServiceInstance> instances = nacosServiceDiscovery.getInstanc
es(service);
      for (ServiceInstance instance : instances) {
           System.out.println("ip:"+instance.getHost()+";"+"port = " +
            instance.getPort());
       }
   }
}
远程调用基本实现
  1. 配置 RestTemplate

    java 复制代码
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
        
  2. 编写调用逻辑

    java 复制代码
        @RestController
        public class ConsumerController {
        
            @Autowired
            private RestTemplate restTemplate;
        
            @GetMapping("/test")
            public String test() {
                // 硬编码URL,存在单点问题,无法负载均衡
                String url = "http://localhost:8081/echo/2025";
                return restTemplate.getForObject(url, String.class);
            }
        }
        
负载均衡API测试与@LoadBalanced注解
  1. 改造 RestTemplate :只需在 Bean 上添加一个注解。

    java 复制代码
        @Bean
        @LoadBalanced // 关键注解
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
        
  2. 改造调用逻辑 :将 IP 和端口替换为服务名。

    java 复制代码
        @GetMapping("/test")
        public String test() {
            // 使用服务名进行调用,Ribbon(或Spring Cloud LoadBalancer)会自动进行负载均衡
            String url = "http://service-provider/echo/2025";
            return restTemplate.getForObject(url, String.class);
        }
        

为了验证负载均衡,我们可以启动多个 service-provider 实例。

在 Nacos 控制台,你会看到 service-provider 下有两个实例。此时,多次访问 http://localhost:8080/test,你会发现请求被轮询分发到了 8081 和 8082 两个端口上,证明负载均衡生效。

经典面试题
  1. Nacos、Eureka、Consul 的区别是什么?

    • 一致性协议:Nacos 支持 AP(默认)和 CP 模式切换;Eureka 是 AP 模式;Consul 是 CP 模式。
    • 健康检查:Nacos/Consul 支持主动检查(TCP/HTTP)和客户端心跳;Eureka 仅支持客户端心跳。
    • 功能集成:Nacos 集成了服务发现和配置中心;Eureka 仅负责服务发现;Consul 功能丰富,包括服务网格支持。
    • 易用性:Nacos 提供了非常友好的控制台和中文文档,对国内开发者更友好。
  2. Nacos 如何保证服务的可用性(AP模式)?

    Nacos 默认采用 AP 模式。每个服务实例会定期向 Nacos Server 发送心跳,证明自己"存活"。Nacos Server 会将实例信息存储在内存中,并定时将数据同步给其他节点。如果一个实例在规定时间内未发送心跳,Nacos 不会立即将其剔除,而是标记为不健康,等待一段时间后仍未恢复才剔除。这种设计优先保证了服务的可用性(即使部分节点宕机,注册中心依然可用),牺牲了数据的强一致性。

配置中心

Nacos 的另一大核心功能是作为配置中心,实现配置的统一管理和动态刷新。

基本用法

在项目中引入 nacos-config 依赖

XML 复制代码
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

创建 bootstrap.yml :配置中心的配置需要优先于 application.yml 加载,因此必须放在 bootstrap.yml(或 bootstrap.properties)中。

bash 复制代码
# 指定配置中⼼地址
spring.cloud.nacos.server-addr=localhost:8848
spring.config.import=nacos:service-order.properties    spring:
      application:
        name: service-consumer # 必须与 Nacos Data Id 中的前缀匹配
      cloud:
        nacos:
          config:
            server-addr: localhost:8848
            file-extension: yaml # 配置文件的后缀
            # group: DEFAULT_GROUP # 配置分组,默认为 DEFAULT_GROUP
    

在 Nacos 控制台添加配置

  • 进入"配置管理 -> 配置列表"。

  • 点击"+"新增配置。

  • Data ID : service-consumer.yaml (规则:${spring.application.name}.${file-extension})

  • Group : DEFAULT_GROUP

  • 配置格式 : YAML

    bash 复制代码
            user:
              name: "Nacos-User"
              age: 25
            
  • 在代码中使用配置

    java 复制代码
        @RestController
        @RefreshScope // 动态刷新的关键注解
        public class ConfigController {
        
            @Value("${user.name}")
            private String userName;
        
            @Value("${user.age}")
            private int userAge;
        
            @GetMapping("/config")
            public String getConfig() {
                return "User Name: " + userName + ", Age: " + userAge;
            }
        }
        
动态刷新

这是 Nacos 配置中心最强大的功能之一。

  • 在上面的代码中,我们添加了 @RefreshScope 注解。
  • 现在,去 Nacos 控制台修改 user.age 的值为 30,然后点击"发布"。
  • 无需重启应用,再次访问 http://localhost:8080/config,你会发现返回的年龄已经变成了 30
配置监听

除了 @RefreshScope 的自动刷新,你还可以通过编程式的方式监听配置变更。

java 复制代码
@Component
public class NacosConfigListener {

    @NacosConfigListener(dataId = "service-consumer.yaml")
    public void onMessage(String newContent) {
        System.out.println("配置发生变更!新内容为:" + newContent);
        // 在这里可以执行更复杂的逻辑,比如更新缓存、通知其他组件等
    }
}
java 复制代码
//⽆需 @RefreshScope,⾃动绑定配置,动态更新
@Component
@ConfigurationProperties(prefix = "order") //配置批量绑定在nacos下,可以⽆需@R
efreshScope就能实现⾃动刷新
@Data
public class OrderProperties {
   String timeout;
   String autoConfirm;
   String dbUrl;
}
经典面试题
  1. Nacos 配置中心是如何实现动态刷新的?

    Nacos 客户端在启动时会从 Nacos Server 拉取一次全量配置,并开启一个长轮询任务。这个任务会向服务端发起一个请求,如果服务端的配置在 30 秒内没有变化,请求会阻塞直到超时。一旦配置发生变化,服务端会立即响应,客户端收到响应后,会再次拉取最新的配置,然后刷新 Spring 上下文中的 Bean。这是一种 Client Pull + Server Push 结合的优化模式,既保证了实时性,又降低了服务端压力。

  2. 如果 Nacos Server 宕机,客户端应用还能读取到配置吗?
    。Nacos 客户端在获取到配置后,会在本地生成一个快照文件。当与 Nacos Server 的连接断开时,客户端会从本地快照中加载配置,保证了应用的容错能力。

数据隔离

在实际开发中,我们通常需要区分开发、测试、生产等不同环境的配置。Nacos 提供了 Namespace 来实现数据隔离。

Namespace区分多环境

Nacos 通过 Namespace(命名空间)Group(组) 实现配置隔离:

  • Namespace :用于区分不同环境(如 dev、test、prod)。
  • Group :用于区分不同微服务或业务模块,⼀般可以⽤微服务的名字作为⾃⼰的组。
  • Data ID :具体配置文件名,完整写法: 名字.后缀 如: common.properties,支持多配置文件管理。

例如:

  • 开发环境:namespace=dev, group=USER_GROUP, data-id=user-service.yaml
  • 生产环境:namespace=prod, group=USER_GROUP, data-id=user-service.yaml
bash 复制代码
spring:
  cloud:
    nacos:
      discovery:
        namespace: ${spring.profiles.active:dev}
      config:
        namespace: ${spring.profiles.active:dev}

步骤:

  1. 创建 Namespace :在 Nacos 控制台的"命名空间"页面,新建一个命名空间,例如 dev,系统会自动生成一个 ID(如 a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6)。

  2. 在 dev Namespace 下创建配置 :在新增配置时,选择刚刚创建的 dev 命名空间。

  3. 修改客户端配置 :在 bootstrap.yml 中指定要使用的 Namespace ID。

    bash 复制代码
        spring:
          cloud:
            nacos:
              config:
                server-addr: localhost:8848
                file-extension: yaml
                namespace: a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6 # 指定命名空间ID
        
动态切换环境

我们可以在启动应用时,通过 JVM 参数动态指定 Namespace,从而实现一套代码、多环境部署。

bash 复制代码
java -jar -Dspring.cloud.nacos.config.namespace=dev-id your-app.jar
java -jar -Dspring.cloud.nacos.config.namespace=prod-id your-app.jar

nacos总结

OpenFeign

OpenFeign是一个声明式的Web服务客户端,它使得编写HTTP客户端变得更加简单。通过简单的接口定义和注解配置,就可以完成对远程服务的调用。

核心优势:

  • 声明式:通过接口和注解定义调用,无需手动拼接 URL 和处理参数。
  • 可插拔:支持多种编码器(如 Jackson)、解码器和拦截器。
  • 集成 Ribbon/LoadBalancer:原生集成了客户端负载均衡,只需提供服务名即可。

远程调用

声明式实现

  • 第一步:添加依赖
XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 第二步:启用 Feign 客户端

在 Spring Boot 启动类上添加 @EnableFeignClients 注解:

java 复制代码
@SpringBootApplication
@EnableFeignClients // 开启 Feign 客户端功能
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
  • 第三步:创建并使用 Feign 客户端
  1. 定义一个接口 ,使用 @FeignClient 注解指定要调用的服务名称。

    java 复制代码
    // UserClient.java
    @FeignClient(name = "user-service") // name 指向服务注册中心的服务名
    public interface UserClient {
    
        // 定义一个 GET 请求,路径为 /users/{id}
        @GetMapping("/users/{id}")
        User getUserById(@PathVariable("id") Long id);
    
        // 定义一个 POST 请求
        @PostMapping("/users")
        User createUser(@RequestBody User user);
    }
  1. 在业务代码中注入并使用 ,就像调用本地方法一样简单

    java 复制代码
    // OrderService.java
    @Service
    public class OrderService {
    
        @Autowired
        private UserClient userClient; // 注入 Feign 客户端
    
        public Order createOrder(Long userId, String productName) {
            // 1. 通过 Feign 远程调用用户服务,获取用户信息
            User user = userClient.getUserById(userId);
            if (user == null) {
                throw new RuntimeException("用户不存在");
            }
    
            // 2. 创建订单逻辑...
            Order order = new Order();
            order.setUserId(userId);
            order.setProductName(productName);
            // ... 其他逻辑
    
            System.out.println("为用户 " + user.getName() + " 创建订单成功!");
            return order;
        }
    }

第三方API

OpenFeign 不仅能调用微服务集群内的服务,也能轻松调用任何外部的第三方 REST API。这时,我们只需要使用 url 属性代替 name

java 复制代码
@FeignClient(name = "weather-api", url = "https://api.openweathermap.org/data/2.5") // 直接指定 URL
public interface WeatherClient {

    @GetMapping("/weather")
    String getWeather(@RequestParam("q") String city, @RequestParam("appid") String apiKey);
}

进阶配置

生产环境中,网络波动、服务延迟是常态。OpenFeign 提供了丰富的配置来增强调用的健壮性。

超时配置

为了避免长时间等待导致服务雪崩,配置超时至关重要。OpenFeign 有两个超时设置:

  • connectTimeout:连接超时,指建立 TCP 连接的时间。
  • readTimeout:读取超时,指连接建立后,等待服务器返回数据的时间。

application.yml 中配置:

bash 复制代码
feign:
  client:
    config:
      # 全局默认配置
      default:
        connectTimeout: 5000 # 5秒
        readTimeout: 10000   # 10秒
      # 针对特定服务的配置(优先级更高)
      user-service:
        connectTimeout: 3000
        readTimeout: 6000
重试机制

对于瞬时的网络抖动,自动重试可以有效提高成功率。OpenFeign 默认是关闭重试的,我们可以自定义一个 Retryer Bean 来开启它。

java 复制代码
@Bean
public Retryer feignRetryer() {
    // 初始间隔100ms,最大间隔1s,最大重试次数3次(总共会尝试4次)
    return new Retryer.Default(100, SECONDS.toMillis(1), 3);
}
拦截器

拦截器可以在请求发送前执行通用逻辑,最常见的场景就是统一添加认证头(如 JWT Token)。

  1. 实现 RequestInterceptor 接口

    java 复制代码
    public class AuthRequestInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate template) {
            // 从上下文或其他地方获取 Token
            String token = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
            
            // 为所有请求添加 Authorization 头
            template.header("Authorization", token);
        }
    }
  2. 注册拦截器

    java 复制代码
    @Bean
    public AuthRequestInterceptor authRequestInterceptor() {
        return new AuthRequestInterceptor();
    }

    这样,所有通过 Feign 发出的请求都会自动带上 Authorization 头。

Fallback兜底回调

当服务提供者不可用或调用失败时,为了不让整个流程崩溃,我们需要一个兜底方案,这就是服务降级。OpenFeign 提供了两种方式实现 Fallback。

方式一:fallback 属性(简单但功能有限)
  1. 创建一个 Fallback 类 ,实现 Feign 客户端接口。

    java 复制代码
    @Component // 注意:需要被 Spring 容器管理
    public class UserClientFallback implements UserClient {
        @Override
        public User getUserById(Long id) {
            // 返回一个兜底的默认用户或空对象
            User defaultUser = new User();
            defaultUser.setId(-1L);
            defaultUser.setName("默认用户");
            return defaultUser;
        }
    
        @Override
        public User createUser(User user) {
            return null;
        }
    }
  2. @FeignClient 中指定

    java 复制代码
    @FeignClient(name = "user-service", fallback = UserClientFallback.class)
    public interface UserClient {
        // ...
    }

    缺点:这种方式无法获取到具体的异常信息。

方式二:fallbackFactory 属性(推荐,功能强大)

FallbackFactory 允许你创建一个工厂类,在 Fallback 逻辑中可以捕获到导致调用失败的异常。

  1. 创建一个 FallbackFactory

    java 复制代码
    @Component
    public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    
        @Override
        public UserClient create(Throwable cause) {
            return new UserClient() {
                @Override
                public User getUserById(Long id) {
                    // 在这里可以记录详细的错误日志
                    System.err.println("调用 user-service 失败,原因: " + cause.getMessage());
                    // 返回兜底数据
                    User defaultUser = new User();
                    defaultUser.setId(-1L);
                    defaultUser.setName("服务降级-默认用户");
                    return defaultUser;
                }
                // ... 其他方法实现
            };
        }
    }
  2. @FeignClient 中指定

    java 复制代码
    @FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
    public interface UserClient {
        // ...
    }

远程调用小技巧&面试题

日志

OpenFeign 支持以下几种日志级别:

日志级别 说明
NONE 不记录任何日志(默认)
BASIC 仅记录请求方法、URL、响应状态码和执行时间
HEADERS 记录 BASIC 信息+请求和响应头信息
FULL 记录请求和响应的 headers、body 和元数据
性能优化
  • 连接池 :Feign 默认使用 JDK 的 HttpURLConnection,性能不高。可以替换为 Apache HttpClientOkHttp,它们支持连接池,性能更优。

  • GZIP 压缩 :开启请求和响应的 GZIP 压缩,可以减少网络传输数据量。

    bash 复制代码
    feign:
      compression:
        request:
          enabled: true
        response:
          enabled: true
面试题1:Feign 的底层实现原理是什么?

答: Feign 的核心是动态代理 。当你注入一个 @FeignClient 标记的接口时,Feign 会为这个接口创建一个动态代理对象(基于 JDK 的 Proxy)。当你调用接口方法时,这个代理对象会拦截所有调用。它会将方法名、参数、注解(如 @GetMapping)等信息封装成一个 RequestTemplate,然后经过编码器、拦截器等处理,最终委托给底层的 HTTP 客户端(如 HttpURLConnection)去发起真正的 HTTP 请求。收到响应后,再由解码器将响应数据反序列化成接口方法的返回值

面试题2:@FeignClient 中的 nameurl 属性有什么区别?

答:

  • name:用于指定服务名 。当使用 name 时,Feign 会结合服务发现组件(如 Nacos、Eureka)和客户端负载均衡器(如 Spring Cloud LoadBalancer)来解析该服务的多个实例地址,并从中选择一个进行调用。这是微服务内部调用的标准用法。
  • url:用于指定一个具体的、固定的 URL 。当使用 url 时,Feign 会直接向这个地址发起请求,不会 经过服务发现和负载均衡。这通常用于调用外部的、固定的第三方 API
面试题3:Ribbon/LoadBalancer 和 Feign 是什么关系?

答: 它们是协作关系,职责不同。

  • Feign :是一个声明式的 HTTP 客户端,负责将 Java 接口调用翻译成 HTTP 请求。它解决了"如何优雅地发起调用"的问题。
  • Ribbon/LoadBalancer :是一个客户端负载均衡器(负载均衡发生在客户端) ,负责从服务实例列表中选择一个具体的实例。它解决了"调用哪一个实例"的问题。

    实际开发通常混用两种模式

在 Spring Cloud 中,Feign 集成了 LoadBalancer。当 Feign 使用 name 属性时,它会自动委托 LoadBalancer 去获取服务实例列表并执行负载均衡算法,然后将选中的实例 IP 和端口交给 Feign 的底层 HTTP 客户端完成请求。

Sentinel

一个服务的故障或延迟,可能会像多米诺骨牌一样,引发整个系统的连锁反应,导致"雪崩效应"。为了保障系统的稳定性和高可用性,流量控制、服务熔断和降级显得至关重要

工作原理
  1. 资源化 :Sentinel 会将需要保护的逻辑,例如一个方法、一个接口,定义为一个资源。只要通过 Sentinel API 对这个资源进行访问,就可以被 Sentinel 监控和保护。

  2. 插槽链:这是 Sentinel 的核心设计。当一个请求访问一个资源时,它会经过一系列的插槽,每个插槽都负责一项特定的功能,如限流、降级、系统保护等。这些插槽串联起来,形成了一条责任链。

    bash 复制代码
        [请求] -> NodeSelectorSlot -> ClusterBuilderSlot -> StatisticSlot -> [其他Slot] -> FlowSlot -> DegradeSlot -> SystemSlot -> [目标资源]
        
    复制代码
    *   `NodeSelectorSlot` & `ClusterBuilderSlot`:负责构建资源的统计节点(StatisticNode),用于后续的统计。
    *   `StatisticSlot`:核心统计插槽,用于记录资源的请求响应时间、成功数、失败数等指标,是限流和熔断的基础。
    *   `FlowSlot`:**流控插槽**。它根据预设的流控规则,检查当前请求是否应该被限流。如果 QPS 或并发线程数超过阈值,就会抛出 `BlockException`。
    *   `DegradeSlot`:**熔断降级插槽**。它根据熔断规则,检查资源的健康状态。如果满足熔断条件(如错误率过高),则会触发熔断,后续请求将在一段时间内直接被拒绝。
    *   `SystemSlot`:**系统保护插槽**。它从整体维度(如 CPU 使用率、系统负载等)来保护应用,防止系统被压垮。

基础场景

Sentinel 的应用场景非常广泛,主要可以归纳为以下三类:

  • 流量控制:在流量洪峰来临之际,通过限制请求的通过速率,防止系统被瞬间的高流量冲垮。例如,将某个 API 的 QPS 限制为 100。
  • 熔断降级:当某个依赖的服务出现不稳定(响应时间变长或错误率升高)时,暂时切断对它的调用,避免请求堆积,从而保护主服务不受影响。这是一种"丢卒保帅"的策略。
  • 系统负载保护:当系统的整体负载(如 CPU、内存)过高时,对进入系统的所有请求进行限流,保证系统不会因过载而崩溃。

异常处理

当 Sentinel 的规则被触发时(如流控或熔断),默认会抛出 BlockException。如果我们不处理这个异常,用户可能会看到一个不友好的错误页面。因此,优雅地处理异常是 Sentinel 应用中至关重要的一环。

1. Web 接口

对于 Spring MVC 或 WebFlux 应用,我们可以使用全局异常处理器来捕获 BlockException

java 复制代码
@ControllerAdvice
public class GlobalBlockExceptionHandler {

    @ResponseBody
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    @ExceptionHandler(BlockException.class)
    public Response handleBlockException(BlockException e) {
        return Response.error(429, "系统繁忙,请稍后再试!"); // 自定义响应
    }
}
2. @SentinelResource

@SentinelResource 是 Sentinel 提供的核心注解,用于更细粒度地定义资源和处理异常。它提供了两个关键属性:blockHandlerfallback

  • blockHandler:专门处理Sentinel 规则 (流控、熔断)被触发时抛出的 BlockException
  • fallback:处理代码本身 抛出的所有异常(如 RuntimeExceptionIOException 等)。
java 复制代码
@Service
public class ProductService {

    // value: 资源名称
    // blockHandler: 处理BlockException的方法
    // fallback: 处理Java异常的方法
    @SentinelResource(value = "getProduct", blockHandler = "handleBlock", fallback = "handleFallback")
    public String getProduct(String id) {
        if ("1".equals(id)) {
            throw new RuntimeException("商品不存在!");
        }
        // ... 正常业务逻辑
        return "Product-" + id;
    }

    // BlockException处理函数,参数需与原方法一致,最后多一个BlockException参数
    public String handleBlock(String id, BlockException ex) {
        return "系统繁忙,请稍后再试 (BlockHandler)";
    }

    // Fallback处理函数,参数需与原方法一致,可以多一个Throwable参数来接收异常
    public String handleFallback(String id, Throwable ex) {
        return "服务异常,商品查询失败 (Fallback)";
    }
}
3. OpenFeign

当 Sentinel 与 OpenFeign 整合时,处理方式更为优雅。我们只需在 Feign Client 的接口上启用 Sentinel,然后实现一个 fallback 类即可。前文有介绍

流控规则

流控规则是 Sentinel 最常用的功能,用于限制访问资源的流量。

阈值类型
  1. 直接:对资源本身进行限流。当资源的 QPS 或线程数超过阈值时,直接触发流控。
  2. 链路 :只针对从某个特定入口 进入的流量进行限流。例如,资源 C 被入口 AB 调用,我们可以只限制从 AC 的流量,而不影响 BC 的调用。这需要使用 @SentinelResource 注解的 entryType 属性配合。
  3. 关联 :当关联资源 达到阈值时,就限制当前资源 。例如,read_dbwrite_db 两个资源。当 write_db 的访问量过大时,可以限制 read_db 的流量,优先保证写入操作,防止数据库被读请求拖垮。
流控效果
  1. 直接 :默认效果。一旦达到阈值,直接拒绝后续请求,抛出 BlockException
  2. Warm Up(预热) :系统启动后,流量阈值会从一个较小的值(coldFactor 默认为 3,即阈值的 1/3)开始,经过预热时长后,逐渐增长到设定的阈值。这适用于需要预热才能达到最佳性能的系统,如缓存初始化、JIT 编译等。
  3. 匀速排队:让请求以均匀的速度通过,对应的是"漏桶算法"。当请求超过阈值时,会排队等待。如果等待超时,则请求被拒绝。这种方式适用于处理突发流量,将请求削峰填谷,保证系统以一个稳定的速率处理请求。

熔断规则

熔断是防止故障扩散的核心手段。

断路器工作原理

Sentinel 的熔断器遵循经典的三种状态:

  1. 闭合:正常状态,允许所有请求通过,并统计调用状态(成功、失败、耗时)。
  2. 打开:当满足熔断条件时,熔断器打开。在熔断时长内,所有到该资源的请求都会被直接拒绝,触发降级逻辑。
  3. 半开 :熔断时长结束后,熔断器进入半开状态。它会放行一个 请求,如果该请求成功,则熔断器闭合 ;如果失败,则熔断器重新打开,并重置熔断时长。
熔断策略
  1. 慢调用比例 :当资源的响应时间大于最大 RT(maxRt)时,被记为慢调用。如果在统计时长内,请求数大于最小请求数,且慢调用比例大于阈值,则触发熔断。
  2. 异常比例:在统计时长内,如果请求数大于最小请求数,且异常(抛出异常)的比例大于阈值,则触发熔断。
  3. 异常数:在统计时长内,如果异常的数量大于阈值,则触发熔断。

热点规则

热点规则是一种特殊的流控规则,它允许我们更精确地对参数化的流量进行控制。

热点参数限流

有时,某个参数的值(如某个商品 ID、用户 ID)可能会成为访问热点,导致系统负载不均。热点规则可以针对这些热点参数值进行单独的限流。

例如,我们可以对 getProduct(id) 方法设置规则:

  • 默认 QPS 阈值为 10。
  • 但当参数 id 的值为 hot_product_id 时,QPS 阈值限制为 1。

这样就能有效防止个别热点商品访问量过大而影响整个系统

java 复制代码
@SentinelResource(value = "getProduct", blockHandler = "handleBlock")
public String getProduct(@SentinelResource("id") String id) {
    // ...
}

补充fallback与blockHander兜底回调

这是对 @SentinelResource 异常处理的再次强调,因为它非常关键且容易混淆。

  • blockHandler管规则 。当 Sentinel 的流控、熔断、系统保护等规则生效时,由它来兜底。它的签名必须包含 BlockException 参数。
  • fallback管异常 。当你的业务代码本身抛出了任何类型的异常时(RuntimeExceptionError 等),由它来兜底。它的签名可以包含 Throwable 参数来获取具体异常。

最佳实践 :通常,我们会同时使用 blockHandlerfallback,实现双重保障。blockHandler 用于处理"被保护"的场景,fallback 用于处理"自身出错"的场景。

Sentinel总结

Sentinel 作为一款功能强大的流量防卫兵,为微服务架构提供了坚实的稳定性保障。

  • 核心优势

    • 实时监控:提供实时、强大的监控能力,可以秒级观察到应用的健康状况。
    • 丰富规则:支持多种流控、熔断、热点、系统保护规则,覆盖了绝大多数应用场景。
    • 易于扩展:基于插槽链的设计,使得自定义和扩展功能变得非常简单。
    • 无缝集成:与 Spring Cloud、Dubbo、gRPC 等主流框架无缝集成,降低了使用门槛。
  • 解决的问题

    • 通过流量控制,防止系统被流量冲垮。
    • 通过熔断降级,防止故障扩散,避免雪崩效应。
    • 通过系统负载保护,保证应用在高压下依然能稳定运行。

Gateway

API 网关已经成为了不可或缺的核心组件。它作为所有客户端请求的统一入口,承担着路由转发、安全校验、流量控制、日志监控等重要职责。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的第二代网关,凭借其非阻塞 I/O、强大的路由功能和丰富的扩展性,成为了众多开发者的首选。

网关功能

  • 路由: 核心功能,将外部请求根据特定规则(如路径、域名、请求头等)转发到正确的后端微服务。
  • 认证与安全: 统一处理身份认证(如 JWT 验证)、权限校验,防止未授权的请求访问后端服务。
  • 限流: 保护后端服务免受流量洪峰的冲击,防止系统被拖垮。
  • 重试与熔断: 当后端服务出现问题时,提供自动重试和熔断机制,提升系统的健壮性。
  • 日志与监控: 记录所有请求的详细信息,为问题排查和系统监控提供数据支持。
  • 请求/响应转换: 可以在请求转发前或响应返回前,对报文进行修改,例如添加/删除请求头。

创建网关

第一步:添加依赖

在你的 pom.xml 中添加 spring-cloud-starter-gateway 依赖。

XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

第二步:配置文件

application.yml 中进行基础配置。

bash 复制代码
server:
  port: 8080 # 网关服务端口

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      # 路由配置
      routes:
        - id: user_service  # 路由的唯一标识
          uri: http://localhost:8081 # 目标服务地址
          predicates: # 断言(条件)
            - Path=/user/** # 匹配路径
        - id: product_service
          uri: http://localhost:8082
          predicates:
            - Path=/product/**

此时访问 http://localhost:8080/user/info 会被转发到 http://localhost:8081/user/info

路由

路由是 Gateway 的核心构建块。一个完整的路由包含四个部分:

  1. ID: 路由的唯一标识符,区别于其他路由。
  2. URI: 目标服务的地址。可以是具体的 URL(http://example.com),也可以是服务发现中的服务名(lb://user-servicelb 代表 LoadBalancer)。
  3. Predicates(断言): 一组匹配条件。如果请求满足所有断言,则该路由被匹配。
  4. Filters(过滤器): 一组过滤器。在请求被转发前后执行,用于修改请求和响应。
规则配置

Gateway 的路由规则主要通过 application.yml 或 Java 配置类来定义。YAML 方式更直观、更常用。

bash 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - After=2023-10-12T17:30:00+08:00[Asia/Shanghai] # 在指定时间之后才匹配
          filters:
            - AddRequestHeader=X-Request-Foo, Bar # 添加请求头
工作原理
  1. 客户端请求 到达 Gateway。
  2. Gateway Handler Mapping 接收请求,根据一系列的 断言 判断该请求匹配哪个路由。
  3. 一旦匹配成功,请求被发送到 Gateway Web Handler
  4. Gateway Web Handler 将请求通过一个 过滤器链 。这个链由所有与该路由关联的 GatewayFilter 和全局的 GlobalFilter 组成。
  5. 过滤器链中的过滤器会按顺序执行,执行前置逻辑(如修改请求),然后发起代理请求。
  6. 代理请求返回后,过滤器链再反向执行后置逻辑(如修改响应)。
  7. 最终,处理完成的响应被返回给客户端。

整个流程基于 Spring WebFlux 的非阻塞、响应式模型,使用 Netty 作为服务器,能够以极少的线程处理高并发请求。

断言

断言是 Gateway 的"路由匹配器",它决定了请求是否应该被当前路由处理。Spring Cloud Gateway 内置了多种强大的断言工厂。

长短写法

许多断言支持两种写法:完整名称和简写。

长写法(完整名称):

复制代码
    - name: Path
      args:
        pattern: /user/**

短写法(快捷方式):

复制代码
    - Path=/user/**
Query 断言

Query 断言用于检查请求的查询参数。

  • 示例1:必须包含 name 参数

    复制代码
      predicates:
        - Query=name

    匹配 http://mygateway/user?name=zhangsan,不匹配 http://mygateway/user

  • 示例2:name 参数的值必须是正则表达式 zhang.*

    复制代码
      predicates:
        - Query=name, zhang.*

    匹配 http://mygateway/user?name=zhangsan,不匹配 http://mygateway/user?name=lisi

自定义断言工厂

当内置断言无法满足需求时,我们可以自定义。

步骤:

  1. 创建一个类,继承 AbstractRoutePredicateFactory<C>,其中 C 是配置类。
  2. 重写 apply 方法,实现断言逻辑。
  3. 将类注册为 Spring Bean(@Component)。

示例:自定义一个检查请求头 X-Source 是否为 mobile 的断言。

java 复制代码
// 1. 创建配置类(可选,但推荐)
public static class Config {
    // 可以在这里定义配置属性
}

// 2. 创建断言工厂类
@Component
public class MobileSourceRoutePredicateFactory extends AbstractRoutePredicateFactory<MobileSourceRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // 3. 实现断言逻辑
            String source = exchange.getRequest().getHeaders().getFirst("X-Source");
            return "mobile".equalsIgnoreCase(source);
        };
    }
}

application.yml 中使用:

bash 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: mobile_route
          uri: http://localhost:8081
          predicates:
            - MobileSource # 使用自定义断言的名称(去掉RoutePredicateFactory后缀)

过滤器

过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。它分为两种类型:GatewayFilter (路由过滤器)和 GlobalFilter(全局过滤器)。

基本使用

GatewayFilter 应用在单个路由上。

示例:AddRequestHeader 过滤器

复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: add_header_route
          uri: http://localhost:8081
          predicates:
            - Path=/api/**
          filters:
            - AddRequestHeader=X-Request-Red, Blue 
默认 filter

Gateway 提供了大量内置的过滤器工厂,例如:

  • AddRequestParameter: 添加请求参数。
  • StripPrefix: 转发前去掉路径的前 N 段。例如 StripPrefix=1,请求 /api/user/info 会被转发为 /user/info
  • SetPath: 修改请求路径。
  • Retry: 自动重试。
GlobalFilter

GlobalFilter 不需要在路由上配置,它会自动应用于所有路由。它更适合实现全局性的功能,如认证、日志、限流等。

示例:一个简单的日志 GlobalFilter

java 复制代码
@Component
@Slf4j
public class SimpleLogGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("请求路径: {}", exchange.getRequest().getPath().value());
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 过滤器的执行顺序,数值越小,优先级越高
        return -1;
    }
}
自定义过滤器工厂

与自定义断言类似,我们也可以创建自定义的 GatewayFilter。

步骤:

  1. 创建一个类,继承 AbstractGatewayFilterFactory<C>
  2. 重写 apply 方法。
  3. 注册为 Spring Bean。

示例:自定义一个打印特定日志的过滤器工厂。

java 复制代码
@Component
public class PrintLogGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintLogGatewayFilterFactory.Config> {

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre Log: Request Path = {}", exchange.getRequest().getPath());
            }
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("Post Log: Response Status = {}", exchange.getResponse().getStatusCode());
                }
            }));
        };
    }

    // 配置类,用于接收yml中的参数
    public static class Config {
        private boolean preLogger;
        private boolean postLogger;
        // getters and setters...
    }
}

application.yml 中使用:

bash 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: custom_filter_route
          uri: http://localhost:8081
          predicates:
            - Path=/**
          filters:
            - name: PrintLog
              args:
                preLogger: true
                postLogger: true

全局跨域

在前后端分离的项目中,跨域(CORS)是常见问题。Spring Cloud Gateway 提供了非常便捷的全局跨域配置方案。

application.yml 中配置:

bash 复制代码
spring:
  cloud:
    gateway:
      # 全局跨域配置
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有路径
            allowedOriginPatterns: "*" # 允许所有来源,生产环境建议指定具体域名
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
            allowedHeaders: "*"
            allowCredentials: true # 允许携带 Cookie
            maxAge: 3600 # 预检请求的有效期

这段配置会为所有经过网关的请求添加 CORS 相关的响应头,从而解决跨域问题。

Gateway总结

Spring Cloud Gateway 是一个功能强大且高度可扩展的 API 网关。它的核心优势在于:

  • 性能卓越: 基于 WebFlux 和 Netty,非阻塞 I/O 模型使其能够轻松应对高并发场景。
  • 功能丰富: 内置了路由、断言、过滤器等一套完整的机制,满足绝大多数网关需求。
  • 易于集成: 与 Spring Cloud 生态(如服务发现、熔断器)无缝集成。
  • 高度可定制: 支持自定义断言和过滤器,可以灵活地扩展其功能

Seata

将单体应用拆分为一系列独立的服务。这带来了灵活性和可扩展性,但也引入了一个棘手的难题:分布式事务。当一个业务操作需要跨越多个服务、多个数据库时,如何保证数据的一致性?

阿里巴巴开源的 Seata (Simple Extensible Autonomous Transaction Architecture) 正是为了解决这一痛点而生。它提供了一套高性能、易于使用的分布式事务解决方案。

环境搭建

我们先来搭建 Seata 的运行环境。Seata 的核心架构包含三个角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与 TC 通信以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

我们的搭建步骤也将围绕这三者展开。

第一步:搭建 TC (Seata Server)
  1. 下载 Seata Server

    从 Seata 官方 GitHub 仓库下载最新版的 Seata Server 压缩包并解压。

  2. 配置存储模式

    Seata Server 需要存储全局事务会话信息,支持文件(file)和数据库(db)。生产环境推荐使用 db

    修改 conf/file.conf 文件:

java 复制代码
    store {
      ## store mode: file、db、redis
      mode = "db"

      ## database store property
      db {
        datasource = "druid"
        dbType = "mysql"
        driverClassName = "com.mysql.cj.jdbc.Driver"
        url = "jdbc:mysql://127.0.0.1:3306/seata_server?rewriteBatchedStatements=true"
        user = "mysql"
        password = "mysql"
        minConn = 5
        maxConn = 30
        globalTable = "global_table"
        branchTable = "branch_table"
        lockTable = "lock_table"
        queryLimit = 100
        maxWait = 5000
      }
    }
  1. 初始化数据库

    在你的 MySQL 中创建一个名为 seata_server 的数据库,并运行 Seata 源码或脚本包中的 script/server/db/mysql.sql 文件,创建所需的 global_tablebranch_tablelock_table 等表。

  2. 配置注册中心

    Seata Server 需要向注册中心注册自身,以便 TM 和 RM 能够发现它。这里我们以 Nacos 为例。

    修改 conf/registry.conf 文件:

java 复制代码
    registry {
      type = "nacos"

      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = ""
        cluster = "default"
      }
    }
  1. 启动 Seata Server
    运行 bin/seata-server.bat (Windows) 或 bin/seata-server.sh (Linux/Mac) 即可启动 TC。
第二步:整合 TM & RM (业务微服务)

假设我们有两个微服务:order-service(订单服务)和 storage-service(库存服务)。

  1. 添加依赖
    在两个服务的 pom.xml 中都添加 Seata 客户端依赖:

    XML 复制代码
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        
  1. 配置数据源代理

    Seata 通过代理数据源来实现分支事务的管理。确保你的 Spring Boot 应用中,数据源是自动配置的,Seata 会自动完成代理。

  2. 配置 Seata 客户端

    application.yml 中配置 Seata 客户端,连接到 TC 和 Nacos。

bash 复制代码
    seata:
      # 事务组,自定义即可
      tx-service-group: my_tx_group
      service:
        # 事务组与集群的映射关系
        vgroup-mapping:
          my_tx_group: default
        # Seata Server 所在的集群
        grouplist:
          default: 127.0.0.1:8091
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group: SEATA_GROUP
          namespace: ""
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group: SEATA_GROUP
          namespace: ""
bash 复制代码
tx-service-group 是关键,它定义了你的服务属于哪个事务组。这个配置需要和 Nacos 中的配置保持一致(如果使用 Nacos 配置中心的话)。

接口与事务测试

环境搭好后,我们通过一个经典的"下单扣库存"场景来测试。

本地事务测试

order-service 中,我们先不进行远程调用,只测试本地事务。

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    // 使用 @GlobalTransactional 开启分布式事务
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(String userId, String commodityCode, int orderCount) {
        // 本地创建订单
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(orderCount);
        orderMapper.insert(order);

        // 模拟异常,测试回滚
        if (commodityCode.equals("error")) {
            throw new RuntimeException("模拟业务异常");
        }
    }
}

调用此接口,当 commodityCode 为 "error" 时,订单插入操作会被回滚。

打通远程调用链路

现在,我们让 order-service 远程调用 storage-service 扣减库存。

  1. storage-service 中提供扣减库存接口
java 复制代码
    @RestController
    public class StorageController {
        @Autowired
        private StorageService storageService;

        @RequestMapping("/deduct")
        public Boolean deduct(@RequestParam String commodityCode, @RequestParam int count) {
            storageService.deduct(commodityCode, count);
            return true;
        }
    }
  1. order-service 中发起远程调用
java 复制代码
    @Service
    public class OrderServiceImpl implements OrderService {

        @Autowired
        private OrderMapper orderMapper;
        @Autowired
        private RestTemplate restTemplate; // 用于远程调用

        @Override
        @GlobalTransactional(name = "create-order-with-storage", rollbackFor = Exception.class)
        public void createOrder(String userId, String commodityCode, int orderCount) {
            // 1. 本地创建订单
            // ... (代码同上)

            // 2. 远程调用扣减库存
            String url = "http://storage-service/deduct?commodityCode=" + commodityCode + "&count=" + orderCount;
            restTemplate.getForObject(url, Boolean.class);

            // 3. 模拟异常,测试全局回滚
            if (commodityCode.equals("error")) {
                throw new RuntimeException("模拟业务异常,触发全局回滚");
            }
        }
    }

接口测试

使用 Postman 或其他工具调用 order-service 的创建订单接口。

  • 成功场景commodityCode 为正常值。你会发现 order-service 的订单表和 storage-service 的库存表都发生了预期的变化。
  • 失败场景commodityCode 为 "error"。你会发现,即使库存已经扣减,由于后续异常,订单创建失败,最终订单表和库存表的数据都会被回滚,保证了数据一致性。

架构原理

Seata 的核心思想是如何在不侵入业务逻辑的前提下,实现分布式事务。以最常用的 AT 模式为例:

  1. 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  2. 二阶段:异步化,快速提交。

整合 Seata 的关键在于 DataSourceProxy。Seata 会自动代理你的数据源,当你执行 SQL 时,它会:

  • 解析 SQL:获取 SQL 类型(UPDATE/INSERT/DELETE)、表名、条件等。
  • 查询前镜像:执行 SQL 前,先查询出被修改数据的原始状态。
  • 执行业务 SQL:执行你写的业务 SQL。
  • 查询后镜像:执行 SQL 后,再查询出被修改数据的新状态。
  • 插入回滚日志:将前后镜像信息组装成回滚日志(undo_log),与业务 SQL 在同一个本地事务中提交。

二阶协议提交流程

  1. 开启全局事务:TM 向 TC 申请开启一个全局事务,TC 生成一个全局事务 ID (XID)。
  2. 注册分支事务:TM 调用微服务 A,微服务 A 作为 RM,在执行业务 SQL 前,向 TC 注册一个分支事务,并绑定到 XID。
  3. 执行并记录 Undo Log:RM 执行业务 SQL,并如上所述生成 Undo Log,与业务数据一起提交。
  4. 报告分支状态:RM 向 TC 报告分支事务执行成功。
  5. 重复步骤 2-4:TM 调用微服务 B、C...,所有分支事务都执行并报告成功。
  6. 全局提交决议:TM 收到所有分支的成功响应后,向 TC 发起全局提交请求。
  7. 异步删除 Undo Log:TC 通知所有 RM 异步删除对应的 Undo Log。至此,事务完成。

如果任何一步发生失败,TM 会向 TC 发起全局回滚请求。

二阶提交可视化

四种事务模式

Seata 提供了四种事务模式,以适应不同的业务场景。

模式 一阶段动作 二阶段动作 一致性 侵入性 性能 适用场景
AT 自动生成Undo Log,提交本地事务 异步删除Undo Log 或 根据Undo Log回滚 最终一致 较高 大多数基于关系型数据库的分布式事务场景,简单易用。
TCC 调用业务Try接口(资源预留和检查) 调用业务ConfirmCancel接口 最终一致 高(需实现三个接口) 对性能要求高、需要资源预留、或非数据库资源的场景。
SAGA 调用正向服务 调用补偿服务 最终一致 高(需提供补偿方法) 业务流程长、涉及多个系统、需要补偿机制的复杂场景。
XA 执行XA prepare,锁定资源 执行XA commitrollback 强一致 无(依赖数据库) 对数据一致性要求极高,能接受性能开销的场景。

模式选择建议:

  • 首选 AT 模式:对于大多数业务,AT 模式提供了最好的易用性和性能平衡。
  • 性能敏感场景选 TCC:如金融领域的核心交易,需要快速响应和资源锁定。
  • 长流程选 SAGA:如旅行预订,涉及机票、酒店、租车等多个独立服务。
  • 强一致要求选 XA:如银行转账,不能容忍任何不一致。
相关推荐
寻寻觅觅☆13 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t14 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划14 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿14 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12315 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗15 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI15 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS15 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子15 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗15 小时前
初识C++
开发语言·c++