微服务基础
视频链接: Spring Cloud 快速通关
采用 JDK21 实现,更推荐使用 JDK17。使用 JDK17 时,需要额外调整 pom 文件配置与相关 API。
0.微服务入门
微服务就是把一个大项目拆成多个小服务,每个服务负责一块独立业务,例如订单服务、商品服务、支付服务、库存服务。每个服务可以单独开发、单独启动、单独部署,也可以有自己的数据库。微服务的好处是项目更容易扩展、维护和部署,某个服务压力大时可以单独加机器,不需要整体扩容。
微服务带来的核心问题是:服务变多以后,服务之间要互相调用,所以需要解决服务注册发现、远程调用、负载均衡、配置管理、限流熔断、网关路由、分布式事务等问题。Spring Cloud Alibaba 就是一套解决这些问题的微服务组件集合,常见组件包括 Nacos、OpenFeign、Sentinel、Gateway、Seata。
单体项目中,方法调用通常是本地调用,例如 orderService.createOrder();微服务项目中,订单服务和商品服务可能是两个独立进程,甚至部署在不同机器上,所以订单服务要通过 HTTP 请求调用商品服务接口,这就是远程调用。
-
单体架构:所有功能打包在一个应用中,部署简单,但扩展性与维护成本随业务增长而上升。

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

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

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

1. Nacos
1.1 简介与下载
Nacos 是 Dynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 是 Spring Cloud Alibaba 中常用的注册中心和配置中心。注册中心负责记录“有哪些服务、每个服务在哪个 IP 和端口”;配置中心负责把配置统一放到 Nacos 中管理,让服务启动时或运行时从 Nacos 读取配置。
Nacos 解决两个问题:第一,服务地址不能写死。商品服务可能部署多台机器,端口也可能变化,订单服务不应该固定写 localhost:8001;第二,配置不应该散落在每个项目里。数据库地址、开关配置、限流参数等可以放到配置中心统一管理。
Nacos(Dynamic Naming and Configuration Service):提供 动态服务发现、配置管理 与 服务管理 平台。
安装启动
- 下载:Nacos 安装包(如 2.4.3)
- 启动(单机):
startup.cmd -m standalone - 控制台:
http://localhost:8848/nacos

1.2 服务注册
服务注册就是微服务启动后,把自己的服务名、IP、端口等信息上报给 Nacos。例如商品服务启动后注册为 service-product,订单服务启动后注册为 service-order。Nacos 保存这些服务实例信息,其他服务需要调用时,就可以通过服务名找到可用地址。
Nacos 在这里充当 注册中心。每个微服务启动后,会把自己的服务名、IP、端口注册到 Nacos。例如商品服务启动后注册为 service-product,订单服务启动后注册为 service-order。注册中心保存的是“服务名和服务实例地址的映射关系”。
服务注册解决的问题是:调用方不需要写死目标服务的 IP 和端口。以前如果订单服务要调用商品服务,可能要写 http://localhost:8001/product/1;但微服务中商品服务可能有多个实例,端口可能变化,所以不能写死地址。注册到 Nacos 后,订单服务只需要知道服务名 service-product,具体访问哪个实例由服务发现和负载均衡完成。
基本使用步骤是:引入 spring-cloud-starter-alibaba-nacos-discovery 依赖,在配置文件中配置 Nacos 地址,然后启动服务。服务启动后会自动向 Nacos 注册。
spring:
cloud:
nacos:
# 配置 Nacos 地址
server-addr: 127.0.0.1:8848
spring.application.name 很重要,它就是当前服务注册到 Nacos 里的服务名。以后其他服务远程调用时,不直接写 IP,而是写这个服务名。
查看注册中心效果,访问 http://localhost:8848/nacos/
测试集群模式启动:单机情况下通过改变端口号模拟微服务集群,例如添加 Program arguments 信息为 --server.port=8001
1.3 服务发现
服务发现就是调用方根据服务名,从 Nacos 获取目标服务的实例列表。例如订单服务想调用商品服务,就先通过 service-product 找到商品服务的 IP 和端口。
服务发现的核心流程是:服务提供者启动后注册到 Nacos;服务消费者调用前根据服务名查询实例列表;如果有多个实例,负载均衡组件会从中选择一个实例;最终发起 HTTP 请求。
- 开启服务发现,在主启动类上添加
@EnableDiscoveryClient注解 - 测试两款 API 的服务发现功能:
DiscoveryClient和NacosServiceDiscovery。前者为 Spring 提供的服务发现标准接口,后者由 Nacos 提供。
1.4 远程调用
远程调用就是一个服务通过 HTTP 请求调用另一个服务的接口。例如订单服务需要商品信息,就请求商品服务的 /product/{id} 接口。它和普通浏览器请求接口本质一样,只是请求方从“浏览器”变成了“另一个后端服务”。
远程调用的关键不是会不会发 HTTP,而是目标地址怎么确定。如果写死 http://localhost:8001/product/1,服务扩容或换端口后就会失效;如果写 http://service-product/product/1,再配合注册中心和负载均衡,就能自动找到可用的商品服务实例。
远程调用基本流程:
订单服务调用商品服务时,不是直接写死商品服务地址,而是先通过注册中心找到商品服务的可用地址列表。流程是:商品服务启动后先注册到 Nacos;订单服务需要调用商品服务时,根据服务名向 Nacos 查询商品服务实例列表;Nacos 返回多个可用地址;订单服务通过负载均衡选择其中一个地址;然后真正发送 HTTP 请求到商品服务;商品服务处理后返回数据。
核心要点是:注册中心不处理业务请求,只负责保存和提供服务地址。真正处理 /product/{id} 请求的是商品服务本身。Nacos 的作用类似服务地址表,订单服务先查表,再发请求。
①手动拼 URL(DiscoveryClient) 典型思路:
DiscoveryClient.getInstances("service-product")拿到实例列表- 选一个实例拿到
host:port - 拼接 URL 后用
RestTemplate.getForObject发请求
要点:
getForObject常见 GET 方法:- 参数 1:URL
- 参数 2:返回体映射的目标类型(例如
Product.class)
private Product getProductFromRemote(Long productId) {
//1.获取到商品服务所在的所有机器IP+port
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance instance = instances.get(0);
//2.拼接请求地址(远程请求地址)
String url = "http://"+instance.getHost()+":"+instance.getPort()+"/product/"+productId;
log.info("远程请求地址:{}",url);
//3.发送请求
RestTemplate 是 Spring 提供的“HTTP 客户端工具”,用来在 Java 代码中发送 HTTP 请求,调用别人写好的接口,并把返回的 JSON/XML 自动转成 Java 对象。在微服务中,它用于服务之间的远程调用。
/* getForObject() 是 RestTemplate 中最常用的 GET 请求方法之一,有两个核心参数:
第一个参数 url:要请求的接口地址(比如 http://localhost:9000/product/1);
第二个参数 Product.class:指定返回数据要转换成的目标类型(这里是 Product 实体类)*/
/*Product.class: Java 的泛型擦除机制,让程序运行时无法自动推断出要转换成的具体类型,
必须通过 Product.class 明确告诉 RestTemplate 要把返回的 JSON 数据转换成哪个类的对象。*/
Product product = restTemplate.getForObject(url, Product.class);
return product;
}
② LoadBalancerClient
典型思路:
loadBalancerClient.choose("service-product")选择实例- 拼接 URL 后用
RestTemplate发请求
private Product getProductFromRemoteWithLoadBalance(Long productId) {
ServiceInstance choose = loadBalancerClient.choose("service-product");
String url = "http://" + choose.getHost() + ":" + choose.getPort() + "/product/" + productId;
log.info("远程请求地址:{}", url);
return restTemplate.getForObject(url, Product.class);
}
1.5 负载均衡
负载均衡就是当同一个服务有多个实例时,从多个实例中选一个来处理请求。例如商品服务启动了 8001、8002、8003 三个实例,订单服务每次调用时不应该永远访问 8001,而应该按一定规则分配到不同实例上。
使用
LoadBalancerClient实现
注入 LoadBalancerClient,调用其 choose() 方法,传入服务名,实现负载均衡。
使用
@LoadBalanced注解实现
在配置类中向 Spring 容器添加 RestTemplate 的 Bean,在 Bean 方法上添加 @LoadBalanced 注解,使用 RestTemplate 进行远程调用时,修改传入的 URL 为服务名,服务名拼 URL,框架自动通过注册中心拿实例列表、通过负载均衡策略选择实例、发起请求完成实例选择与负载均衡。比如:
@Configuration //告诉 Spring:这个类里定义了 Bean,启动时要处理,没它,Spring 不会扫描这个类,里面的 @Bean 不会生效
public class OrderConfig {
@LoadBalanced // 注解式负载均衡
@Bean //Spring 注解,表示方法的返回值要注册成 Spring 容器里的 Bean
public RestTemplate restTemplate() {//一个普通 Java 方法,返回一个 `RestTemplate` 对象
return new RestTemplate();//因为 @Bean 修饰后,Spring 会调用这个方法,把返回值当成 Bean 存到容器
}
}
private Product getProductFromRemoteWithLoadBalancerAnnotation(Long productId) {
// 给远程发送请求:service-product 会被动态替换
String url = "http://service-product/product/" + productId;
log.info("远程请求: {}", url);
// 给远程发送请求
return restTemplate.getForObject(url, Product.class);
}
此时底层会将服务名替换为负载均衡后的目标 URL。
1.6 注册中心宕机还能不能调用
经典面试题:如果注册中心宕机,远程调用是否可以成功?
远程调用不是每一次都必须实时访问注册中心。调用方通常会缓存一份服务实例列表,所以如果之前已经调用过商品服务,注册中心短时间宕机时,订单服务可能还能根据本地缓存继续调用商品服务。
但是要分情况看。第一种:从来没调用过目标服务,此时本地没有缓存,第一次调用必须去 Nacos 获取服务地址,如果注册中心宕机,就会失败。第二种:之前调用过目标服务,本地有服务地址缓存,注册中心宕机后仍可能继续调用成功。第三种:注册中心和商品服务都宕机,即使本地缓存了地址,请求也会因为目标服务不可用而失败,常见错误是连接拒绝或超时。
所以面试题可以这样答:注册中心宕机不一定导致远程调用立刻失败,关键看调用方是否已有服务实例缓存,以及目标服务本身是否还存活。
1.7配置中心

