目录
前言
Spring Cloud 是一个基于 Spring Boot 的微服务架构综合解决方案,提供了一套完整的分布式系统开发工具集。它通过模块化设计简化了微服务基础设施的搭建,让开发者能快速构建弹性、可靠、可扩展的云原生应用。
核心组件有Nacos、OpenFeign、Sentinel、Gateway、Seata等待,下面逐一介绍。
参考视频: Spring Cloud 快速通关
环境配置:jdk17
源代码地址:yozp/cloud-demo: springcloud自学代码 (github.com)
有参考其他同学的笔记...
如果本文对你有用,可以点个赞吗~
1.Nacos
1.1.简介
官网链接:https://nacos.io/
概念:Nacos 是一个开源的、云原生的动态服务发现、配置和服务管理平台,它的名字来源于 Naming (命名) 和 Configuration (配置) Service (服务)。
安装:
- 下载 nacos-server-2.2.0 版本的安装包:
- 找到nacos下的bin文件夹,进入输入cmd,再输入启动命令:
startup.cmd -m standalone
1.2.服务注册
微服务应用启动时,可以将自己的信息(如 IP 地址、端口号、服务名、健康状态等)注册到 Nacos 服务器。
- 引入相关依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 服务发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 远程调用 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
- 配置nacos地址
spring: cloud: nacos: # 配置 Nacos 地址 server-addr: 127.0.0.1:8848
- 启动项目,访问 http://localhost:8848/nacos/
- 测试集群模式:单机情况下通过改变端口号模拟微服务集群,例如添加 Program arguments 信息为
--server.port=8001 --server.port=8002 --server.port=8003
1.3.服务发现
服务消费者(客户端)可以通过 Nacos 查询并获取它需要调用的服务的可用实例列表(提供者地址)。
-
开启服务发现,在主启动类上添加
@EnableDiscoveryClient
注解 -
提供两款 API 的服务发现功能:
DiscoveryClient
和NacosServiceDiscovery
。前者为 Spring 提供的服务发现标准接口,后者由 Nacos 提供。
举例子:获取商品服务
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance instance = instances.get(0);
1.4.远程调用
消费者通过服务发现获取实例地址后发起远程调用。
主要通过 RestTemplate 实现远程调用
举例子:下单场景,订单服务调用商品服务
基本流程:
1.5.负载均衡
结合客户端负载均衡器(如 Spring Cloud Ribbon, Nacos 自身也提供),服务发现功能使得请求能动态地、负载均衡地路由到健康的服务实例上。
1)引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2)配置 RestTemplate
在配置类中将 RestTemplate 放进添加Bean
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
3)使用 LoadBalancerClient 实现负载均衡
主要调用其choose()方法,将服务名传入,即可实现负载均衡
private Product getProductFromRemoteWithLoadBalancer(Long productId) {
ServiceInstance instance = loadBalancerClient.choose("service-product");
// 远程 url
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + productId;
log.info("远程请求: {}", url);
// 给远程发送请求
return restTemplate.getForObject(url, Product.class);
}
4)使用 @LoadBalanced 实现负载均衡
在远程调用RestTemplate方法上面加上@LoadBalanced即可实现
private Product getProductFromRemoteWithLoadBalancerAnnotation(Long productId) {
// 给远程发送请求:service-product 会被动态替换
String url = "http://service-product/product/" + productId;
log.info("远程请求: {}", url);
// 给远程发送请求
return restTemplate.getForObject(url, Product.class);
}
1.6.配置中心
将应用程序的配置信息(如数据库连接、功能开关、参数设置等)集中存储在 Nacos 服务器上。
读取配置步骤:
1)引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2)导入配置
可以导入多个,同时指定分组
---
spring:
config:
import:
- nacos:common.yml?group=order # 关键!显式导入 Nacos 配置
- nacos:database.yml?group=order
3)若导入了依赖但是不想对这个服务启动配置中心,则禁用
spring:
cloud:
nacos:
config:
import-check:
# 禁用 nacos 配置中心导入检查
enabled: false
动态刷新配置的三种方法:
-
@Value("${xx}")
获取配置 +@RefreshScope
实现动态刷新 -
@ConfigurationProperties
无感自动刷新(推荐) -
NacosConfigManager
监听配置变化
举例:以@ConfigurationProperties为例
创建一个类来接收配置参数,加上注解,同时指定配置前缀,这样就能实现动态刷新
@Data
@Component
@ConfigurationProperties(prefix = "order")//批量配置绑定在nacos下,可以实现自动刷新配置
public class OrderProperties {
String timeout;
String autoConfirm;
String dbUrl;
}
order配置举例:
order:
timeout: 30min
auto_confirm: 7d
1.7.数据隔离
支持按照不同的环境(如 dev, test, prod)、不同的集群、不同的命名空间等维度来管理和隔离配置。
需求描述
- 项目有多套环境:dev,test,prod
- 每个微服务,同一种配置,在每套环境的值都不一样。
- 如:database.properties • 如:common.properties
- 项目可以通过切换环境,加载本环境的配置
Nacos 的解决方案:
- 用名称空间区分多套环境
- 用 Group 区分多种微服务
- 用 Data-id 区分多种配置
- 在配置文件中激活对应环境的配置
配置示例:
这是订单服务的配置类
server:
port: 8000
spring:
application:
name: service-order
profiles:
active: dev #手动指示激活哪个环境
cloud:
nacos:
# 配置 Nacos 地址
server-addr: 127.0.0.1:8848
config:
namespace: ${spring.profiles.active:public} #要读取的名称空间,如dev、prod、test(与环境关联),默认设置为public
import-check: #因为默认需要导入配置,但是我们是动态指定,所以先禁用
enabled: false
#上面指示加载哪个名称空间(环境),下面指示加载哪个配置
---
spring:
config:
import:
- nacos:common.yml?group=order # 关键!显式导入 Nacos 配置
- nacos:database.yml?group=order
activate:
on-profile: dev
---
spring:
config:
import:
- nacos:common.yml?group=order # 关键!显式导入 Nacos 配置
- nacos:database.yml?group=order
- nacos:haha.yml?group=order
activate:
on-profile: test
---
spring:
config:
import:
- nacos:common.yml?group=order # 关键!显式导入 Nacos 配置
- nacos:database.yml?group=order
- nacos:hehe.yml?group=order
activate:
on-profile: prod
1.8.常见面试题
1)如果注册中心宕机,远程调用是否可以成功?
- 若从未调用过:调用会立即失败
- 若调用过:会因为存在缓存的服务信息,调用会成功,但如果注册中心和对方服务都宕机,因为会缓存名单,调用会阻塞后失败(Connection Refused)
2)如果存在多个相同的配置信息,那么会读取哪个配置?
配置信息优先级遵循:
- 先导入优先:SpringCloud提供的config.import可以以逗号分割,导入多个配置信息,对于相同配置信息,先导入的配置优先
- 外部优先:配置中心里可能存在与项目配置文件里相同的配置信息,此时外部优先,即配置中心里的配置优先
2.OpenFeign
2.1.简介
OpenFeign,是一种 Declarative REST Client,即声明式 Rest 客户端。核心思想是 “声明式”。开发者只需要定义一个 Java 接口,并使用特定的注解来描述这个接口应该如何映射到某个 HTTP API 请求(如目标服务名、路径、参数、请求方法、请求体等)。
相关注解:
- 指定远程地址:@FeignClient
- 指定请求方式:@GetMapping、@PostMapping、@DeleteMapping ...
- 指定携带数据:@RequestHeader、@RequestParam、@RequestBody ...
- 指定结果返回:响应模型
mvc注解的两套使用逻辑,如@GetMapping:
- 标注在Controller下,是接收请求
- 标注在FeignClient下,是发送请求
如何编写好 OpenFeign 声明式的远程调用接口:
-
针对业务 API:直接复制对方的 Controller 签名即可;
-
第三方 API:根据接口文档确定请求如何发
2.2.整合OpenFeign
1)引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2)在主启动类上使用以下注解:
@EnableFeignClients
3)编写一个远程调用的接口:
使用声明式 Rest 客户端实现远程调用,同时自动实现负载均衡,不需要手动编写
/**
* 发送远程调用的客户端
*/
@FeignClient(value = "service-product")
public interface ProductFeignClient {
/**
* 表示向/product/{id}发送请求,同时接收Product响应对象
*/
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}
4)调用接口
//引入
@Autowired
private ProductFeignClient productFeignClient;
//调用
Product product=productFeignClient.getProductById(productId);
2.3.日志
开启 Feign 日志(通常设为 BASIC
或 FULL
)有助于调试,但生产环境注意日志级别和性能。
1)配置文件:
logging:
level:
# 指定 feign 接口所在的包的日志级别为 debug 级别
com.yzj.order.feign: debug
2)向 Spring 容器中注册 feign.Logger.Level
对象:
@Bean
public Logger.Level feignlogLevel() {
// 指定 OpenFeign 发请求时,日志级别为 FULL
return Logger.Level.FULL;
}
2.4.超时控制
务必配置合理的连接超时 (connectTimeout
) 和读取超时 (readTimeout
),防止线程阻塞。
配置文件:
spring:
cloud:
openfeign:
client:
config:
default: # 全局默认配置
logger-level: full
connect-timeout: 1000
read-timeout: 2000
service-product: # 针对特定服务的配置(覆盖默认)
logger-level: full
connect-timeout: 3000 # 连接超时,3000 毫秒
read-timeout: 5000 # 读取超时,5000 毫秒
2.5.重试机制
远程调用超时失败后,还可以进行多次尝试,如果某次成功返回ok,如 果多次依然失败则结束调用,返回错误。底层默认不重试。
默认重试规则
-
重试间隔 100ms
-
最大重试间隔 1s。新一次重试间隔是上一次重试间隔的 1.5 倍,但不能超过最大重试间隔。
-
最多重试 5 次
向 Spring 容器中添加 Retryer
类型的 Bean即可生效:
//重试机制
//使用默认的重试等待次数(最大间隔1秒,最大重连5次)
@Bean
public Retryer retryer() {
return new Retryer.Default();
}
2.6.拦截器
拦截器允许在Feign客户端发起请求前或收到响应后对请求或响应进行定制化处理。拦截器通常用于添加请求头、日志记录、认证授权等通用逻辑。
举例:
1)自定义请求拦截器
//这个拦截器会被自动调用,不需要编写其他配置
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {
/**
* 请求拦截器
*
* @param template 封装本次请求的详细信息
*/
@Override
public void apply(RequestTemplate template) {
System.out.println("XTokenRequestInterceptor ...");
template.header("X-Token", UUID.randomUUID().toString());
}
}
2)拦截器生效的两种方法
方式一:通过配置文件注册
spring:
cloud:
openfeign:
client:
config:
# 具体 feign 客户端
service-product:
# 该请求拦截器仅对当前客户端有效
request-interceptors:
- com.yzj.order.interceptor.XTokenRequestInterceptor
方式二:将自定义的请求拦截器添加到 Spring 容器中(@Component注解)(推荐)
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {
// ...
}
2.7.Fallback兜底返回
OpenFeign通过Hystrix或Sentinel支持服务降级(Fallback),在远程调用失败时返回预设的兜底数据。
注意,此功能需要整合 Sentinel 才能实现。
1)导入Sentinel 依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2)配置文件
feign:
sentinel:
enabled: true
3)对 Feign 客户端 ProductFeignClient
配置 Fallback
先实现 ProductFeignClient
编写兜底返回逻辑,并将其交由 Spring 管理
//当远程调用getProductById失败就会触发
@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;
}
}
4)在 ProductFeignClient 指定熔断降级处理类
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient{
//...
}
只要不启动product服务即可测试,同时要关掉public Retryer retryer(),否则一直重连,最后直接返回500,得不到想要的效果(兜底数据)
2.8.常见面试题
1)客户端负载均衡与服务端负载均衡的区别?
- 客户端负载均衡由客户端决定请求分发策略,客户端内置负载均衡逻辑(如轮询、随机、权重等),直接从服务注册中心(如Nginx、Eureka、Consul)获取可用服务列表并选择目标实例。
- 服务端负载均衡通过独立组件(如Nginx、HAProxy、F5)接收请求,按策略(如最小连接数、IP哈希)转发到后端服务。客户端无需感知服务实例信息。
3.Sentinel
3.1.简介
官方文档:Sentinel
Sentinel 是阿里巴巴开源的流量控制组件,主要用来保护微服务和分布式系统,防止因为流量过大或服务故障导致系统崩溃。可以简单理解为它是一个“保护盾”,用来保障系统的稳定性和高可用性。
定义资源:(Sentinel 保护的基本单元)
- 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor),所有 Web 接口均为资源
- 编程式:SphU API
- 声明式:
@SentinelResource
定义规则:(定义在资源上,控制资源访问行为的策略集合)
- 流量控制(FlowRule)
- 熔断降级(DegradeRule)
- 系统保护(SystemRule)
- 来源访问控制(AuthorityRule)
- 热点参数(ParamFlowRule)
3.2.原理
架构原理:
Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性
工作原理:
3.3.整合Sentinel
1)下载并启动 Dashboard
下载 Sentinel Dashboard,选择 1.8.8 版本,下载 sentinel-dashboard-1.8.8.jar。
进入下载的目录,输入cmd,运行以下命令:
java -jar sentinel-dashboard-1.8.8.jar
启动完成后,浏览器访问 http://localhost:8080/,默认用户与密码均为 sentinel
。
2)引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3)配置文件
spring:
cloud:
sentinel:
transport:
# 控制台地址
dashboard: localhost:8080
# 立即加载服务
eager: true
配置完成后启动项目,进入Sentinel Dashboard查看,可以看到对应的服务信息
4)@SentinelResource 注解
可以在一个方法上使用 @SentinelResource
注解,将其标记为一个「资源」,当方法被调用时,能够在 Dashboard 的「簇点链路」上找到对应的资源,之后在界面上完成对资源的流控、熔断、热点、授权等操作。
//sentinel定义资源,如果只是指定了名称,则表示没有自定义异常
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId){
//...
}
3.4.异常处理
1)web 接口资源
自定义异常处理时,可以实现 BlockExceptionHandler
接口,并将实现类交给 Spring 管理:
/**
* 自定义返回错误信息
* 同时交给spring容器
*/
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private final ObjectMapper objectMapper;
public MyBlockExceptionHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
String resourceName,
BlockException e) throws Exception {
// too many request
response.setStatus(429);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
R error = R.error(500, resourceName + " 被 Sentinel 限制了, 原因: " + e.getClass());
String json = objectMapper.writeValueAsString(error);
writer.write(json);
writer.flush();
writer.close();
}
}
以 /create
接口为例,当其被流控时,页面显示:
{
"code": 500,
"message": "/create 被 Sentinel 限制了, 原因: class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
"data": null
}
2)@SentinelResource 注解标记资源
需要自定义异常处理时,一般可以增加 @SentinelResource
注解的以下任意配置:
-
blockHandler
-
fallback
-
defaultFallback
以 blockHandler
为例:
@Override
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId) {
// ...
}
在当前类中创建名称为 blockHandler
值的方法,并且返回值类型、参数信息与 @SentinelResource
标记的方法一致(可以额外增加一个 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)OpenFeign 接口资源
当 Feign 接口作为资源并被流控时,如果调用的 Feign 接口指定了 fallback
,那么就会使用 Feign 接口的 fallback
进行异常处理,否则由 SpringBoot 进行全局异常处理。
其实就是上面OpenFeign的fallback
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)//加上兜底机制fallback
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}
3.5.流控规则
流控,即流量控制(FlowRule),用于限制多余请求,从而保护系统资源不被耗尽。
1)阈值类型
- QPS:Queries Per Second,用于限制资源每秒的请求次数,防止突发流量,应用于高频短时接口(如 API 网关)。当每秒的请求数超过设定的阈值时,就会触发流控。比如上图设置的 QPS = 5,就表示每秒最多允许 5 个请求。
- 并发线程数:用于限制同时处理该资源的线程数(即并发数),保护系统资源(线程池),应用于耗时操作(如数据库查询)。当处理该资源的线程数超过阈值时,就会触发流控。比如设置并发线程数为 5,表示最多允许 5 个线程同时处理该资源。
2)是否集群
- 单机均摊:将设置的「均摊阈值」均摊到每个节点。以上图为例,假设集群有 3 个节点,那么每个节点的阈值都是 5;
- 总体阈值:整个集群共享设置的「均摊阈值」。假设集群有 3 个节点,这 3 个节点的的总阈值只有 5,比如按
2-2-1
的形式将阈值均摊到每个节点。
3)流控模式
- 直接:默认选项。
- 关联:关联资源超阈值时,限流当前资源。
- 链路:仅对于某一路径下的资源访问生效。使用时需要在配置文件中设置
spring.cloud.sentinel.web-context-unify=false
。
4)流控效果
- 快速失败:默认选项。注意,只有该选项支持「流控模式」(直接、关联、链路)的设置。
- Warm Up:初始阈值较低(默认是设定阈值的 $\frac{1}{3}$),随后在预热时间内逐步提升至设定阈值。例如设定阈值为 3 QPS、预热时间 3 秒,初始阈值为 1 QPS,3 秒内逐步升至 3。
- 排队等待:基于漏桶算法,请求进入队列后按固定间隔时间匀速处理。若请求的预期等待时间超过设定的超时时间,则拒绝请求。
3.6.熔断规则
即 DegradeRule,当调用某个不稳定资源(如远程服务、数据库访问慢)时,为了防止调用方线程被大量阻塞导致自身资源耗尽(线程池耗尽),需要及时中断对该资源的调用,并进行降级处理(返回默认值、空值、友好提示等),快速释放资源。当检测到该资源恢复稳定后,再恢复调用。
三种熔断策略:
1)慢调用比例
表示在 5000ms 内,有 80%(0.8 的比例阈值)的请求的最大响应时间超过 1000ms,则进行 30s 的熔断。 如果 5000ms 内,请求数不超过 5,就算达到熔断规则,也不进行熔断。
2)异常比例
表示在 5000ms 内,有 80%(0.8 的比例阈值)的请求产生了异常,则进行 30s 的熔断。
3)异常数
表示在 5000ms 内,有 10个的请求产生了异常,则进行 30s 的熔断。
3.7.热点规则
即 ParamFlowRule,对频繁访问的热点参数进行细粒度限流。例如,针对某个商品ID (productId=12345
) 的查询请求特别多,需要单独限制这个商品ID的访问频率,而不影响其他商品ID的访问。
需求:
-
每个用户秒杀 QPS 不得超过 1(秒杀下单时,userId 级别)
-
6 号用户是 vvip,不限制 QPS(例外情况)
-
666 号商品是下架商品,不允许访问
这表示:访问 seckill-order
资源时,第一个参数(参数索引 0)在 1 秒的统计窗口时长下,其阈值为 1,也就是 QPS = 1。
访问 seckill-order
资源时,第一个参数(参数索引 0)的类型是 long
,当其值为 6
时,限流阈值为 1000000
,变相不限制「6 号用户」的 QPS。
访问 seckill-order
资源时,第二个参数(参数索引 1)在 1 秒的统计窗口时长下,其阈值为 1000000,这是一个无法达到的值,相当于不进行限流。但有一个例外:当其值为 666 时,限流阈值为 0,也就是不允许访问。
4.Gateway
4.1.简介
官网:Spring Cloud Gateway 中文文档 (springdoc.cn)
Spring Cloud Gateway 是 Spring Cloud 生态系统中的 API 网关组件,基于 Spring 5、Spring Boot 2 和 Project Reactor 构建,旨在为微服务架构提供简单、有效且统一的 API 路由管理方式。
4.2.路由
根据预定义的规则,将请求转发到后端的微服务实例。
需求:
-
客户端发送
/api/order/**
转到service-order
-
客户端发送
/api/product/**
转到service-product
-
以上转发有负载均衡效果
示例配置文件:
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com
predicates:
- Path=/**
order: 10
- id: order-route #全局唯一id
uri: lb://service-order # 指定服务名称
predicates: # 指定断言规则,即路由匹配规则
- Path=/api/order/**
order: 1 #优先级
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
order: 2
4.3.断言
用于判断当前请求是否符合某个路由规则的条件。
1)断言短写法和长写法
示例配置文件:
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
predicates:
# - Path=/api/order/** # 断言短写法
- name: Path # 断言长写法
args:
patterns: /api/order/**
matchTrailingSlash: true
2)断言种类
断言的实现都是 RoutePredicateFactory
接口的实现,可以推断出断言的名称可以通过去掉实现类名后的 RoutePredicateFactory
来确定,比如 HeaderRoutePredicateFactory
对应名为 Header
的断言。
断言的各种实现类
名称 | 参数(个数/类型) | 作用 |
---|---|---|
After | 1/datetime | 在指定时间之后 |
Before | 1/datetime | 在指定时间之前 |
Between | 2/datetime | 在指定时间区间内 |
Cookie | 2/string,regexp | 包含 cookie 名且必须匹配指定值 |
Header | 2/string,regexp | 包含请求头且必须匹配指定值 |
Host | N/string | 请求 host 必须是指定枚举值 |
Method | N/string | 请求方式必须是指定枚举值 |
Path | 2/List<String>,bool | 请求路径满足规则,是否匹配最后的 / |
Query | 2/string,regexp | 包含指定请求参数 |
RemoteAddr | 1/List<String> | 请求来源于指定网络域(CIDR写法) |
Weight | 2/string,int | 按指定权重负载均衡 |
XForwardedRemoteAddr | 1/List<String> | 从 X-Forwarded-For 请求头中解析请求来源,并判断是否来源于指定网络域 |
以Path和Query为例:
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
完整路径:http://localhost/search?q=haha
这里表示访问网关的 /search 地址,并且使用了名为 q 的请求参数,且值为 haha,才会将请求转到 https://cn.bing.com
3)自定义断言工厂
在上述规则的基础上,再指定一个名为 Vip
的断言规则,要求存在名为 user
的请求参数,并且值为 yzj
时才将请求跳转到 https://cn.bing.com
:
先编写一个 VipRoutePredicateFactory,继承 AbstractRoutePredicateFactory<C>,而AbstractRoutePredicateFactory<C>自动实现了 RoutePredicateFactory<C>:
@Component//记得加入ioc容器才能生效
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
/**
* 构造器
*/
public VipRoutePredicateFactory() {
super(Config.class);
}
/**
* 定义参数顺序(为短写法准备的)
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return List.of("param", "value");
}
/**
* 断言逻辑
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return (GatewayPredicate) serverWebExchange -> {
// localhost/search?q=haha&user=yzj
ServerHttpRequest request = serverWebExchange.getRequest();//拿到请求
String first = request.getQueryParams().getFirst(config.param);//取值
return StringUtils.hasText(first) && first.equals(config.value);//校验并返回
};
}
/**
* 自定义配置的参数
*/
@Validated
@Getter
@Setter
public static class Config {
@NotEmpty
private String param;
@NotEmpty
private String value;
}
}
示例配置文件:
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
- name: Vip
args:
param: user
value: yzj
完整路径:http://localhost/search?q=haha&user=yzj
这里表示访问网关的 /search 地址,并且使用了名为 q 的请求参数,且值为 haha,携带参数user,且值为yzj,才会将请求转到 https://cn.bing.com
4.4.过滤器
在请求被路由之前或之后,对请求和响应进行修改或执行特定逻辑(如添加/删除请求头、鉴权、日志记录、请求体修改、限流等)。
1)简单过滤器
需求:先前在网关中配置了将 /api/order/
开头的请求转到 service-order
服务,并要求在 service-order
服务中也存在 /api/order/
开头的请求路径,比如 /api/order/readDb
。如果该服务中原先并不存在 /api/order/
开头的请求,比如只有 /readDb
,那么在以 /api/order/readDb
进行访问就会出现 404 错误。
即输入 /api/order/readDb 能自动转化为 /readDb
解决办法:可以使用路径重写RewritePath 过滤器
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
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
2)默认过滤器
如果需要为所有路由都添加同一个过滤器,则可以使用 默认过滤器 :
spring:
cloud:
gateway:
default-filters:
# 为所有路由添加响应头过滤器
- AddResponseHeader=X-Response-Abc, 123
3)全局过滤器
除了默认过滤器,全局过滤器也能为所有匹配的路由添加一个过滤器,全局过滤器的配置无需修改配置文件。
实现 GlobalFilter
接口,并将实现类交由 Spring 管理,即可实现全局过滤器。还可以实现 Ordered
接口,调整多个全局过滤器的执行顺序。
/**
* 自定义全局过滤器
* 针对统计响应时间的过滤器
*/
@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;
}
}
4)自定义过滤器工厂
与自定义断言类似,自定义过滤器工厂的类名也有限制,要求以 GatewayFilterFactory
结尾,而配置文件中配置的名称就是类名开头。
比如需要在配置文件中定义名为 OnceToken
的过滤器,那么需要新增OnceTokenGatewayFilterFactory
/**
* 自定义过滤器
* 用来满足特定的需求
* 每次响应之前,添加一个一次性令牌,支持uuid、jwt等各种格式
*/
@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.全局跨域
跨域问题(Cross-Origin Resource Sharing, CORS)是浏览器出于安全考虑限制网页脚本访问不同源(协议、域名、端口任一不同)资源的行为。
如果需要配置跨域,可以在 Controller 的类上添加 @CrossOrigin
注解。但是有许多 Controller时,逐一添加注解太麻烦,可以在项目的配置类中添加 CorsFilter
类型的 Bean。
上述方法只适用于单体服务,那如果在微服务中呢?
借由 Gateway 的功能,可以在配置文件中轻松完成微服务的跨域配置:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: '*' #允许所有的跨域
allowed-headers: '*' #允许所有的头
allowedMethods: '*' #允许所有的请求方式
5.Seata
背景:
在微服务架构中,一个业务操作往往需要跨多个服务、操作多个独立的数据源(数据库),传统的本地事务无法覆盖全局。这就产生了分布式事务问题,核心挑战是如何保证所有参与的服务要么全部成功提交,要么全部失败回滚。
5.1.简介
Seata (Simple Extensible Autonomous Transaction Architecture) 是 Spring Cloud Alibaba 提供的一个分布式事务解决方案,用于解决微服务架构下的数据一致性问题。
三个核心角色:
- TC:Transaction Coordinator,即事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚;
- TM:Transaction Manager,即事务管理器。定义全局事务的范围,开始全局事务、提交或回滚全局事务;
- RM:Resource Manager,即资源管理器。管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
四种事务模式:
- AT (Automatic Transaction) 模式 (默认 & 推荐):通过代理数据源自动生成 SQL 快照 (
undo_log
) 实现自动回滚补偿。对业务代码零侵入。 - TCC (Try-Confirm-Cancel) 模式:一个业务操作需要拆分为三个接口(均由业务代码实现)。
- Saga 模式:将一个大事务拆分成一系列可补偿的小事务(子事务)。每个子事务都有对应的补偿操作。
- XA 模式:基于数据库原生支持的 XA 协议(两阶段提交 - 2PC)。Seata 的 RM 作为数据库 XA Resource Manager 的代理。
需求:
发起采购流程后,需要扣库存、生成订单、从账户中扣除指定金额,任一流程发生异常时,整个流程应当回滚。
5.2.原理
参考了Ai:
-
TM 开启全局事务: TM(标记了
@GlobalTransactional
的方法)向 TC 申请开启一个新的全局事务。TC 生成全局唯一的XID
并返回给 TM。 -
XID 传播:
XID
通过微服务间调用链(如 OpenFeign 请求头)传播到后续所有相关的微服务(RM)。 -
RM 注册分支事务 & 执行业务 SQL:
-
每个 RM 在执行本地数据库操作前,会向 TC 注册一个分支事务(绑定到当前
XID
)。 -
Seata 的 DataSourceProxy 会拦截业务 SQL 的执行:
-
执行前:查询并保存数据快照 (
before image
) 到undo_log
表。 -
执行业务 SQL。
-
执行后:查询并保存数据快照 (
after image
) 到undo_log
表。
-
-
RM 将本地事务的提交/回滚权交给 TC(本地事务会先提交,但此时全局事务未完成,数据对其他全局事务不可见 - 依赖全局锁)。
-
-
业务执行完成: TM 根据整体业务逻辑执行结果(是否有异常),向 TC 发起全局提交 (
Commit
) 或全局回滚 (Rollback
) 请求。 -
TC 驱动最终决议:
-
Commit:
-
TC 异步通知所有 RM 删除对应的
undo_log
记录(表示提交成功)。 -
释放该全局事务持有的所有全局锁。
-
-
Rollback:
-
TC 查找该全局事务下所有分支事务的
undo_log
记录。 -
TC 向每个 RM 发送回滚请求。
-
RM 收到回滚请求后:
-
根据
XID
和branch_id
查找undo_log
。 -
校验
after image
数据是否与当前数据库数据一致(防止“脏回滚”,即数据被其他事务修改)。如果不一致,记录告警,需要人工介入。 -
如果一致,则根据
before image
生成反向补偿 SQL 并执行,将数据恢复到业务执行前的状态。 -
删除
undo_log
。
-
-
TC 释放该全局事务持有的所有全局锁。
-
-
-
事务结束: TC 更新全局事务状态为已完成。
5.3.整合Seata
1)下载并启动Seata
下载2.1版本Seata-Server版本历史 | Apache Seata,解压 Seata 后,进入 bin
目录,使用 命令启动 Seata。
seata-server.bat
访问:http://127.0.0.1:7091/#/transaction/list
账号密码都是seata
2)添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3)配置文件 file.conf
要使用seata必须先连上seata服务器,在需要使用 Seata 的模块中添加 Seata 的配置文件 file.conf
:
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
4)开启事务
最后在最顶端的方法入口上使用 @GlobalTransactional
注解,由此开启全局事务。
举例:
@Override
@GlobalTransactional //开启全局事务
public void purchase(String userId, String commodityCode, int orderCount) {
// 1. 扣减库存
storageFeignClient.deduct(commodityCode, orderCount);
// 2. 创建订单
orderFeignClient.create(userId, commodityCode, orderCount);
}
本文到此结束,如果对你有帮助,可以点个赞~