当前位置: 首页 > news >正文

Spring Cloud『学习笔记』

Zero、前言


主要学习与使用框架:

  • Nacos(Spring Cloud Alibaba)注册中心
  • OpenFeign(Spring Cloud)远程调用
  • Sentinel(Spring Cloud Alibaba)服务保护
  • Gateway(Spring Cloud)网关
  • Seata(Spring Cloud Alibaba)分布式事务

在这里插入图片描述
在这里插入图片描述

  • 单体架构:

    • 优点:开发与部署都相对简单
    • 缺点:无法应对高并发
  • 集群架构:同样的应用部署到多台服务器上(副本),通过一台部署了网关的服务器,去决定让哪个服务器处理。

    • 网关:目前主流的是 ngnix,用它去进行转发。利用 负载均衡的算法,去决定当前应该转发给哪个服务器。
    • 扩容:这个专业术语的意思通常指的是,在当前的集群架构体量下,再多弄些副本。
    • 优点:解决了 高并发问题。
    • 缺点:
      • 模块化更新代码,会导致 牵一发而动全身。
      • 多语言协作开发时,怎么互相通讯就是个问题了。
  • 分布式架构:将一个项目的多个功能,划分为各个模块(也被称为微服务)。然后单独打包在服务器上。甚至是 数据库,也可以按照功能模块,去进行划分。(商品库、订单库、用户库 等等)

    • 优点:每个微服务是独立的,数据隔离、语言无关。
    • 缺点:要想各个微服务之间进行通讯,那么就需要 远程调用(RPC),并且需要知道这个服务在哪台服务器上,这都是对效率的妥协。
    • 注册中心:如果你想知道某个服务在哪台服务器上,就必须先把这个服务的一些关键信息存储起来。而如果这样的服务是集群部署的,那么还需要负载均衡算法,决定转发到哪一台服务器。注册中心就是为了解决这样的问题诞生的。
      • 服务发现:注册中心会查看注册服务的表,看是否有你需要的服务,如果有,会通过负载均衡,转发你这个请求。
      • 服务注册:微服务必须告诉注册中心,我在某个服务器上,并且我是什么服务。
    • 配置中心:这一块功能,基本集成在注册中心里面。它是为了解决某个微服务需要热更新配置文件的需求。即,微服务会先把自己的配置注册到配置中心,然后这个服务的配置文件更改后,配置中心会推送到每个该服务的副本,进行热更新!
  • 服务器雪崩现象:若某个微服务突然卡顿,此时还处于高并发状态。那么就会导致整个调用链卡顿,慢慢的耗尽资源。最后服务器宕机!

    • 服务器熔断机制:我发现你这边卡顿了,那么我就快速返回。我不去等你。比如我配置了,五秒内百分之五十的请求都没收到返回。那么其实就算是卡顿情况了。此时我会全部快速返回错误信息。也可以说叫 快速失败…

此时需要的技术:

  • 微服务:SpringBoot
  • 注册、配置中心:Spring Cloud Alibaba Nacos
  • 网关:Spring Cloud Gateway
  • 远程调用:Spring Cloud OpenFeign
  • 服务熔断:Spring Cloud Alibaba Sentinel
  • 分布式事务(用来处理分布式数据库事务问题):Spring Cloud Alibaba Seata

一、搭建微服务项目

1.1 搭建项目过程图

在这里插入图片描述

让其只保留 pom.xml

在这里插入图片描述

新建 Services 总服务模块(所有微服务模块的父级模块)

在这里插入图片描述
在这里插入图片描述
新建 model 模块(所有模块的实体类存放模块)

在这里插入图片描述
在这里插入图片描述

  • Order.java
package top.muquanyu.bean;import lombok.Data;import java.math.BigDecimal;
import java.util.List;@Data
public class Order {private Long id;private BigDecimal totalAmount;private Long userId;private String nickName;private String address;private List<Product> productList;
}
  • Product.java
package top.muquanyu.bean;import lombok.Data;import java.math.BigDecimal;@Data
public class Product {private Long id;private BigDecimal price;private String productName;private int num;
}

1.2 pom.xml

  • cloud-demo
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version> <!-- spring-boot 版本 --><relativePath/> <!-- lookup parent from repository --></parent><modules><module>services</module><module>model</module></modules><modelVersion>4.0.0</modelVersion><groupId>top.muquanyu</groupId><artifactId>cloud-demo</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-cloud.version>2023.0.3</spring-cloud.version> <!-- spring-cloud 版本 --><spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version> <!-- spring-cloud-alibaba 版本 --></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
  • model
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>top.muquanyu</groupId><artifactId>cloud-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>model</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
</project>
  • services
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>top.muquanyu</groupId><artifactId>cloud-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>services</artifactId><packaging>pom</packaging><modules><module>service-product</module><module>service-order</module></modules> <!-- 打包方式 --><dependencies><!--服务发现--><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><!--负载均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>
  • service-product / service-order
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>top.muquanyu</groupId><artifactId>services</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>service-product</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>top.muquanyu</groupId><artifactId>model</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency></dependencies></project>

二、Nacos


