【自学笔记】快速入门 SpringCloud

SpringCloud快速入门自学笔记

目录

前言

1.Nacos

1.1.简介

1.2.服务注册

1.3.服务发现

1.4.远程调用

1.5.负载均衡

1.6.配置中心

1.7.数据隔离

1.8.常见面试题

2.OpenFeign

2.1.简介

2.2.简单使用

2.3.日志

2.4.超时控制

2.5.重试机制

2.6.拦截器

2.7.Fallback兜底返回

2.8.常见面试题

3.Sentinel

3.1.简介

3.2.原理

3.3.整合Sentinel

3.4.异常处理

3.5.流控规则

3.6.熔断规则

3.7.热点规则

4.Gateway

4.1.简介

4.2.路由

4.3.断言

4.4.过滤器

4.5.全局跨域

5.Seata

5.1.简介

5.2.原理

5.3.整合Seata


前言

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 服务器。

  1. 引入相关依赖:
    <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>
  2. 配置nacos地址
    spring:
      cloud:
        nacos:
          # 配置 Nacos 地址
          server-addr: 127.0.0.1:8848
  3. 启动项目,访问 http://localhost:8848/nacos/
  4. 测试集群模式:单机情况下通过改变端口号模拟微服务集群,例如添加 Program arguments 信息为
    --server.port=8001
    --server.port=8002 
    --server.port=8003  

1.3.服务发现

服务消费者(客户端)可以通过 Nacos 查询并获取它需要调用的服务的可用实例列表(提供者地址)。

  1. 开启服务发现,在主启动类上添加 @EnableDiscoveryClient 注解

  2. 提供两款 API 的服务发现功能:DiscoveryClientNacosServiceDiscovery。前者为 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.路由

根据预定义的规则,将请求转发到后端的微服务实例。

需求:

  1. 客户端发送 /api/order/** 转到 service-order

  2. 客户端发送 /api/product/** 转到 service-product

  3. 以上转发有负载均衡效果

示例配置文件:

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 的断言。

断言的各种实现类

名称参数(个数/类型)作用
After1/datetime在指定时间之后
Before1/datetime在指定时间之前
Between2/datetime在指定时间区间内
Cookie2/string,regexp包含 cookie 名且必须匹配指定值
Header2/string,regexp包含请求头且必须匹配指定值
HostN/string请求 host 必须是指定枚举值
MethodN/string请求方式必须是指定枚举值
Path2/List<String>,bool请求路径满足规则,是否匹配最后的 /
Query2/string,regexp包含指定请求参数
RemoteAddr1/List<String>请求来源于指定网络域(CIDR写法)
Weight2/string,int按指定权重负载均衡
XForwardedRemoteAddr1/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:

  1. TM 开启全局事务: TM(标记了 @GlobalTransactional 的方法)向 TC 申请开启一个新的全局事务。TC 生成全局唯一的 XID 并返回给 TM。

  2. XID 传播: XID 通过微服务间调用链(如 OpenFeign 请求头)传播到后续所有相关的微服务(RM)。

  3. RM 注册分支事务 & 执行业务 SQL:

    • 每个 RM 在执行本地数据库操作前,会向 TC 注册一个分支事务(绑定到当前 XID)。

    • Seata 的 DataSourceProxy 会拦截业务 SQL 的执行:

      • 执行前:查询并保存数据快照 (before image) 到 undo_log 表。

      • 执行业务 SQL。

      • 执行后:查询并保存数据快照 (after image) 到 undo_log 表。

    • RM 将本地事务的提交/回滚权交给 TC(本地事务会先提交,但此时全局事务未完成,数据对其他全局事务不可见 - 依赖全局锁)。

  4. 业务执行完成: TM 根据整体业务逻辑执行结果(是否有异常),向 TC 发起全局提交 (Commit) 或全局回滚 (Rollback) 请求。

  5. 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 释放该全局事务持有的所有全局锁。

  6. 事务结束: 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);
}

本文到此结束,如果对你有帮助,可以点个赞~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yuniko-n

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值