如果父模块或公共模块中引入了 spring-cloud-starter-alibaba-nacos-config,子模块 service-order、service-product 都会继承配置中心依赖。Spring Boot 启动时会认为当前服务要从 Nacos 配置中心导入配置,因此每个独立服务都需要在自己的 application.properties 中配置 spring.config.import=nacos:...。
如果 order 写了 spring.config.import,而 product 没写,那么 order 可以启动,product 会报 Add a spring.config.import=nacos: 错误。
解决方式:
- 当前服务要用配置中心:添加
spring.config.import=optional:nacos:service-product.properties - 当前服务不用配置中心:添加
spring.cloud.nacos.config.import-check.enabled=false - 更推荐:不要把 nacos-config 放到所有服务继承的公共依赖中,哪个服务需要配置中心,哪个服务单独引入。
① @Value + @RefreshScope:
- 配合
@RefreshScope让 Bean 在配置变化后刷新,加在配置类上激活配置刷新 - 适合少量配置项
- 用
@Value("${xx}")注入配置
@RefreshScope
@RestController
public class OrderController {
@Value("${order.timeout}")
String orderTimeout;
@Value("${order.auto-confirm}")
String orderAutoConfirm;
@GetMapping("/config")
public String config() {
return "orderTimeout:" + orderTimeout + ", orderAutoConfirm:" + orderAutoConfirm;
}
}
② @ConfigurationProperties(推荐):
- 将配置批量绑定到属性对象中
- 使用时注入该配置类对象读取
- 通常比零散的
@Value更易维护无需@RefreshScope即可自动刷新。 - 定义配置类:
@ConfigurationProperties(prefix = "order")
@Component
@ConfigurationProperties(prefix = "order")
@Data
public class OrderProperties {
String timeout;
String autoConfirm;
String dbUrl;
}
③ NacosConfigManager 监听(自定义逻辑):
目的:
- 项目启动后动态监听配置文件变化
- 变化后拿到新值并执行自定义逻辑(例如通知、刷新缓存等)
- 在启动类中使用
NacosConfigManager获取ConfigService - 为指定的
dataId + group添加 Listener - 在回调中处理变化后的配置内容
@Bean
ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager) {
// ApplicationRunner 是 Spring Boot 提供的启动后执行接口
// 这个 Bean 会在项目启动完成后自动执行
return args -> {
// 从 NacosConfigManager 中获取 Nacos 的配置服务对象
// ConfigService 专门用来操作 Nacos 配置中心
ConfigService configService = nacosConfigManager.getConfigService();
// 给 Nacos 中的某个配置文件添加监听器
// 参数1:dataId,配置文件名
// 参数2:group,配置分组
// 参数3:Listener,监听器对象
configService.addListener(
"service-order.properties",
"DEFAULT_GROUP",
// 匿名内部类:现场创建一个 Listener 接口的实现类对象
new Listener() {
// 指定监听器回调方法使用哪个线程池执行
@Override
public Executor getExecutor() {
// 创建一个固定大小为 4 的线程池
return Executors.newFixedThreadPool(4);
}
// 当 Nacos 配置发生变化时,会自动调用这个方法
// configInfo 就是变化后的最新配置内容
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("配置变化:" + configInfo);
// 这里模拟配置变化后发送邮件通知
System.out.println("邮件通知");
}
}
);
};
}
面试题:配置优先级 思考:Nacos中的数据集和application.properties有相同的配置项,哪个生效? 如果存在多个相同的配置信息,那么:
- 生效配置进入环境变量,应用从环境变量中获取最终值。
1.8 数据隔离
一个项目通常部署在多套环境上,比如 dev、test、prod。
项目中每个微服务的配置信息在每套环境上的值可能不一样,要求项目可以通过切换环境,加载本环境的配置。
如果要完成以上需求,其中的难点是如何:
- 区分多套环境
- 区分多种微服务
- 区分多种配置
- 按需加载配置
实际开发时,一般用 Spring Boot 的 profile 激活当前环境,然后让服务去 Nacos 中加载对应 Namespace 下的配置。例如开发环境加载 dev 命名空间,测试环境加载 test 命名空间,生产环境加载 prod 命名空间。这样同一个服务在不同环境中可以使用不同配置。
Nacos 的解决方案:
- Namespace:用名称空间区分多套环境(dev / test / prod)
- Group:用 Group 区分多种微服务
- Data ID:用 Data-id 区分多种具体配置(如
common.properties、database.properties) - 使用 SpringBoot 激活对应环境的配置
spring.cloud.nacos.config.namespace=xxx
用来指定 Nacos 配置中心使用的命名空间 ID。
如果不设置,默认加载的是 public 命名空间下的配置。
spring.config.import 是 Spring 官方提供的加载外部配置源的标准方式。
Spring Cloud Alibaba 遵循这套官方规范,未来会统一收口到只支持这一种方式。
application.yml 多环境配置:
# 当前服务公共配置:无论 dev/test/prod 都会生效
spring:
profiles:
# 当前启动环境:dev / test / prod
# 改这里,就会切换下面不同环境的配置段
active: dev
application:
# 当前服务名,也会注册到 Nacos
name: service-order
cloud:
nacos:
# Nacos 地址:注册中心、配置中心都连这个
server-addr: 127.0.0.1:8848
config:
# 当前环境对应的 Nacos 命名空间
# active=dev 时,相当于 namespace=dev
# 注意:实际项目里 namespace 常填 Nacos 生成的命名空间 ID
namespace: ${spring.profiles.active:public}
---
# dev 环境生效:active=dev 时才加载这一段
spring:
config:
import:
# 启动时去 Nacos 读取 order 分组下的 common.properties
- optional:nacos:common.properties?group=order
# 启动时去 Nacos 读取 order 分组下的 application.properties
- optional:nacos:application.properties?group=order
activate:
on-profile: dev
---
# test 环境生效:active=test 时才加载这一段
spring:
config:
import:
- optional:nacos:common.properties?group=order
- optional:nacos:haha.properties?group=order
activate:
on-profile: test
---
# prod 环境生效:active=prod 时才加载这一段
spring:
config:
import:
- optional:nacos:common.properties?group=order
- optional:nacos:hehe.properties?group=order
activate:
on-profile: prod
2. OpenFeign
2.1 简介与使用
OpenFeign,是一种 Declarative REST Client,即声明式 Rest 客户端,与之对应的是编程式 Rest 客户端,比如 RestTemplate。
OpenFeign 是声明式 HTTP 客户端,用来简化微服务之间的远程调用。以前使用 RestTemplate 需要手动拼 URL、手动发送请求;使用 OpenFeign 后,只需要写一个接口,用注解描述要调用哪个服务、哪个路径、什么请求方式,Spring 会自动生成代理对象帮你发送 HTTP 请求。它的核心作用是:把远程 HTTP 调用写得像本地方法调用一样。
OpenFeign 会根据接口上的 @FeignClient、@GetMapping、@PostMapping 等注解生成代理对象。调用接口方法时,代理对象不会执行普通 Java 方法逻辑,而是根据注解信息组装 HTTP 请求,然后通过服务发现和负载均衡找到目标服务,最后发送请求并把响应结果转换成 Java 对象。
简单流程是:调用 Feign 接口方法;Feign 代理对象解析注解;根据服务名找实例;负载均衡选择实例;发送 HTTP 请求;反序列化响应结果。
注意:有些第三方 API 不需要注册中心,例如查询天气(墨迹天气等)。此时 @FeignClient 的 url 属性可直接指定第三方地址。
2.2 接入步骤
- 引入依赖:
spring-cloud-starter-openfeign - 启动类添加
@EnableFeignClients(开启 Feign 客户端扫描) - 创建 Feign 接口,使用
@FeignClient(value = "service-product")指定目标服务 - 在接口中使用 Spring MVC 注解定义请求方式与路径
- 注入 Feign 接口即可发起远程调用
核心注解:
- 指定远程地址:
@FeignClient(value = 目标微服务名称) - 指定请求方式:
@GetMapping、@PostMapping、@DeleteMapping… - 指定携带数据:
@RequestHeader、@RequestParam、@RequestBody… - 指定结果返回:响应模型类
MVC 两套使用逻辑:
- 放到 Controller 上是接受这样的请求
- 放到 FeignClient 上是发送这样的请求
使用 OpenFeign 会自动连上注册中心,然后自动负载均衡地挑一个实例发送请求。
2.2 小技巧
如何编写好 OpenFeign 声明式的远程调用接口:
- 针对业务 API:直接复制对方的 Controller 签名即可;
- 第三方 API:根据接口文档确定请求如何发
2.3 一道面试题
面试题:客户端负载均衡与服务端负载均衡区别:
- 客户端负载均衡(如 OpenFeign + LoadBalancer):客户端从注册中心获取实例列表 → 自己选择一个 → 发起调用
- 服务端负载均衡(如 Nginx):客户端直接请求负载均衡服务器 → 由服务端决定转发给哪个实例
2.4 进阶用法
2.4.1日志
在配置文件中指定 feign 接口所在包的日志级别:
logging:
level:
# 指定 feign 接口所在的包的日志级别为 debug 级别
indi.mofan.order.feign: debug
向 Spring 容器中注册 feign.Logger.Level 对象:
@Bean
public Logger.Level feignlogLevel() {
// 指定 OpenFeign 发请求时,日志级别为 FULL
return Logger.Level.FULL;
}
2.4.2超时控制
主要控制两个超时:
- 连接超时(connectTimeout):商品服务器宕机,连接不上
- 读取超时(readTimeout):API 速度慢不返回,读取不到
流程:OpenFeign 发起远程调用 → 限时等待 → 未超时返回正确结果 / 超时则中断调用 → 返回错误信息或兜底数据
如果需要修改默认超时时间,在配置文件中进行如下配置:
spring:
cloud:
openfeign:
client:
config:
# 默认配置
default:
logger-level: full
connect-timeout: 1000
read-timeout: 2000
# 具体 feign 客户端的超时配置
service-product:
logger-level: full
# 连接超时,3000 毫秒
connect-timeout: 3000
# 读取超时,5000 毫秒
read-timeout: 5000
2.4.3重试机制
远程调用超时失败后,还可以进行多次尝试,如果某次成功则返回 ok,如果多次尝试后依然失败则结束调用,返回错误。
OpenFeign 底层默认使用 NEVER_RETRY,即从不重试策略。
向 Spring 容器中添加 Retryer 类型的 Bean:
@Bean
public Retryer retryer() {
return new Retryer.Default();
// 参数:period(初始间隔100ms), maxPeriod(最大间隔1s), maxAttempts(最大重试次数5)
// 间隔递增:100ms → ×1.5 → ×1.5 → ×1.5 ...
// 每次间隔要乘以一个因子
}
这里使用 OpenFeign 的默认实现 Retryer.Default,在这种默认实现下:
public Default() {
this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}
OpenFeign 的重试规则是:
- 重试间隔 100ms
- 最大重试间隔 1s。新一次重试间隔是上一次重试间隔的 1.5 倍,但不能超过最大重试间隔。
- 最多重试 5 次
2.4.4 拦截器
目的:在每次 Feign 请求发出前,统一添加 Header 或其他处理逻辑(如添加 Token)。 这张图说明 Feign 请求在真正发出去之前,可以经过请求拦截器;响应返回后,也可以经过响应拦截器。请求拦截器常用来统一添加请求头,例如 token、traceId、用户信息、来源标识等。
以请求拦截器为例,自定义的请求拦截器需要实现 RequestInterceptor 接口,并重写 apply() 方法:
通过 @Component 注解 ,实现 RequestInterceptor 接口(全局生效):
package indi.mofan.order.interceptor;
public class XTokenRequestInterceptor implements RequestInterceptor {
/**
* 请求拦截器
*
* @param template 封装本次请求的详细信息
*/
@Override
public void apply(RequestTemplate template) {
System.out.println("XTokenRequestInterceptor ...");
template.header("X-Token", UUID.randomUUID().toString());
}
}
如果把拦截器注册成 Spring Bean,它通常会对当前服务内所有 Feign 客户端生效;如果只在配置文件中给某个 Feign Client 配置,则只对指定客户端生效。开发时要注意作用范围,避免把某个接口专用的请求头加到所有远程调用上。
要想要该拦截器生效有两种方法:
-
在配置文件中配置对应 Feign 客户端的请求拦截器,此时该拦截器只对指定的 Feign 客户端生效 ① 不使用
@Component注解,通过配置文件注册拦截器数组(可以在default下全局配置,也可以针对特定服务单独配置(更灵活)):spring: cloud: openfeign: client: config: # 具体 feign 客户端 service-product: # 该请求拦截器仅对当前客户端有效 request-interceptors: - indi.mofan.order.interceptor.XTokenRequestInterceptor -
还可以直接将自定义的请求拦截器添加到 Spring 容器中,此时该拦截器对服务内的所有 Feign 客户端生效 ②通过
@Component注解 ,实现RequestInterceptor接口(全局生效):@Component public class XTokenRequestInterceptor implements RequestInterceptor { // --snip-- }
2.4.5Fallback兜底回调
Fallback,即兜底返回。 目的:在远程调用失败(超时/错误)后返回默认兜底数据,让业务继续往下走。 注意,此功能需要整合 Sentinel 才能实现。
因此需要先导入 Sentinel 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
并在需要进行 Fallback 的服务的配置文件中开启配置: ① 开启 Sentinel 支持:
feign:
sentinel:
enabled: true
现在需要对 Feign 客户端 ProductFeignClient 配置 Fallback,那么需要先实现 ProductFeignClient 编写兜底返回逻辑,并将其交由 Spring 管理:
③ 编写 Fallback 实现类,实现 FeignClient 接口,返回兜底数据。
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public Product getProductById(Long id) {
System.out.println("Fallback...");
Product product = new Product();
product.setId(id);
product.setPrice(new BigDecimal("0"));
product.setProductName("未知商品");
product.setNum(0);
return product;
}
}
之后回到对应的 Feign 客户端,配置 Fallback:
② 在 @FeignClient 中指定 fallback 类:
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}
2.5OpenFeign 小结
- 核心定位:声明式 HTTP 客户端,用注解代替手写
RestTemplate调用,自动集成注册中心与负载均衡。 - 基本使用:引入依赖 →
@EnableFeignClients→ 创建@FeignClient接口 → 注入使用。 - 进阶配置:
- 日志:
Logger.Level.FULL+ 包级别debug,调试远程调用详情 - 超时控制:
connectTimeout(连接超时)+readTimeout(读取超时) - 重试机制:默认不重试,注册
Retryer.Default开启(间隔递增,最大重试 5 次) - 拦截器:实现
RequestInterceptor,统一添加 Header 等请求前处理 - Fallback:整合 Sentinel,远程调用失败时返回兜底数据,保障业务可用性
- 日志:
目的:防止服务雪崩 —— 一个服务挂了 → 调用它的服务也跟着卡住 → 再上游的服务也被拖垮 → 像雪崩一样逐层崩溃。
| 手段 | 作用 |
|---|---|
| 超时控制 | 不无限等待,到时间就断开,释放线程 |
| 重试机制 | 失败后有限次重试,不行就放弃 |
| Fallback 兜底 | 调用失败时返回默认数据,不让整条链路崩掉 |
3. Sentinel
官方文档:Sentinel
Sentinel(分布式系统的流量防卫兵):阿里巴巴开源的 流量控制 与 熔断降级 组件,核心目标是保障微服务在高并发、故障等场景下的 稳定性 与 可用性。
Sentinel 是微服务稳定性保护组件,主要用于流量控制、熔断降级、热点参数限流、系统保护等。它解决的问题是:当请求太多、某个服务变慢、某个接口大量报错时,系统不能无限制地继续放行请求,否则可能导致线程耗尽、服务雪崩。
Sentinel 的核心思想是:先把接口、方法或远程调用定义为资源,再给资源配置规则。请求进入资源前,Sentinel 会检查规则;如果没有违反规则就放行,如果违反规则就限流、熔断或执行兜底逻辑。
核心概念:
- 资源(Resource):Sentinel 保护的目标,可以是一个接口、一段代码或一次远程调用
- 规则(Rule):针对资源制定的保护策略,如流控规则、熔断规则、热点规则等
安装启动
- 下载:Sentinel Dashboard(如 1.8.9)
- 启动:
java -jar sentinel-dashboard-1.8.9.jar - 控制台:
http://localhost:8080(默认账号密码均为sentinel)