Nacos 是一个开源的动态服务发现、配置和服务管理平台,由阿里巴巴开发并开源,现属于 Spring Cloud Alibaba 生态的核心组件之一。它的名字是 Naming and Configuration Service 的缩写,主要解决微服务架构中的两大核心问题:服务注册与发现 和 动态配置管理。

  • 服务注册与发现

    • 服务提供者启动时将自己的信息(如IP、端口、健康状态)注册到 Nacos。
    • 服务消费者通过 Nacos 查询可用服务列表,实现负载均衡和动态路由。
    • 支持健康检查,自动剔除故障节点,保证服务高可用。
  • 命名空间(Namespace)与分组(Group)

    • 通过命名空间实现多租户隔离,分组用于逻辑区分不同服务或环境(如 DEV/TEST/PROD)。
  • 动态配置管理(Dynamic Configuration)

    • 集中管理微服务的配置(如数据库连接、开关参数),支持多环境(开发、测试、生产)。
    • 配置变更时实时推送到服务,无需重启应用。
    • 支持配置版本管理、灰度发布和回滚。
  • 服务治理

    • 提供服务的元数据管理、权重调整、流量路由等功能。
    • 支持与 Spring Cloud、Dubbo、Kubernetes 等生态集成。

2.1 下载安装 Nacos与远程调用

nacos 在windows和linux环境下的安装及启动教程(单机版nacos)by 作者(onecyl)

这里建议下载安装 2.4.3

  • 只要在某个微服务模块 resources/application.yaml 配置如下内容,就可以让 Nacos 注册该服务。
    在这里插入图片描述
spring:application:name: service-productcloud:nacos:discovery:server-addr: 127.0.0.1:8848 # nacos 服务地址
server:port: 9002
  • 如果想发现其它服务(则还需要启动类处使用 @EnableDiscoveryClient
package top.muquanyu;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@EnableDiscoveryClient // 开启服务发现功能
@SpringBootApplication
public class ProductMainApplication {public static void main(String[] args) {SpringApplication.run(ProductMainApplication.class, args);}
}
  • OrderServiceImpl(主要是这里怎么远程调用的)
package top.muquanyu.service.impl;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import top.muquanyu.bean.Order;
import top.muquanyu.bean.Product;
import top.muquanyu.service.OrderService;import java.math.BigDecimal;
import java.util.List;@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {private final RestTemplate restTemplate;private final DiscoveryClient discoveryClient;private final LoadBalancerClient loadBalancerClient;@Overridepublic Order createOrder(Long userId, Long productId) {Order order = new Order();order.setId(1L);//TODO 总金额order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("张三");order.setAddress("火星");//TODO 远程查询商品列表order.setProductList(List.of(getProductFromRemoteByAnnotion(productId)));return order;}// 远程调用获取商品信息public Product getProductFromRemote(Long productId) {ServiceInstance instance = loadBalancerClient.choose("service-product");String url = instance.getUri() + "/product/" +  productId;log.info("远程请求:{}", url);Product product = restTemplate.getForObject(url, Product.class);return product;}// 远程调用获取商品信息public Product getProductFromRemoteByAnnotion(Long productId) {// 这里的 service-product 是服务名,它远程调用时 会自动替换为正确的 字符串String url = "http://service-product/product/" +  productId;log.info("远程请求:{}", url);Product product = restTemplate.getForObject(url, Product.class);return product;}
}

思考:注册中心宕机,远程调用还能成功吗?

若已经远程调用过相关的目标地址,那么就会有缓存。此时哪怕Nacos服务宕机,也不会影响该目标地址的远程调用。但如果目标服务宕机或压根没有缓存,那么肯定远程调不通。

2.2 配置中心

<!--配置中心 放到 servces\pom.xml 下-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

若某个微服务模块暂未用到Nacos配置中心,则需要关闭配置检查。

spring:application:name: service-ordercloud:nacos:discovery:server-addr: 127.0.0.1:8848 # nacos 服务地址config:import-check:enabled: false # 关闭配置检查
server:port: 9001

2.2.1 使用 Nacos 远程的配置文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

spring:config:import: nacos:service-order.yaml # 导入 nacos 中这个配置application:name: service-ordercloud:nacos:discovery:server-addr: 127.0.0.1:8848 # nacos 服务地址server:port: 9001

你会惊奇的发现,Nacos 的配置要优先于我们本地写好的。它会覆盖掉!那么多个 Nacos 配置呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们会发现,越靠后的配置,越能覆盖掉前面的 Nacos 配置!

2.2.2 获取到配置文件的值

  • @Value@RefreshScope // 若配置文件内的值刷新,则也刷新 @Value 绑定的变量
package top.muquanyu.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.muquanyu.bean.Order;
import top.muquanyu.service.OrderService;@RefreshScope // 若配置文件内的值刷新,则也刷新 @Value 绑定的变量
@RestController
public class OrderController {@AutowiredOrderService orderService;@Value("${server.port}")String port;@GetMapping("/config")public String config(){return port;}@GetMapping(value = "/create")public Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {Order order = orderService.createOrder(userId, productId);return order;}
}
  • @ConfigurationProperties(prefix = "order") 自动刷新与装配配置文件中指定前缀的的数据集
package top.muquanyu.properties;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component // 
@ConfigurationProperties(prefix = "server")
@Data
public class OrderProperties {String port;
}
  • NacosConfigManager 编码方式获取到变量值(一般都用于配置文件变更触发后的逻辑)
package top.muquanyu;import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;import java.util.concurrent.Executor;
import java.util.concurrent.Executors;@EnableDiscoveryClient // 开启服务发现功能
@SpringBootApplication
public class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}@BeanApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager){ // 启动一个后台任务return args -> {ConfigService configService = nacosConfigManager.getConfigService();configService.addListener("service-order", "DEFAULT_GROUP",new Listener() {@Overridepublic Executor getExecutor() { // 监听器的监听任务是开启线程去监听的(很合理)return Executors.newFixedThreadPool(4);}@Overridepublic void receiveConfigInfo(String s) { // 接收所有变化的配置信息System.out.println("变化的配置信息" + s);}}); // 对某个 Nacos 配置文件添加监听};}
}

