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的哪条链路)比如秒杀创建订单这条链路才会被限制,而创建普通订单无需被限制。
- 链路(表面上控制的是资源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 就没那么太必要了。