3.1 工作原理
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
定义规则:
- 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor),所有 Web 接口均为资源。比如在 Spring Web 项目中,Controller 接口会自动被识别为资源,一般不需要手动定义。
- 编程式:SphU API,在代码中手动用
SphU.entry("资源名")包住一段业务逻辑,表示这段代码是一个受 Sentinel 保护的资源。适合精确保护某一小段代码。 - 声明式:
@SentinelResource,直接在方法上加注解,把这个方法声明为 Sentinel 资源。写法比SphU简洁,常用于保护 Service 方法。
定义资源:
流量控制 FlowRule:限制资源的访问流量,例如限制某接口每秒最多访问 10 次,超过后直接限流,防止瞬时流量把服务打挂。
熔断降级 DegradeRule:当资源出现异常比例高、响应时间过长等情况时,临时熔断该资源,快速失败,避免慢接口或异常接口拖垮整个系统。
系统保护 SystemRule:从整个系统角度保护应用,例如根据系统负载、CPU 使用率、线程数、入口 QPS 等指标进行限流,防止服务器整体过载。
来源访问控制 AuthorityRule:根据调用来源控制是否允许访问,例如只允许某些应用访问该资源,或者禁止某些来源访问。
热点参数 ParamFlowRule:针对请求参数限流,例如商品详情接口中,某个热门商品 ID 被大量访问,可以单独限制这个热点参数的访问频率。
3.2 整合 Sentinel