2.3 数据隔离

  • 如果项目有多个环境:dev、test、prod
    • 每个微服务,同一种配置,在每套环境的值可能是不一样的。(database.properties、common.properties)
    • 项目可以通过切换环境,加载本环境的配置
  • 难点
    • 区分多套环境
    • 区分多种微服务
    • 区分多种配置
    • 按需加载配置

在这里插入图片描述
在这里插入图片描述

建立三个环境(Nacos 命名空间)

在这里插入图片描述

然后你就可以在不同的环境下,建立每个组下面的多个配置文件了。

在这里插入图片描述

  • 真实使用情况

在这里插入图片描述

通常我们会建立三个配置文件,以便于结构化清晰。

在这里插入图片描述

spring:config:import:- nacos:common.yaml?ORDER_GROUP # ?ORDER_GROUP 确认是哪个分组application:name: service-ordercloud:nacos:discovery:server-addr: 127.0.0.1:8848 # nacos 服务地址config:namespace: dev # dev 命名空间

java -jar xxx.jar --spring.profiles.active=prod 此方式就可以让其指定为某个配置文件激活!

2.4 数据库更换为MySQL

  • 创建 nacos 数据库
    在这里插入图片描述
  • 找到 nacos/conf/application.properties
    在这里插入图片描述
  • 书写数据库配置
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123123

在这里插入图片描述

  • 用 mysql-schema.sql 脚本文件初始化数据库

在这里插入图片描述
在这里插入图片描述

此时重新启动 Nacos 服务即可!当你发现 你以前的操作都没有了,那就证明数据库更换成功了!

在这里插入图片描述

三、OpenFeign

OpenFeign 是声明式的 REST 客户端,区别于 RestTemplate(编程式)

  • 注解驱动
    • 指定远程地址 @FeignClient
    • 指定请求方式 @GetMapping、@PostMapping、@DeleteMapping ……
    • 指定携带数据 @RequestHeader、@RequestParam、@RequestBody ……
    • 指定返回结果 响应模型(可自定义)
		<!--远程调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

3.1 自有微服务与三方接口

3.1.1 客户端负载均衡与服务端负载均衡的区别

  • 客户端负载均衡:指的是发起远程调用方去利用负载均衡算法,选出合适的目标地址,我们称为客户端负载均衡。
  • 服务端负载均衡:指的是有一个专门做负载均衡的服务器(网关),远程调用方直接可以发送请求给这个网关,网关帮我们选出合适的目标地址,去进行请求转发。拿到数据后再返给我们。三方接口,一般都是服务端负载均衡的。

有些时候,客户端、服务端,都会做负载均衡。可以理解为客户端可能负载均衡不同的网关。

3.2 配置日志

日志是非常重要的,特别是在远程调用其它微服务模块的时候。我们希望在调用方能够看到一些关于远程调用的日志信息。

logging:level:top.muquanyu.feign: debug # 配置 top.muquanyu.feign 包下日志的输出级别
package top.muquanyu.config;import feign.Logger;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;@Configuration
public class OrderConfig {@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL; // 配置 feign 日志全记录组件}@LoadBalanced@Beanpublic RestTemplate productService(){return new RestTemplate();}
}

在这里插入图片描述
此时你就会发现,Feign 就可以打印很详细的日志了。

3.3 超时控制

  • 服务宕机(一直连接不上 connectTimeout
  • API速度慢(读取不到结果 readTimeout

这会导致 服务雪崩的问题!也就是一大半的服务都处于等待状态。。。高并发下,就会耗尽服务器的所有资源。

最简单的解决方案:为发送远程调用服务加入 限时等待(未超时 => 返回正确结果 / 超时 => 中断这次远程调用)

若超时,可以返回 自定义错误信息,也可以返回 兜底数据

  • application-feign.yaml
spring:cloud:openfeign:client:config:default: # 默认配置(针对于所有服务都生效)logger-level: full # 输出日志等级connect-timeout: 2000 # 连接超时限制read-timeout: 3000 # 读取返回超时限制
#          service-product: # 针对于 service-product 服务
#            logger-level: full # 输出日志等级
#            connect-timeout: 3000 # 连接超时限制
#            read-timeout: 5000 # 读取返回超时限制
  • application.yaml

server:port: 9001spring:application:name: service-orderprofiles:active: dev # dev / prod / testinclude: feign

你会发现它无论是读取超时还是连接超时,都是抛出异常的。所以我们如果想要自定义这种错误的返回信息,就要把异常拦截下来。然后去return返回自定义的错误信息。只不过后面的话,我们可能会用到 Sentinel

在这里插入图片描述
在这里插入图片描述

  • GlobalExceptionHandler.java
package top.muquanyu.handler;import feign.FeignException;
import feign.RetryableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.net.SocketTimeoutException;
import java.rmi.ConnectException;@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(RetryableException.class)public String retryableExceptionHandler(RetryableException  e) {if (e.getCause() instanceof SocketTimeoutException) {return "读取超时:请稍后重试";}if (e.getCause() instanceof ConnectException) {return "连接超时:请检查服务是否启动";}return "服务请求失败:" + e.getMessage();}@ExceptionHandler(FeignException.class)public String handleFeignException(FeignException e) {return "远程服务异常:" + e.getMessage();}@ExceptionHandler(Exception.class)public String handleOther(Exception e) {return "系统错误:" + e.getMessage();}
}

在这里插入图片描述

3.4 重试机制

package top.muquanyu.config;import feign.FeignException;
import feign.Logger;
import feign.Response;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;@Configuration
public class OrderConfig {// 配置重试机制(100ms 后发第二次、100 * 1.5 发第三次// 150 * 1.5 发第四次、225 * 1.5 发第五次)@BeanRetryer retryer() {// 默认是 第二次按照 100ms 后发送 总共 发五次return new Retryer.Default();// 参数说明:初始间隔(ms)、最大间隔(ms)、最大重试次数// return new Retryer.Default(100, 1000, 3);}@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL; // 配置 feign 日志全记录组件}@LoadBalanced@Beanpublic RestTemplate productService(){return new RestTemplate();}
}

3.5 拦截器

Feign 这里分为请求拦截器响应拦截器

  • 请求拦截器:常用于请求定制修改(比如统一放置 token 令牌
  • 响应拦截器:用作响应的预处理(但几乎用不到,毕竟都是协调好的数据结构).

可以是 yaml 配置文件的方式,让其拦截器只作用于一个模块。

spring:cloud:openfeign:client:config:default: # 默认配置(针对于所有服务都生效)logger-level: full # 输出日志等级connect-timeout: 2000 # 连接超时限制read-timeout: 3000 # 读取返回超时限制service-product: # 针对于 service-product 服务logger-level: full # 输出日志等级connect-timeout: 3000 # 连接超时限制read-timeout: 5000 # 读取返回超时限制request-interceptors:- top.muquanyu.interceptor.XTokenRequestInterceptor # 但这样写只能生效给着一个模块(前提是拦截器并未注册到 IOC 容器中)

也可以是直接让拦截器注册到 IOC 容器中,适配于所有的模块,即都要走这个拦截器。

在这里插入图片描述

  • XTokenRequestInterceptor.java
package top.muquanyu.interceptor;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;import java.util.UUID;@Component // 只要是注册到 IOC 容器中,那么每次远程调用之前,都会走 请求拦截器,不管你是什么Client
public class XTokenRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header("X-Token", UUID.randomUUID().toString()); // 比如这样 ~}
}

3.6 Fallback 兜底返回

此功能需要整合 Sentinel 才能得以实现。

兜底返回:默认数据/假数据/缓存数据

		<!-- 流量控制框架 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
  • feign/fallback/ProductFeignClientFallback.java
package top.muquanyu.feign.fallback;import top.muquanyu.bean.Product;
import top.muquanyu.feign.ProductFeignClient;public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {Product product = new Product();return product;}
}
  • ProductFeignClient.java
package top.muquanyu.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import top.muquanyu.bean.Product;
import top.muquanyu.feign.fallback.ProductFeignClientFallback;// 标注好微服务名就行(OpenFeign 集成了负载均衡,此负载均衡也可以自定义配置类)
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class) // 说明是发送远程调用的客户端
public interface ProductFeignClient {@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id/*,@RequestHeader("token") String token*/);
}
  • application-feign.yaml
spring:cloud:openfeign:client:config:default: # 默认配置(针对于所有服务都生效)logger-level: full # 输出日志等级connect-timeout: 2000 # 连接超时限制read-timeout: 3000 # 读取返回超时限制service-product: # 针对于 service-product 服务logger-level: full # 输出日志等级connect-timeout: 3000 # 连接超时限制read-timeout: 5000 # 读取返回超时限制request-interceptors:- top.muquanyu.interceptor.XTokenRequestInterceptor # 但这样写只能生效给着一个模块(前提是拦截器并未注册到 IOC 容器中)feign:sentinel:enabled: true # 开启 sentinel

这里的 GlobalExceptionHandler 不会与 Fallback 冲突。这是因为 GlobalExceptionHandler 必须得捕获到抛出的异常,而Fallback是直接把异常处理掉了,再给你返回兜底数据的。

在这里插入图片描述
在这里插入图片描述

四、Sentinel

  • 定义资源

    • 自动适配主流web框架的接口资源
    • 编程式:SphU API
    • 声明式:@SentinelResource
  • 定义规则

    • 流量控制(FlowRule 比如 “每秒通过100个请求”)
    • 熔断降级(DegradeRule 防止雪崩。最常见且简单的做法就是一旦访问失败,去走Fallback 兜底回调)
    • 系统保护(SystemRule 观察CPU、内存占用,去采取限制请求数量)
    • 来源访问控制(AuthorityRule 比如上游可以访问下游的资源)
    • 热点参数(ParamFlowRule)

4.1 sentinel-dashboard、异常机制

下载地址

java -jar sentinel-dashboard-1.8.8.jar --server.port=9999

在这里插入图片描述