启动 Dashboard
前往 Sentinel GitHub Realease 页下载 Sentinel Dashboard,这里选择 1.8.8 版本,因此下载 sentinel-dashboard-1.8.8.jar。
在 sentinel-dashboard-1.8.8.jar 所在的目录运行以下命令,启动 Dashboard:
java -jar sentinel-dashboard-1.8.8.jar
启动完成后,浏览器访问 http://localhost:8080/,默认用户与密码均为 sentinel。
服务整合 Sentinel 引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置文件中添加:
spring:
application:
name: service-product
cloud:
sentinel:
transport:
# 控制台地址
dashboard: localhost:8080
# 立即加载服务
eager: true
配置完成后启动对应服务,再前往 Sentinel Dashboard 查看,能够看到对应服务信息。
可以在一个方法上使用 @SentinelResource 注解,将其标记为一个「资源」,当方法被调用时,能够在 Dashboard 的「簇点链路」上找到对应的资源,之后在界面上完成对资源的流控、熔断、热点、授权等操作。
3.3异常处理机制
当请求触发 Sentinel 的保护规则(流控、熔断等)时,会抛出 BlockException。Sentinel 提供了 四种异常处理机制 来捕获和处理这个异常。
核心原则:有兜底回调就走兜底,没有则交给 Spring Boot 全局异常处理。
3.3.1 Web 接口级(BlockExceptionHandler)
目的:为 Controller 层的 Web 接口提供统一的限流/熔断返回格式,让前端拿到结构化的错误信息。
实现方式:自定义 BlockExceptionHandler,统一返回 JSON 格式。
当 Web 接口作为资源被流控时,默认情况下会在页面显示: <pre> Blocked by Sentinel (flow limiting) </pre>
如果需要自定义异常处理,可以实现 BlockExceptionHandler 接口,并将实现类交给 Spring 管理:
响应类R是自定义的
@Component // 注册为 Spring Bean,自动生效
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
String resourceName, // 被保护的资源名称(新版新增参数)
BlockException e) throws Exception {
// 设置响应类型为 JSON
response.setContentType("application/json;charset=utf-8");
// 返回 429 状态码(Too Many Requests)
response.setStatus(429);
// 默认提示信息
R result = R.error(429, "请求被限流,请稍后重试");
// 根据异常类型返回不同提示
if (e instanceof FlowException) {
result = R.error(429, "触发流控规则"); // 流控异常
} else if (e instanceof DegradeException) {
result = R.error(429, "触发熔断降级"); // 熔断异常
}
// 将结果序列化为 JSON 写入响应
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}
以 /create 接口为例,当其被流控时,页面显示:
{
"code": 500,
"message": "/create 被 Sentinel 限制了, 原因: class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
"data": null
}
3.3.2 @SentinelResource 注解级
目的:标注在 非 Controller 层(如 Service)的方法上,将该方法定义为 Sentinel 资源,并可指定 blockHandler 兜底回调。
- 违反规则时 → 有
blockHandler就调用兜底方法 → 没有则向上抛出异常交给全局处理 blockHandler方法必须与原方法在 同一个类 中,且参数列表末尾多一个BlockException
当 @SentinelResource 注解标记的资源被流控时,默认返回 500 错误页。
如果需要自定义异常处理,一般可以增加 @SentinelResource 注解的以下任意配置:
blockHandlerfallbackdefaultFallback
以 blockHandler 为例:
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId) {
// --snip--
}
在当前类中创建名称为 blockHandler 值的方法,并且返回值类型、参数信息与 @SentinelResource 标记的方法一致(可以额外增加一个 BlockException 类型的参数):
// 兜底方法:参数与原方法一致 + 末尾加 BlockException
/**
* 指定兜底回调
*/
public Order createOrderFallback(Long productId, Long userId, BlockException e) {
Order order = new Order();
order.setId(0L);
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(userId);
order.setNickname("未知用户");
order.setAddress("异常信息: " + e.getClass());
return order;
}
当资源被流控时,执行 blockHandler 指定的方法:
{
"id": 0,
"totalAmount": 0,
"userId": 666,
"nickname": "未知用户",
"address": "异常信息: class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
"productList": null
}
3.3.3 OpenFeign 整合
与 @SentinelResource 逻辑一致:指定了 fallback 就调用兜底,否则向上抛出直到 Spring Boot 全局处理。
配置详见 3.2.5 Fallback 部分(feign.sentinel.enabled=true + @FeignClient(fallback = ...))。
3.3.4 SphU 硬编码(了解)
通过 SphU.entry("resourceName") 手动编码的方式定义资源和处理异常,底层源码中经常看到。日常业务开发中较少直接使用。
3.4 流控规则
流控,即流量控制(FlowRule),用于限制多余请求,从而保护系统资源不被耗尽。
3.4.1 阈值类型

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

配置流控规则时,可以点击下方的「高级选项」,在这里可以配置「流控模式」,共有三种可选项:
- 直接:默认选项,对当前资源直接做限制。举例:给
/order/create设置 QPS 阈值为 5,每秒超过 5 次请求就触发流控。。 - 关联:关联资源超阈值时,限流当前资源。
- 链路:根据不同的调用链路来限制同一个资源。仅对于某一路径下的资源访问生效。注意:使用时需要关闭 Sentinel 的 Web 上下文统一合并(
spring.cloud.sentinel.web-context-unify=false),否则所有链路会合并成一个。
web-context-unify 是什么意思?
Sentinel 默认会把所有 Spring MVC 的 Web 入口(Controller 接口)归到同一个上下文(Context)里。也就是说,不管请求是从 /order/seckill 进来的还是从 /order/normal 进来的,Sentinel 在统计链路时都认为它们是 同一个入口。
这就导致一个问题:链路模式失效。
举个例子
假设你有两个 Controller 接口,它们内部都调用了同一个 Service 方法 reduceStock():
/order/seckill → reduceStock() ← 想限流这条链路
/order/normal → reduceStock() ← 不想限流
你在 Sentinel 控制台给 reduceStock 配了链路模式的流控规则,入口资源指定为 /order/seckill。
web-context-unify=true(默认):Sentinel 把所有 Web 请求合并成一个 context,看不到/order/seckill和/order/normal是两条不同的链路 → 链路规则不生效,两条路都会被限或都不被限。web-context-unify=false:Sentinel 为每个 Web 入口创建独立的 context,能识别出两条不同链路 → 链路规则正常生效,只限制秒杀链路,普通订单不受影响。
spring:
cloud:
sentinel:
web-context-unify: false # 关闭上下文合并,让每个入口独立
调用关系包括调用方、被调用方;一个方法又可能会调用其他方法,形成一个调用链路的层次关系;有了调用链路的统计信息,可以衍生出多种流量控制手段。
| 维度 | 直接 | 关联 | 链路 |
|---|---|---|---|
| 作用对象 | 当前资源本身 | 关联的其他资源 | 特定调用链路的入口 |
| 触发逻辑 | 当前资源超阈值 | 关联资源超阈值时,限流当前资源 | 从指定入口发起的请求超阈值 |
| 核心目的 | 保护当前资源 | 保护关联资源或间接限流 | 按入口细分流量控制 |
| 典型场景 | 独立接口的直接限流 | 资源依赖(如读操作限流写操作) | 区分不同调用来源 |
| 配置依赖 | 无需额外配置 | 需指定关联资源 | 需指定资源访问入口 |
3.4.3 流控效果
打开流控规则中的高级配置后,还可以配置「流控效果」,同样有三种选项:
- 快速失败:默认选项。超过阈值直接拒绝,抛出
FlowException。注意,只有该选项支持「流控模式」(直接、关联、链路)的设置。 - Warm Up:初始阈值较低(默认是设定阈值的 $\frac{1}{3}$),随后在预热时间内逐步提升至设定阈值。例如设定阈值为 3 QPS、预热时间 3 秒,初始阈值为 1 QPS,3 秒内逐步升至 3。
- 排队等待:基于漏桶算法,请求进入队列后按固定间隔时间匀速处理。若请求的预期等待时间超过设定的超时时间,则拒绝请求。
| 效果 | 核心机制 | 适用场景 | 阈值动态变化 | 流量特征 |
|---|---|---|---|---|
| 快速失败 | 直接拒绝超出阈值的请求 | 明确系统处理能力并快速保护 | 固定阈值 | 突发流量 |
| Warm Up | 阈值逐步提升 | 冷启动或流量突增的平滑过渡 | 动态提升 | 逐步增长的流量 |
| 排队等待 | 匀速处理请求 | 服务处理均匀,避免突发压力 | 固定阈值 | 均匀的流量 |
3.5 熔断规则
熔断:当下游服务持续出现问题时,暂时 切断 对它的调用,避免拖垮上游服务,等一段时间后再尝试恢复。 熔断规则,即 DegradeRule。
使用熔断规则可以配置熔断降级,用于:
- 切断不稳定调用
- 快速返回不积压
- 避免雪崩效应
最佳实践: 熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