spring:cloud:sentinel:transport:dashboard: localhost:9999eager: true # 提前加载(默认是懒加载服务,即只有远程调用了,才会被检测到。然后被加载)openfeign:client:config:default: # 默认配置(针对于所有服务都生效)logger-level: full # 输出日志等级connect-timeout: 2000 # 连接超时限制read-timeout: 3000 # 读取返回超时限制
#          service-product: # 针对于 service-product 服务
#            logger-level: full # 输出日志等级
#            connect-timeout: 3000 # 连接超时限制
#            read-timeout: 5000 # 读取返回超时限制
#            request-interceptors:
#              - top.muquanyu.interceptor.XTokenRequestInterceptor # 但这样写只能生效给着一个模块(前提是拦截器并未注册到 IOC 容器中)feign:sentinel:enabled: true # 开启 sentinel

在这里插入图片描述
在这里插入图片描述

我们的资源,哪怕是注册了。我们必须也得调用相关资源,才能在这里被检测到。

在这里插入图片描述
在这里插入图片描述
以下是对资源的四个控制手段。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • MyBlockExceptionHandler.java(如果我们想要 BlockException 的返回信息自定义,那么就得自己写一个 实现 BlockExceptionHandler 的类)
    在这里插入图片描述
package top.muquanyu.handler;import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;import java.io.PrintWriter;@Component // 注册到 IOC 容器,就会覆盖默认的那个
public class MyBlockExceptionHandler implements BlockExceptionHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {httpServletResponse.setContentType("application/json;charset=utf-8");PrintWriter printWriter = httpServletResponse.getWriter();printWriter.write("{code: 500, msg: \"你访问的太快了\", data: null}");printWriter.flush();printWriter.close();}
}

在这里插入图片描述

  • 兜底返回(Sentinel 比较提倡用回调方法的形式去返回兜底数据)也恰好与 OpenFeign 兜底数据互相隔离开了
@Override@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback") // blockHandler 指定一个 fallback 回调方法public Order createOrder(Long userId, Long productId) {Order order = new Order();order.setId(1L);//TODO 总金额order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("张三");order.setAddress("火星");//TODO 远程查询商品列表order.setProductList(List.of(getProductFromRemoteByOpenFeign(productId)));return order;}// 兜底回调方法public Order createOrderFallback(Long userId, Long productId, BlockException ex) {Order order = new Order();order.setId(1L);order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("<UNK>");order.setAddress("异常信息" + ex.getClass());order.setProductList(List.of());return order;}

在这里插入图片描述
注意:兜底回调的优先级是要高于 MyBlockExceptionHandler.java

4.2 流控规则

流量控制:俗称 节流(限制多余请求,从而保护系统资源不被耗尽)

  • 针对来源:default(任何 客户端 IP)
  • 阈值类型:QPS(每秒多少次)、并发线程数(要配合线程池,统计线程数量来判断每秒多少次,所以性能不强)
  • 集群阈值模式:单机均摊(每台机器的服务都最多每秒n次)、总体阈值(只要每台机器每秒访问的次数总和是 <= n 就可以)

4.2.1 流控模式

    • 直接(直接针对某个资源进行限制)
    • 链路(表面上控制的是资源B,但实际上它会让你确认是通往资源B的哪条链路)比如秒杀创建订单这条链路才会被限制,而创建普通订单无需被限制。
      在这里插入图片描述
// 下面两个请求,就属于是都调用了 createOrder 资源,但它们不是同一个链路。因为走的是两个请求。@GetMapping(value = "/create")public Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {Order order = orderService.createOrder(userId, productId);return order;}@GetMapping(value = "/seckill")public Order createSeckillOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {Order order = orderService.createOrder(userId, productId);return order;}

在这里插入图片描述

在这里插入图片描述

    • 关联:当我们想要某个目标资源,关联的其它资源请求超出了阈值。才会被限流时,我们就要用这个模式。比如关联资源是 资源B,那么当资源B 访问阈值超出的时候,我们的目标资源,才会限流。

4.2.2 流控效果

  • 快速失败:直接抛出 BlockExcption异常,可以兜底回调或者自定义BlockExcptionHandler
  • Warm Up(预热冷启动、不支持 关联与链路):
    • QPS:最终每秒多少次请求
    • Period:预热时间
    • 比如 QPS = 3、Period = 3:第一秒,只让通过一个请求。第二秒,可以通过两个、第三秒可以通过三个。然后以后都是每秒通过三个!
  • 排队等待(不支持 QPS > 1000、不支持 关联与链路):
    • QPS:每秒多少次请求
    • timeout:最多可以等待多少秒
    • 比如说 QPS = 2、timeout = 20s:那么一瞬间可能也就进来最多 40 次请求,因为40次,就相当于 20s 的等待了。多余的请求会以排队失败为由被抛弃掉。

4.3 熔断规则

在微服务项目中,有一种情况是必须要有解决方案的。那就是如果某个服务宕机或者返回数据超慢,就会影响服务雪崩效应。此时应该及时切掉这些不稳定的调用(一旦确定某个服务有问题,那么我们就在调用处快速返回,使其不积压。) —— 这被称之为 熔断降级,而且都是在 调用端 进行配置的。

但如果目标服务哪天又恢复正常了呢?
答:Sentinel 提供的 断路器,支持 半开,也就是每次调用都会真正的发起一次请求。来判断B服务是否ok?

  • statIntervalMs(统计时长):就是比如你是慢调用比例,我们肯定要去统计有多少是慢请求,那么多久时间统计一次呢?就拿这个参数来设置。
  • minRequestAmount(最小请求数):在统计时长内,如果请求数量都没有达到最低数量的时候,其实我们就没必要进行熔断。因为请求数太小。
  • 慢调用比例:我觉得目标服务返回数据太慢了,那么我就不想调用你了,采取熔断。比如 0.7,即 百分之70的请求是慢的,那么我就认为你是 慢调用!这里肯定会让你设置一个阈值,来判断是不是慢的。
    在这里插入图片描述

这里的 最大 RT 就是用来判断慢调用的

  • 异常比例:远程服务出现异常也是常见的,我们可以设定若远程服务异常占总请求比例达到了设定值,那么就认定你这个服务方法是有点儿问题的。最后我们再采取熔断!
  • 异常数:也可以直接设定异常的数目,来判定你的服务方法有问题。最后我们再采取熔断!
  • timeWindow 熔断时长:开启熔断后,肯定不能一直熔断。我们会设定一个熔断的时长。等待熔断结束后,断路器会变为 Half-Open 半开状态,放行一个请求去探测目标服务是否ok?

PS:熔断后,会直接走兜底回调方法。

4.4 热点规则

热点规则是对资源流控,进行更加细致化的判断。也就是某个参数!它会先判断你是不是带有某个热点参数的请求,如果是。我们才会走规则,进行流控。仅支持 QPS 模式

  • 参数索引:从0 开始是第一个参数

在这里插入图片描述
它的高级选项,是来判断你这个热点参数传过来什么样的值,然后有特殊的限流阈值!


4.5 调用方黑白名单

在这里插入图片描述
这个很好理解,是为某个远程资源,添加黑白名单的。

4.6 持久化

目前主流的方式:sentinel + nacos 双向通讯(即用 nacos 做持久化)

【基于Sentinel1.8.8持久化流控规则和熔断降级规则到Nacos2.3.0】(作者:Of Chen)

五、Gateway

  • 统一入口
  • 请求路由
  • 负载均衡:针对于服务外部的负载均衡(比如说订单服务有三个,那么Gateway是用来负载均衡它的。当确认订单服务后,会使用Nacos服务内负载均衡来进行服务之间远程调用的负载均衡。)
  • 流量控制
  • 身份认证
  • 协议转换
  • 系统监控
  • 安全防护

5.1 创建网关

在这里插入图片描述

  • application.yaml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>top.muquanyu</groupId><artifactId>cloud-demo</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>gateway</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- nacos 注册中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies>
</project>
  • application.yaml
spring:application:name: gatewaycloud:nacos:server-addr: 127.0.0.1:8848profiles:include: routeserver:port: 8000 # 网关端口号一般都是 80
  • application-route.yaml(我们可以通过配置文件来设置 各个服务的路由、包括简单的负载均衡)
spring:cloud:gateway:routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-order- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-product