-
思考:有无熔断规则显著区别是什么?
没有熔断:一直硬撑,陪着下游一起死;
有熔断:及时止损,保护自己,等对方恢复了再重新连接。
3.5.1 断路器工作原理(三种状态)
| 状态 | 行为 | 转换条件 |
|---|---|---|
| 关闭(Closed) | 正常放行所有请求 | 统计窗口内触发熔断条件 → 转为 打开 |
| 打开(Open) | 所有请求直接拒绝(或走兜底) | 经过熔断时长后 → 转为 半开 |
| 半开(Half-Open) | 放行一个 探测请求 试探下游是否恢复 | 探测成功 → 回到 关闭;探测失败 → 回到 打开 |
熔断降级里的核心组件是「断路器」,其工作原理如下:
3.5.2 熔断策略
断路器是机制,熔断策略是触发条件。
① 慢调用比例 定义:统计窗口内,响应时间超过设定阈值的请求占比
配置参数:maxSlowCallMs:慢调用阈值(如 1000ms) ratioThreshold:比例阈值(如 0.5,即 50%) statIntervalMs:统计窗口(如 10000ms)
触发条件:在 慢调用阈值 时间内内,请求数 >= 最小请求数,且慢调用比例 >= 比例阈值

在 5000ms 内,有 80%(0.8 的比例阈值)的请求的最大响应时间超过 1000ms,则进行 30s 的熔断。 如果 5000ms 内,请求数不超过 5,就算达到熔断规则,也不进行熔断。
- 设定 最大 RT(响应时间),超过该时间的调用视为”慢调用”
- 在统计时长内,慢调用比例超过阈值 → 触发熔断
举例:最大 RT = 1000ms,比例阈值 = 0.8,统计时长 = 10s,最小请求数 = 5。
如果 10s 内有 8 个以上请求,且其中超过 80% 的调用耗时 > 5000ms,则触发熔断。
② 异常比例
| 定义 | 统计窗口内,抛出异常的请求占比 |
|---|---|
| 配置参数 | ratioThreshold:比例阈值(如 0.3,即 30%) <br>statIntervalMs:统计窗口 |
| 触发条件 | 在统计窗口内,请求数 >= 最小请求数,且异常比例 >= 阈值 |
在远程调用的目标接口里添加 int i = 1 / 0; 模拟远程调用异常。 |
|
| 此时尚未配置任何熔断规则,然后远程调用存在异常的接口,此时会触发使用 OpenFeign 配置的兜底回调。 | |
| 换句话说,没有配置任何熔断规则可以触发兜底回调,而配置熔断规则也是为了触发兜底回调,那岂不是配不配置熔断规则都可以? |
当 A 服务向 B 服务发送请求时,远程调用的 B 服务接口中存在异常,此时触发兜底回调。 在这个过程,由 A 服务发送的请求依旧会打到 B 服务上。 而配置熔断规则后,A 服务发送的请求快速失败,立即出发兜底回调,不会再把请求打到 B 服务上。**

在 5000ms 内,有 80%(0.8 的比例阈值)的请求产生了异常,则进行 30s 的熔断。
- 在统计时长内,异常请求占比超过阈值 → 触发熔断
举例:比例阈值 = 0.8,统计时长 = 5000ms。
5000ms 内超过 80% 的请求抛出异常,则触发熔断。
③ 异常数
| 定义 | 统计窗口内,抛出异常的请求总数 |
| 配置参数 | count:异常数阈值(如 5) <br>statIntervalMs:统计窗口 |
| 触发条件 | 在统计窗口内,异常请求数 >= count |

「异常数」的熔断策略与「异常比例」很类似,只不过「异常数」是直接统计异常个数,就算统计时长内产生了一百万个请求,但只要有 10 个请求出现了异常,也会触发熔断。
- 在统计时长内,异常请求的绝对数量超过阈值 → 触发熔断
举例:异常数阈值 = 10,统计时长 = 5000ms。
5000ms 内出现 10 次以上异常,则触发熔断。
3.6 热点规则
热点参数限流:根据方法参数的具体值来做更细粒度的流控。普通流控是对整个接口限流,热点限流可以针对某个参数的某些值单独设阈值。
所谓热点,即经常访问的数据。很多时候希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。
热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
在需求中学习
现有如下需求:
- 每个用户秒杀 QPS 不得超过 1(秒杀下单时,userId 级别)
- 6 号用户是 vvip,不限制 QPS(例外情况)
- 666 号商品是下架商品,不允许访问
在 Sentinel GitHub Wiki 中指出:
- 目前 Sentinel 自带的 adapter 仅 Dubbo 方法埋点带了热点参数,其它适配模块(如 Web)默认不支持热点规则,可通过自定义埋点方式指定新的资源名并传入希望的参数。注意自定义埋点的资源名不要和适配模块生成的资源名重复,否则会导致重复统计。
| 模块 | 是否支持按参数值分别统计 |
|---|---|
| Dubbo 适配器 | ✅ 支持(调用 Dubbo 方法时,会把参数值一起统计) |
| Web 适配器(Controller) | ❌ 不支持(只能统计接口总调用次数,不能按参数分别统计) |
| 既然 Web Controller 默认不支持按参数统计,那你可以手动在代码里插一个埋点,自己起个新的资源名,自己把参数传进去,这样就能实现按参数分别统计了。 | |
Sentinel 自动给 Controller 生成了一个资源名(如 GET:/user/{id}),如果你自定义埋点时也用了同一个名字,就会导致: |
- 同一个方法的调用被统计了两次
- 限流/熔断规则可能会重复触发
必须使用 @SentinelResource 标注资源(热点规则不支持纯 Web 路径资源)
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "productId", defaultValue = "1000") Long productId) {
Order order = orderService.createOrder(productId, userId);
order.setId(Long.MAX_VALUE);
return order;
}
public Order seckillFallback(Long userId,
Long productId,
// 使用 fallback,而不是 blockHandler
// 最后一个参数类型是 Throwable,而不是 BlockException
Throwable throwable) {
System.out.println("seckillFallback...");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息: " + throwable.getClass());
return order;
}
对 seckill-order 资源进行如下热点规则配置:

这表示:访问 seckill-order 资源时,第一个参数(参数索引 0)在 1 秒的统计窗口时长下,其阈值为 1,也就是 QPS = 1。
需要注意:携带此参数,则参与流控;不携带不流控。
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", defaultValue = "888") Long userId,
@RequestParam(value = "productId", defaultValue = "1000") Long productId) {
// --snip--
}
上述代码中,userId 的默认值为 888,也就是以 http://localhost:8000/seckill?productId=777 的形式进行访问时,userId 的值为 888,此时依旧传入了 userId,依旧触发流控。
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order", fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "productId", defaultValue = "1000") Long productId) {
// --snip--
}
上述代码中,required = false。userId 可以不传,当以 http://localhost:8000/seckill?productId=777 的形式进行访问时,userId 为 null,没有传入 userId,不会触发流控。
经过上述配置,已经完成「每个用户秒杀 QPS 不得超过 1」的需求,但「6 号用户」是个例外:

访问 seckill-order 资源时,第一个参数(参数索引 0)的类型是 long,当其值为 6 时,限流阈值为 1000000,变相不限制「6 号用户」的 QPS。
现在还有最后一个需求「666 号商品是下架商品,不允许访问」,这其实相当于:对 666 号商品进行流控(限流阈值为 0,不允许访问),对其他商品不进行流控(或阈值非常大)。
新增热点规则:

访问 seckill-order 资源时,第二个参数(参数索引 1)在 1 秒的统计窗口时长下,其阈值为 1000000,这是一个无法达到的值,相当于不进行限流。但有一个例外:当其值为 666 时,限流阈值为 0,也就是不允许访问。
3.7 补充:fallback 与 blockHandler 的区别
| 属性 | blockHandler | fallback |
|---|---|---|
| 触发条件 | 违反 Sentinel 规则(流控、熔断、热点等)抛出 BlockException |
方法执行过程中抛出 业务异常(如 NullPointerException) |
| 作用 | 规则层面的兜底 | 业务层面的兜底 |
| 两者都配置时 | Sentinel 规则异常 → blockHandler 优先处理;业务异常 → fallback 优先处理 |
-
注意:使用fallback时抛出类型应为
Throwable为什么 fallback 用
Throwable而不是Exception?因为
Throwable能接住一切可能抛出的东西(包括Error和Exception),是最”兜底”的类型。Sentinel 的 fallback 设计就是要做到”不管出什么问题都能兜住”,所以参数类型定为Throwable。
3.8 Sentinel 小结
- 核心定位:流量控制 + 熔断降级组件,防止服务雪崩,保障系统稳定性。
- 异常处理四种机制:
BlockExceptionHandler:Web 接口统一返回@SentinelResource+blockHandler:非 Controller 层的方法级兜底- OpenFeign
fallback:远程调用兜底 SphU硬编码:底层方式(了解)
- 流控规则:
- 阈值类型:QPS(推荐)/ 并发线程数
- 流控模式:直接 / 链路 / 关联
- 流控效果:快速失败 / Warm Up 预热 / 匀速排队
- 熔断规则:断路器三状态(关闭 → 打开 → 半开),策略有慢调用比例、异常比例、异常数
- 热点规则:按参数值做精细化限流,适合爆款商品等场景。⚠️ Web 适配模块默认不带热点参数,必须用
@SentinelResource自定义埋点才能生效 - fallback vs blockHandler:
blockHandler处理规则异常(末尾参数BlockException),fallback处理业务异常(末尾参数Throwable——Java 异常体系顶级父类,能接住所有异常) - ⚠️ 注意:Sentinel 规则默认只存在内存中,重启后丢失,生产环境需配合持久化方案(如 Nacos)。
4. Gateway
Spring Cloud Gateway:微服务架构中的 API 网关,作为所有外部请求的统一入口,提供 路由转发、请求过滤、负载均衡、跨域处理 等功能,是前端与后端微服务之间的”门卫”。
核心概念:
- 路由(Route):网关的基本构建块,定义了”请求从哪来,转发到哪去”,由 ID、目标 URI、断言和过滤器组成
- 断言(Predicate):匹配请求的条件(如路径、参数、请求头等),满足条件才会路由到目标服务
- 过滤器(Filter):在请求转发前后对请求/响应进行修改(如重写路径、添加请求头、鉴权等)
4.1 网关功能与分类
网关是微服务的统一入口,核心功能:
- 路由转发:根据请求路径将流量分发到不同微服务
- 负载均衡:结合注册中心,自动在多个服务实例间分配请求
- 统一鉴权:在网关层统一处理认证和权限校验
- 跨域处理:集中配置 CORS,无需每个服务单独配置
- 限流熔断:可整合 Sentinel 在网关层做流量防护
网关分类:

| 类型 | 代表 | 说明 |
|---|---|---|
| 响应式网关(推荐) | Spring Cloud Gateway | 基于 WebFlux + Netty,非阻塞异步模型,高并发性能更优 |
| 传统网关 | Zuul 1.x | 基于 Servlet,阻塞式模型,性能相对较低 |
4.2 创建网关
4.2.1 接入步骤
- 创建 Spring Boot 项目,引入依赖:
spring-cloud-starter-gateway、spring-cloud-starter-alibaba-nacos-discovery - 在
application.yml中配置 Nacos 地址、网关端口等基础信息 - 使用
spring.profiles.active=router激活路由配置文件application-router.yml(路由规则单独管理,便于维护) - 启动网关服务,通过网关端口统一访问后端微服务
4.3 路由
路由:网关的核心,每条路由定义了一组匹配规则(断言)和目标地址(URI),请求命中规则后被转发到对应微服务。
4.3.1 规则配置
路由规则在配置文件中定义,每条路由包含三个要素:
- id:路由唯一标识
- uri:目标服务地址(
lb://前缀表示从注册中心负载均衡获取实例) - predicates:断言条件,决定哪些请求匹配该路由
4.3.2 工作原理
请求到达网关后的处理流程:
- 客户端发送请求到网关
- Gateway Handler Mapping:遍历所有路由,用断言匹配请求
- 匹配成功后,将请求交给 Gateway Web Handler
- 请求经过 过滤器链(前置过滤 → 转发到目标服务 → 后置过滤)
- 返回响应给客户端
需求:
配置路由规则时,可直接在配置文件中完成:
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com
predicates:
- Path=/**
order: 10 # 顺序,越小优先级越高
# id 全局唯一
- id: order-route
# 指定服务名称
uri: lb://service-order
# 指定断言规则,即路由匹配规则
predicates:
- Path=/api/order/**
order: 1
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
order: 2
4.4 断言
断言:路由匹配的前提条件。只有请求满足断言规则,才会被路由到目标服务。Gateway 内置了多种断言工厂(如 Path、Query、Header、Method 等),也支持自定义。
断言的两种书写方式:
- 短写法(简洁):
- Path=/api/order/** - 长写法(可配置更多参数):使用
name+args结构
spring:
cloud:
gateway:
routes:
# id 全局唯一
- id: order-route
# 指定服务名称
uri: lb://service-order
# 指定断言规则,即路由匹配规则
# Fully Expanded Arguments
predicates:
# 短写法
# - Path=/api/order/**
# 长写法
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
- id: product-route
uri: lb://service-product
# Shortcut Configuration
predicates:
- Path=/api/product/**
在 Spring Cloud Gateway 的实现中,断言的实现都是 RoutePredicateFactory 接口的实现。
因此除了直接查看官方文档外确定有哪些断言形式外,还可以通过查看 RoutePredicateFactory 的实现:
HeaderRoutePredicateFactoryPathRoutePredicateFactoryReadBodyRoutePredicateFactoryBeforeRoutePredicateFactory- …
断言的名称可以通过去掉实现类名后的 RoutePredicateFactory 来确定,比如 HeaderRoutePredicateFactory 对应名为 Header 的断言。

4.4.2 Query 断言
根据 请求参数 匹配路由,可以指定参数名和参数值(支持正则表达式)。
Query 断言就是根据 URL 里的查询参数(就是
?后面那部分)来决定这个请求要不要走这条路由。
举例:请求必须携带参数 q,且值匹配 haha,才会被路由匹配。
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha
这表示:访问网关的 /search 地址,并且使用了名为 q 的请求参数,且值为 haha,才会将请求转到 https://cn.bing.com。
尽管 Gateway 内置了许多断言规则,但依旧难以满足千变万化的需求。
在上述规则的基础上,再指定一个名为 Vip 的断言规则,要求存在名为 user 的请求参数,并且值为 mofan 时才将请求跳转到 https://cn.bing.com:
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha
- Vip=user,mofan
4.3 自定义断言工厂
内置断言不够用时,可以自定义断言工厂,实现自定义匹配逻辑。
实现三要素:
- 类名:必须以
RoutePredicateFactory结尾,前缀即为配置中使用的名称(如VipRoutePredicateFactory→ 配置名Vip) - 参数映射:重写
shortcutFieldOrder()方法定义短写法的参数顺序 - 断言逻辑:在
apply()方法中编写匹配规则
- 配置示例
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha
# 自定义断言:短写法
# - Vip=user,leifengyang
# 自定义断言:长写法
- name: Vip
args:
param: user
value: leifengyang
自定义 AbstractRoutePredicateFactory 实现类 VipRoutePredicateFactory:
// 1️⃣ 类名必须以 RoutePredicateFactory 结尾
// 前缀 "Vip" 就是配置文件中使用的断言名称(- Vip=user,leifengyang)
// 2️⃣ 必须加 @Component 注册为 Spring Bean,Gateway 才能自动发现
@Component
public class VipRoutePredicateFactory
extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
// 继承 AbstractRoutePredicateFactory,泛型是内部配置类 Config
// 构造方法:告诉父类配置类的类型
public VipRoutePredicateFactory() {
super(Config.class);
}
// 3️⃣ 核心方法:编写断言匹配逻辑
// 返回一个 Predicate,Gateway 每次收到请求都会调用 test() 判断是否匹配
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 从请求中获取 ServerHttpRequest 对象
ServerHttpRequest request = serverWebExchange.getRequest();
// 根据配置的参数名(config.param),从 URL 查询参数中取值
// 例如:localhost/search?user=leifengyang → 取 "user" 参数的值
String first = request.getQueryParams().getFirst(config.param);
// 判断:参数不为空 且 参数值等于配置的期望值(config.value)
// 满足条件返回 true → 路由匹配;不满足返回 false → 路由不匹配
return StringUtils.hasText(first) && first.equals(config.value);
}
};
}
// 4️⃣ 专门为短写法准备的方法
// 定义短写法中参数的顺序:- Vip=user,leifengyang
// 第1个值 "user" 映射到 Config.param
// 第2个值 "leifengyang" 映射到 Config.value
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}
// 5️⃣ 配置类:承载从 YAML 中读取的断言参数
// Gateway 会自动把配置文件中的 args 或短写法的值注入到这个对象中
@Validated
public static class Config {
@NotEmpty
private String param; // 要检查的请求参数名,如 "user"
@NotEmpty
private String value; // 期望的参数值,如 "leifengyang"
// getter / setter 省略
}
}
然后访问 http://localhost/search?q=haha&user=mofan 时,会跳转到 Bing 搜索 haha。
4.5 过滤器
过滤器:在请求被路由之前或之后,对请求/响应进行处理。可以实现路径重写、添加请求头/响应头、鉴权、日志记录等功能。
Gateway 过滤器分为三个层级:
| 过滤器类型 | 作用范围 | 配置方式 |
|---|---|---|
路由过滤器(filters) |
只对当前路由生效 | 配置在某条路由的 filters 下 |
默认过滤器(default-filters) |
对所有路由生效 | 配置在 spring.cloud.gateway.default-filters 下 |
全局过滤器(GlobalFilter) |
对所有路由生效 | 编写 Java 类实现 GlobalFilter 接口 |
![]() |
先前在网关中配置了将 /api/order/ 开头的请求转到 service-order 服务,并要求在 service-order 服务中也存在 /api/order/ 开头的请求路径,比如 /api/order/readDb。如果该服务中原先并不存在 /api/order/ 开头的请求,比如只有 /readDb,那么在以 /api/order/readDb 进行访问就会出现 404 错误。
为了解决这个问题,可以在 service-order 服务对应的 Controller 上添加 基准路径@RequestMapping("/api/order") 注解,但这并不是最佳方案,如果能直接在网关层面解决这个问题就好了,就像把 /api/order/readDb 重写为 /readDb。
Gateway 中内置了许多过滤器,其中有一个常用的过滤器名为:RewritePath,即路径重写。
spring:
cloud:
gateway:
routes:
# id 全局唯一
- id: order-route
# 指定服务名称
uri: lb://service-order
# 指定断言规则,即路由匹配规则
# Fully Expanded Arguments
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
filters:
# 类似把 /api/order/a/bc 重写为 /a/bc,移除路径前的 /api/order/
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
order: 1
- id: product-route
uri: lb://service-product
# Shortcut Configuration
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/product/?(?<segment>.*), /$\{segment}
order: 2
默认过滤器
如果需要为所有路由都添加同一个过滤器,则可以使用 默认过滤器,比如:
spring:
cloud:
gateway:
default-filters:
# 为所有路由添加响应头过滤器
- AddResponseHeader=X-Response-Abc, 123
全局过滤器
除了默认过滤器,全局过滤器也能为所有匹配的路由添加一个过滤器,全局过滤器的配置无需修改配置文件。
实现 GlobalFilter 接口,并将实现类交由 Spring 管理,即可实现全局过滤器。
还可以实现 Ordered 接口,调整多个全局过滤器的执行顺序。
/**
* @author mofan
* @date 2025/5/1 13:49
*/
@Slf4j
@Component
public class RtGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String uri = request.getURI().toString();
long start = System.currentTimeMillis();
log.info("请求 [{}] 开始,时间:{}", uri, start);
return chain.filter(exchange)
.doFinally(res -> {
long end = System.currentTimeMillis();
log.info("请求 [{}] 结束,时间:{},耗时:{}ms", uri, start, end - start);
});
}
@Override
public int getOrder() {
return 0;
}
}
自定义过滤器工厂
尽管 Gateway 内置了许多过滤器,但仍有无法满足需求的情况,此时就需要自定义过滤器工厂。
与自定义断言类似,自定义过滤器工厂的类名也有限制,要求以 GatewayFilterFactory 结尾,而配置文件中配置的名称就是类名开头。
比如需要在配置文件中定义名为 OnceToken 的过滤器,那么需要新增 OnceTokenGatewayFilterFactory:
/**
* @author mofan
* @date 2025/5/1 14:24
*/
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
String value = switch (config.getValue().toLowerCase()) {
case "uuid" -> UUID.randomUUID().toString();
case "jwt" -> "Test Token";
default -> "";
};
HttpHeaders headers = response.getHeaders();
headers.add(config.getName(), value);
}));
}
}
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
filters:
# 自定义过滤器
- OnceToken=X-Response-Token, uuid
4.5 全局跨域
**> 跨域(CORS):浏览器的同源策略限制,前端页面无法直接请求不同域名/端口的接口。微服务架构下每个服务端口不同,跨域问题更突出。
- 单体项目:在 Controller 上加
@CrossOrigin注解 - 微服务架构:每个服务都配置太麻烦 → 直接在 网关层统一配置跨域,一次配置全部搞定**
如果需要配置跨域,可以在 Controller 的类上添加
@CrossOrigin注解。 如果有许多 Controller,逐一添加注解太麻烦,可以在项目的配置类中添加CorsFilter类型的 Bean。 上述方法只适用于单体服务,那如果在微服务中呢? 借由 Gateway 的功能,可以在配置文件中轻松完成微服务的跨域配置:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: '*'
allowed-headers: '*'
allowedMethods: '*'
之后在请求的 Response Headers 中会增加一些允许跨域的信息。
4.6 面试题:微服务之间的调用经过网关吗?
默认不经过。
网关是对接前端的统一入口,后端微服务之间通过 OpenFeign / RestTemplate 直接调用即可。
如果强制走网关也可以,但多了一跳没有实际意义(了解即可)。
4.7 Gateway 小结
- 核心定位:微服务的统一入口,基于 WebFlux 的响应式 API 网关,提供路由转发、过滤、负载均衡、跨域等能力。
- 三大核心组件:
- 路由(Route):定义请求的匹配规则与目标地址(
uri: lb://服务名表示负载均衡转发) - 断言(Predicate):路由的匹配条件,内置 Path、Query、Header 等,支持自定义断言工厂
- 过滤器(Filter):请求/响应的处理链,实现路径重写、添加 Header、鉴权、日志等
- 路由(Route):定义请求的匹配规则与目标地址(
- 过滤器三个层级:
- 路由过滤器(
filters):仅对单条路由生效 - 默认过滤器(
default-filters):对所有路由生效(配置方式) - 全局过滤器(
GlobalFilter):对所有路由生效(编码方式,可写任意逻辑)
- 路由过滤器(
- 自定义扩展:自定义断言工厂(继承
AbstractRoutePredicateFactory)、自定义过滤器工厂(继承AbstractNameValueGatewayFilterFactory) - 全局跨域:在网关层统一配置 CORS,无需每个微服务单独处理
- 微服务间调用不经过网关,网关只面向外部(前端)请求
5. Seata
Seata(Simple Extensible Autonomous Transaction Architecture):阿里巴巴开源的 分布式事务 解决方案,核心目标是在微服务架构下保障跨服务的数据一致性。当一次业务操作涉及多个微服务(如下单同时扣库存、扣余额),Seata 确保它们 要么全部成功,要么全部回滚。
Seata 是分布式事务解决方案。它解决的问题是:一个业务操作涉及多个微服务、多个数据库时,如何保证它们要么全部成功,要么全部回滚。
原有一个事务必须基于同一个数据库连接(Connection),一个项目 → 一个数据库 → 一个 Connection → 一个事务搞定所有操作。 每个服务用自己的数据库连接,无法用本地事务统一管理。 Java Web 的本地事务确实基于一个 Connection 管理。但微服务架构中,一个业务可能涉及多个数据库(图里的场景),每个库都有自己的连接,无法用单连接事务统一管理。解决办法是用分布式事务框架(如 Seata)或消息队列保证最终一致性。 例如购买商品时,可能要生成订单、扣库存、扣账户余额。这三个操作可能分别属于订单服务、库存服务、账户服务,并且连接不同数据库。如果订单创建成功,但扣库存失败,就会出现数据不一致。Seata 就是为了解决这种跨服务、跨数据库事务一致性问题。

核心概念:
- TC(Transaction Coordinator / 事务协调器):Seata 服务器本身,负责维护全局事务和分支事务的状态,驱动全局提交或回滚
- TM(Transaction Manager / 事务管理器):事务的发起方(通常是调用入口的微服务),负责定义全局事务的范围——开始、提交或回滚全局事务
- RM(Resource Manager / 资源管理器):事务的参与方(每个被调用的微服务),负责管理分支事务,向 TC 注册并汇报分支事务状态
- 全局事务:由 TM 发起的一次完整业务操作,包含多个分支事务
- 分支事务:全局事务中每个微服务各自执行的本地事务(例如:订单服务创建订单是一个分支,商品服务扣库存是另一个分支)
5.1 环境搭建与接入
安装启动
- 下载:Seata Server 安装包
- 启动:
seata-server.bat - 控制台:
http://localhost:7091(7091 是 Web 管理界面端口,8091 才是 TC 协调通信端口)
下载并解压 Seata 后,进入 bin 目录,使用 seata-server.bat 命令启动 Seata。
下载的 Seata 版本保证与 pom 文件中引入的 spring-cloud-alibaba-dependencies 依赖中的 Seata 版本一致。
在需要使用分布式事务的模块中添加依赖: 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2023.0.3.2</version>
</dependency>
配置 TC 地址(Seata服务器)
在 resources 目录下创建 file.conf:
service {
#事务服务组映射
vgroupMapping.default_tx_group = "default"
#仅当注册中心类型为file时支持,请不要配置多个地址
default.grouplist = "127.0.0.1:8091"
#降级开关,当前暂不支持
enableDegrade = false
#禁用Seata全局事务
disableGlobalTransaction = false
}
开启全局事务
在事务发起方(TM)的业务方法上添加 @GlobalTransactional 注解,即可将该方法内的所有远程调用纳入分布式事务管理。各分支事务(RM)会自动向 TC 注册并汇报执行状态。
@GlobalTransactional // 开启 Seata 全局事务,分支事务会自动汇报给 TC
public Order createOrder(Long productId, Long userId) {
// 远程调用:扣减库存(分支事务1)
productFeignClient.reduceStock(productId);
// 本地操作:创建订单(分支事务2)
Order order = new Order();
// ...
return order;
}
注意:
@EnableFeignClients(basePackages = "com.atguigu.order.feign")仍然需要配置以启用 Feign 客户端扫描。
本地事务测试
在启动类上加上 @EnableTransactionManagement,在方法上加上 @Transactional:
| 注解 | 加在哪里 | 作用 |
|---|---|---|
@EnableTransactionManagement |
启动类上 | 开启 Spring 注解式事务管理功能(”开关”),不开则 @Transactional 不生效。Spring Boot 默认已自动开启,显式加上更清晰 |
@Transactional |
方法(或类)上 | 将该方法包裹在一个 本地数据库事务 中,方法内所有数据库操作要么全部成功提交,要么遇到异常全部回滚。常用参数:rollbackFor = Exception.class(指定哪些异常触发回滚,默认只回滚 RuntimeException) |
| 示例代码 |
@Override
@Transactional(rollbackFor = Exception.class)
public void deduct(String commodityCode, int count) {
storageTblMapper.deduct(commodityCode, count);
if (Objects.equals(5, count)) {
throw new RuntimeException("内存不足!");
}
}
@Transactional 与 @GlobalTransactional 的区别:
@Transactional |
@GlobalTransactional |
|
|---|---|---|
| 范围 | 单个服务内的本地数据库事务 | 跨多个微服务的分布式事务 |
| 管理者 | Spring 事务管理器 | Seata TC(事务协调器) |
| 回滚能力 | 只能回滚本服务的数据库操作 | 能协调所有参与服务一起回滚 |
💡 在 Seata 场景下两者通常 配合使用:每个分支事务用
@Transactional保证本地原子性,入口方法用@GlobalTransactional保证全局一致性。
- 未使用
@GlobalTransactional时:某个分支出错,只有出错的服务回滚,其他服务不会回滚 → 数据不一致 - 使用
@GlobalTransactional后:某个分支出错,TC 会通知所有分支事务回滚 → 数据一致
5.2 架构原理
整体流程:
- TM 向 TC 申请开启一个全局事务,TC 生成全局事务 ID(XID)
- XID 在微服务调用链中传播(通过 Feign 请求头自动传递)
- 每个 RM 将本地事务作为分支事务向 TC 注册,并汇报执行结果
- TM 根据业务执行结果,通知 TC 发起全局 提交 或 回滚
- TC 驱动所有分支事务完成提交或回滚

5.3 AT 模式与二阶提交协议
AT 模式(Auto Transaction):Seata 默认推荐 的事务模式,基于 二阶段提交协议(2PC) 实现,对业务代码 无侵入。核心思想是先让各分支事务本地提交,再由 TC 统一协调最终的提交或回滚。
关键名词解释:
| 名词 | 解释 |
|---|---|
| 前镜像(Before Image) | 执行 SQL 之前,将目标数据的当前状态快照记录下来,用于回滚时恢复数据 |
| 后镜像(After Image) | 执行 SQL 之后,将目标数据的新状态快照记录下来,用于回滚前做数据校验 |
| undo_log | 回滚日志表,保存前镜像和后镜像数据,每个参与 Seata 的数据库都需要建此表 |
| 全局锁(Global Lock) | 由 TC 管理的行级锁,锁定被操作的数据行,防止其他全局事务并发修改同一数据。注意:这不是 MySQL 的全局锁(锁整个数据库),而是类似 MySQL 行锁级别,只锁操作涉及的数据 |
| XID | 全局事务 ID,在整个调用链中传播,用于标识同一个全局事务 |
| Branch ID | 分支事务 ID,每个 RM 注册分支时由 TC 分配,用于标识某个分支事务 |
5.3.1 二阶提交协议流程
第一阶段:本地提交(各分支事务独立执行)
每个分支事务(RM)依次执行以下步骤:
-
生成前镜像:对即将操作的数据执行
SELECT,将当前数据状态记录为前镜像(Before Image) -
执行业务 SQL:
- MySQL 自动对操作行加 行锁(
SELECT ... FOR UPDATE),锁定数据防止并发修改 - 执行具体的业务 SQL(INSERT / UPDATE / DELETE)
- SQL 执行完毕后释放 MySQL 行锁
💡 行锁期间:普通
SELECT可以正常读取(取决于隔离级别),但加锁读(SELECT ... FOR UPDATE)会被阻塞 - MySQL 自动对操作行加 行锁(
-
生成后镜像:将操作后的数据状态记录为后镜像(After Image)
-
写入 undo_log:将前镜像和后镜像一起保存到
undo_log表中 -
向 TC 注册分支 & 申请全局锁:RM 向 TC 注册当前分支事务,并申请全局锁锁定被操作的数据行,防止其他全局事务修改
-
本地事务提交:将业务数据变更和
undo_log记录在 同一个本地事务 中一起提交(保证原子性) -
向 TC 汇报:告知 TC 当前分支事务的执行状态(成功 / 失败)
⚠️ 关键点:第一阶段就 真正提交了本地事务,数据已经落库。这意味着不会长时间持有数据库锁,不会在第二阶段阻塞数据库。
第二阶段:全局提交或回滚(由 TC 统一协调)
TC 能感知到所有分支事务的状态,根据整体情况决定提交或回滚:
✅ 情况一:所有分支事务都成功 → 全局提交
- TC 通知所有分支事务进行提交
- 各 RM 收到通知后,将删除
undo_log记录的操作放入 异步任务队列 - 异步 + 批量 删除对应的
undo_log记录(因为数据已经在第一阶段提交了,不需要额外操作,只需清理日志)
❌ 情况二:某个分支事务失败 → 全局回滚
- TC 通知 所有 分支事务进行回滚
- 各 RM 通过 XID + Branch ID 找到对应的
undo_log记录 - 数据校验:将后镜像与数据库中的 当前数据 进行对比
- 一致 → 说明数据没有被其他操作篡改,安全执行回滚:用前镜像的数据覆盖当前数据,恢复到操作前的状态
- 不一致 → 说明数据已被其他操作修改(出现 脏写),需要根据配置的策略处理(如人工介入、忽略等)
- 回滚完成后,删除
undo_log记录
⚠️ 全局锁的生命周期:只要还有分支事务未处理完(提交或回滚),全局锁就 一直存在。整个流程全部结束后,全局锁才会被释放。
5.4 四种事务模式
| 模式 | 原理简述 | 适用场景 |
|---|---|---|
| AT 模式(默认推荐) | 基于二阶段提交,通过 undo_log 自动记录和回滚,对业务 无侵入 | 大多数业务场景,适合关系型数据库 |
| TCC 模式 | 业务层面的二阶段:手动实现 Try(预留资源)→ Confirm(确认提交)/ Cancel(取消回滚)三个方法 | 对性能要求高、需要精细控制资源的场景(如资金操作) |
| Saga 模式 | 长事务方案:每个分支事务提交后,如需回滚则执行预先定义的 补偿操作(逆向操作) | 业务流程长、参与方多、无法做资源预留的场景、请假等 |
| XA 模式 | 基于数据库原生的 XA 协议,由数据库本身保证事务一致性,分支事务在第一阶段 不提交,等 TC 通知后才统一提交 | 对一致性要求极高、可接受较长锁定时间的场景 |
5.5 Seata 小结
- 核心定位:分布式事务解决方案,保障微服务间跨服务调用的数据一致性。
- 三大角色:TC(事务协调器 / Seata 服务器)、TM(事务发起方)、RM(事务参与方)
- 接入方式:引入依赖 → 配置
file.conf指向 TC → 业务方法加@GlobalTransactional即可 - AT 模式(默认):基于二阶段提交协议,对业务无侵入
- 第一阶段:生成前后镜像 → 执行业务 SQL → 写入 undo_log → 申请全局锁 → 本地提交 → 向 TC 汇报
- 第二阶段(提交):异步批量删除 undo_log
- 第二阶段(回滚):校验后镜像 → 用前镜像恢复数据 → 删除 undo_log
- 四种事务模式:AT(默认无侵入)、TCC(手动三阶段)、Saga(补偿型长事务)、XA(数据库原生协议)
- 关键机制:全局锁保护数据不被并发篡改;第一阶段即真正提交本地事务,不长时间占用数据库锁