这里注意:gateway 在判断完 /api/order/** 之后,会把 /api/order/** 转发到目标服务。所以目标服务如果没有这种url的接口,就会报 404 找不到错误。

在这里插入图片描述
在这里插入图片描述
所以,我们一定要记得把这两处地方做合适的更改。

在这里插入图片描述
路由匹配顺序:从上到下,如果在上面的已经匹配到。那么就不会让下面的路由匹配。

其实我们上述写的是短写法,下面是长写法:

        # 长写法- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- name: Pathargs:patterns: /api/order/**matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/

5.2 断言

中文官方文档

  • 自定义断言工厂
    在这里插入图片描述
package top.muquanyu.predicate;import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {public VipRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return new GatewayPredicate() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {ServerHttpRequest request = serverWebExchange.getRequest();String first = request.getQueryParams().getFirst(config.param);return StringUtils.hasText(first) && first.equals(config.value);}};}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("param", "value");}@Validated@Datapublic static class Config {@NotEmptyprivate String param;private String value;}
}
spring:cloud:gateway:routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-order- name: Vip # 长写法args:param: uservalue: mqy- Vip=user,mqy # 短写法# 长写法
#        - id: order-route # 订单服务的路由
#          uri: lb://service-order # lb 是负载均衡的转发给某服务的意思
#          predicates: # 断言
#            - name: Path
#              args:
#                patterns: /api/order/**
#                matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-product

5.3 过滤器

每个请求在进行转发之前,可以使用多个过滤器。它会陆续经过多个过滤器的前置方法,到达目标服务。然后将返回结果陆续经过这些过滤器的后置方法,最后返回给请求方!

5.3.1 路径重写(rewritePath)

这个过滤工厂解决了一个很大的麻烦。那就是之前我们发现,如果 /api/order/** 则必须也把目标服务的 controller 改请求路径。这样做,会有两个问题。第一个是比较麻烦、第二个则是不一定这个服务内所有的请求都统一前缀为 /api/order/**

而路径重写,可以将我们寻找的目标请求url 进一步改写。从 /api/order/create => /create

spring:cloud:gateway:routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-orderfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /create- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 添加响应头
#            - name: Vip # 长写法
#              args:
#                param: user
#                value: mqy
#            - Vip=user,mqy # 短写法# 长写法
#        - id: order-route # 订单服务的路由
#          uri: lb://service-order # lb 是负载均衡的转发给某服务的意思
#          predicates: # 断言
#            - name: Path
#              args:
#                patterns: /api/order/**
#                matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-productfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /create

5.3.2 默认过滤器、全局Filter

若任何服务,都要走一个过滤器。那么就可以设置默认过滤器!

spring:cloud:gateway:routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-orderfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /create- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 添加响应头
#            - name: Vip # 长写法
#              args:
#                param: user
#                value: mqy
#            - Vip=user,mqy # 短写法# 长写法
#        - id: order-route # 订单服务的路由
#          uri: lb://service-order # lb 是负载均衡的转发给某服务的意思
#          predicates: # 断言
#            - name: Path
#              args:
#                patterns: /api/order/**
#                matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-productfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /createdefault-filters:- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 统一添加响应头

但这样可能处理的有点儿太粗糙了。。。正常来说应该写一个类,去处理复杂的逻辑。由此 全局Filter被引入了

在这里插入图片描述

package top.muquanyu.filter;import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.util.UUID;public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 由于是在响应头加东西,所以要先放行,也就是chain.filter(exchange)chain.filter(exchange).then(Mono.fromRunnable(() -> {ServerHttpResponse response = exchange.getResponse();String val = config.getValue();if ("uuid".equalsIgnoreCase(val)) {val = UUID.randomUUID().toString();}response.getHeaders().add(config.getName(), val);}));return null;}};}
}
spring:cloud:gateway:routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-orderfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /create- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 添加响应头- OnceToken=X-Response-Token, uuid # 自定义的过滤器
#            - name: Vip # 长写法
#              args:
#                param: user
#                value: mqy
#            - Vip=user,mqy # 短写法# 长写法
#        - id: order-route # 订单服务的路由
#          uri: lb://service-order # lb 是负载均衡的转发给某服务的意思
#          predicates: # 断言
#            - name: Path
#              args:
#                patterns: /api/order/**
#                matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-productfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /createdefault-filters:- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 统一添加响应头

5.3.3 全局跨域

spring:cloud:gateway:globalcors:cors-configurations:'[/**]': # 所有请求都配置跨域allowed-origin-patterns: '*' # 允许所有IP访问allowed-headers: '*' # 允许携带所有头allowed-methods: '*' # 允许所有请求方法
# 以下是正常的跨域配置
#      globalcors:
#        cors-configurations:
#          '[/**]':
#            # 只允许特定来源
#            allowed-origins:
#              - "http://localhost:3000"
#              - "http://your-frontend-server.com"
#            allowed-headers:
#              - "Content-Type"
#              - "Authorization"
#              - "X-Requested-With"
#            allowed-methods:
#              - "GET"
#              - "POST"
#              - "PUT"
#              - "DELETE"routes:- id: order-route # 订单服务的路由uri: lb://service-order # lb 是负载均衡的转发给某服务的意思predicates: # 断言- Path=/api/order/** # 以 /api/order 开头的请求都转发 lb://service-orderfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /create- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 添加响应头- OnceToken=X-Response-Token, uuid # 自定义的过滤器
#            - name: Vip # 长写法
#              args:
#                param: user
#                value: mqy
#            - Vip=user,mqy # 短写法# 长写法
#        - id: order-route # 订单服务的路由
#          uri: lb://service-order # lb 是负载均衡的转发给某服务的意思
#          predicates: # 断言
#            - name: Path
#              args:
#                patterns: /api/order/**
#                matchTrailingSlash: true # 允许url后面携带多余的 / 比如 product/1/- id: product-route # 商品服务的路由uri: lb://service-productpredicates: # 断言- Path=/api/product/** # 以 /api/product 开头的请求都转发 lb://service-productfilters:- RewritePath=/api/order/?(?<segment>.*),/$\{segment} # 比如是 /api/order/create => /createdefault-filters:- AddResponseHeader=X-Response-Time,$(new java.util.Date().getTime()) # 统一添加响应头

结论:Gateway 内置的过滤器、断言等,只是为了方便我们直接去用。但在实际开发中,也就那么几个内置是需要记住的。其余情况大多数都是需要自定义断言、过滤器的!

六、Seata 分布式事务

由于一个功能,可能牵扯到多个微服务。而传统事务管理,是基于单条线程数据库连接的。所以就会造成,某些微服务回滚了,而某些微服务并未回滚的奇葩现象。

6.1 Seata 原理

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

全局事务:比如我这一个功能,调了好几个远程方法。没有人去调用我。那么我应该开启全局事务。
分支事务:全局事务下面的事务,被称为分支事务。

在这里插入图片描述
首先,要启动一个 Seata 服务器(TC),然后在每个微服务里面,引入Seata客户端(TM/RM)。这样就可以汇报我当前微服务的事务状态,并且接收Seata服务器发出的命令(某个事务回滚)。缺点就是 TC 宕机了,就G了!那么最好TC去做集群。

Seata 官网

在这里插入图片描述
2.4 版本之后,官方把控制台提取到了 seata-namingserver,但实测访问 localhost:8081 是报 404 的。但我们通常情况下,也无需使用控制台。所以不用过于担心~

在这里插入图片描述
在这里插入图片描述

<!-- Seata 分布式事务 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>

在这里插入图片描述

6.2 搭建官方给的示例

CREATE DATABASE IF NOT EXISTS `storage_db`;
USE  `storage_db`;
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`commodity_code` varchar(255) DEFAULT NULL,`count` int(11) DEFAULT 0,PRIMARY KEY (`id`),UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO storage_tbl (commodity_code, count) VALUES ('P0001', 100);
INSERT INTO storage_tbl (commodity_code, count) VALUES ('B1234', 10);-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE DATABASE IF NOT EXISTS `order_db`;
USE  `order_db`;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` varchar(255) DEFAULT NULL,`commodity_code` varchar(255) DEFAULT NULL,`count` int(11) DEFAULT 0,`money` int(11) DEFAULT 0,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE DATABASE IF NOT EXISTS `account_db`;
USE  `account_db`;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` varchar(255) DEFAULT NULL,`money` int(11) DEFAULT 0,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO account_tbl (user_id, money) VALUES ('1', 10000);
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

6.3 原理

Seata主要是用了二阶提交协议:

  • 一阶段(本地事务):在执行 SQL 语句之前,会拿到你的条件语句拼接sql 查到改变之前的数据,即现有数据。我们称之为 前镜像,然后将其查询结果缓存起来。我们再去执行修改操作。然后还会去查询,查到的结果称之为后镜像
    • 此时,我们会把前后镜像拿过来,插入到 undo_log 表中,作为回滚日志。
    • 接下来,注册分支事务,申请 storage_tbl 表1号记录的 全局锁(即,在此期间,其它的线程没法干扰我 毕竟担心其它人也来修改
    • 本地事务提交:业务数据 + undo_log 数据一起去保存
    • 汇报提交结果到TC服务器(如果有一个事务是不太对劲的,那么TC就会认为需要全部回滚!)

一阶段,简单来说就是各个分支事务,将它们本地事务都提交了。只不过是保存了前后镜像并向TC注册了分支事务。

  • 二阶段(分支提交/回滚)

    • 当所有分支包括自身都提交ok了,没问题!那么我们就立即响应。并且新增一个异步任务去批量的删除相应的 undo_log 记录。
    • TC早都维护了一个标识,当标识是需要回滚。那么就通过xid、branch id 找到 undo_log 的记录,然后数据校验后镜像与当前数据,如果不是一样的(则说明可能其它渠道给改了。。那你得看看咋回事,或者配置相应策略)。如果是一样的,证明确实无其它干扰,并且需要回滚。然后所有的都会恢复到 undo_log 前镜像的内容,完成后也会删除掉对应的 undo_log 记录!
  • AT 模式:自动模式(默认)

  • XA 模式(性能低):第一阶段会把所有分支都阻塞,拿到锁。等到了第二阶段再统一提交,提交失败的再回滚。

seata:data-source-proxy-mode: XA # 开启 XA
  • TCC 模式:全手动模式 资金扣减、库存冻结这类关键分布式事务,建议用 TCC 或 SAGA,而不是完全依赖 AT 自动回滚。
  • Saga 模式:长事务模式(消息队列)你要是想用这个,就比如直接用 消息队列 RabbitMQ 去做了。Seata 就没那么太必要了。
http://www.xdnf.cn/news/1217629.html

相关文章:

  • [硬件电路-111]:滤波的分类:模拟滤波与数字滤波; 无源滤波与有源滤波;低通、带通、带阻、高通滤波;时域滤波与频域滤波;低价滤波与高阶滤波。
  • 《Java 程序设计》第 17 章 - 并发编程基础
  • 澳交所技术重构窗口开启,中资科技企业如何破局?——从ASX清算系统转型看跨境金融基础设施的赋能路径
  • 数据结构与算法:队列的表示和操作的实现
  • HighgoDB查询慢SQL和阻塞SQL
  • 模型优化——在MacOS 上使用 Python 脚本批量大幅度精简 GLB 模型(通过 Blender 处理)
  • 打车小程序 app 系统架构分析
  • 【12】大恒相机SDK C#开发 ——多相机开发,枚举所有相机,并按配置文件中的相机顺序 将所有相机加入设备列表,以便于对每个指定的相机操作
  • 深入理解 Slab / Buddy 分配器与 MMU 映射机制
  • 【源力觉醒 创作者计划】对比与实践:基于文心大模型 4.5 的 Ollama+CherryStudio 知识库搭建教程
  • mysql结构对比工具
  • 类与对象(上),咕咕咕
  • ECMAScript2024(ES15)新特性
  • SpringAI 1.0.0发布:打造企业级智能聊天应用
  • AI 安监系统:为工业园安全保驾护航
  • 【Debian】4-‌1 Gitea简介以及与其他git方案差异
  • Windows 10 WSLUbuntu 22.04 安装并迁移到 F 盘
  • 2018 年 NOI 最后一题题解
  • 【预判一手面试问题:排序】
  • 2023 年 NOI 最后一题题解
  • n8n为什么建议在数组的每个item中添加json键?
  • Docker部署Nacos
  • LeetCode 53 - 最大子数组和
  • Android Emoji 全面解析:从使用到自定义
  • 《嵌入式C语言笔记(十六):字符串搜索、动态内存与函数指针精要》
  • 企业微信API接口发消息实战:从0到1的技术突破之旅
  • MySQL索引和事务笔记
  • 2419.按位与最大的最长子数组
  • JAVAEE--4.多线程案例
  • Mac配置iterm2