本文主要用于帮自己复习微服务,因为分布式系统和微服务本身的庞大和复杂,笔者很难在大学期间得到最佳实践,所以笔记中可能也存在不少疏漏,如有问题,请在下方留言,如果对你有帮助,那就太好啦(*^▽^*)。
序言:
什么是SpringCloudAlibaba?
- 它是Spring Cloud的子项目
- 致力于提供微服务开发的一站式解决方案
- 基于Spring Cloud,符合Spring Cloud标准
- 包含微服务开发的必备组件
- 服务发现与注册-Nacos
- 负载均衡-Ribbon
- 服务调用-Feign
- 服务流控与降级-Sentinel
- 配置中心-Nacos-config
- 全局事务处理-Seata
- 网关-Gateway
- 消息队列-Stream
- 链路追踪-Skywalking
- ……
如何在springboot中整合springcloudalibaba?
在父工程的pom.xml中的dependencyManagement里添加相应版本的springcloud以及springcloudalibaba依赖,当子工程中需要相应的组件时,直接引入无版本号的组件依赖即可
现在让我们来开始一一整理其中的各个组件。
一、服务发现与注册-Nacos
1.1 服务提供者与服务消费者
- 服务提供者:服务的被调用方
- 服务消费者:服务的调用方
1.2 服务发现原理
- 问题:如果用户地址发生变化,怎么办?
- 服务发现机制就是通过一个中间件去记录服务提供者的ip地址,服务名以及心跳等数据(比如用mysql去存储这些信息),然后服务消费者会去这个中间平台去查询相关信息,然后再去访问对应的地址,这就是服务注册和服务发现。
- 当用户地址发生了变化也没有影响,因为服务提供方修改了用户地址,在中间件中会被更新,当服务消费方去访问中间件时就能及时获取最新的用户地址,就不会出现用户地址发生变化导致服务找不到。
1.3 搭建Nacos Server
下载Nacos Server
(下载地址)[https://github.com/alibaba/nacos/releases]
搭建Nacos Server
(参考文档)[https://nacos.io/zh-cn/docs/quick-start.html]
启动服务器:
Windows:cmd startup.md(此处启动命令为单机模式,非集群模式)
1.4 将应用注册到Nacos中
- 加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> - 加注解:
早期在启动类上需要加上 @EnableDiscoveryClient注解,现在已经可以不需要加了 - 加配置:
spring:
cloud:
discovery:
server-addr: localhost:8848 #指定nacos server的地址
application:
name: 服务名称 # 比如 user-center,服务名称尽量用- ,不要用_
1.5 Nacos服务发现的领域模型
- Nacos中各个关键字释义如下:
- Namespace: 实现隔离,默认public,一般用来区分开发环境
- Group: 不同服务可以分到一个组,默认DEFAULT_GROUP,对Namespace的进一步细分,一般用来隔离项目
- Service:微服务
- Cluster: 对指定微服务的一个虚拟划分,默认DEFAULT
- Instance: 微服务实例
- Nacos元数据
- 什么是元数据?(Metadata)
- Nacos数据(如配置和服务)描述信息,如服务版本、权重、容灾策略、负载均衡策略、鉴权配置、各种自定义标签(label),从作用范围来看,分为服务级别的元信息、集群的元信息及实例的元信息。
- 元数据作用:
- 提供描述信息
- 让微服务调用更加灵活:例如微服务版本控制
- 元数据操作方式:
- 在Nacos Server中进行 集群、服务、实例各级别的元数据控制
- 在application.yml中进行配置
- 什么是元数据?(Metadata)
二、负载均衡-Ribbon
2.1 负载均衡的两种方式
- 利用Nginx在服务端做负载均衡,由Nginx分发用户请求到各个Tomcat实例上
- 客户端侧负载均衡,将负载均衡规则嵌在客户端里,由客户端向服务端发请求
2.2 客户端侧负载均衡的简单原理
在客户端中,获取到某服务的所有url,然后以轮询、随机等方式进行调用指定的url
2.3 使用Ribbon实现负载均衡
- Ribbon是什么?
- Ribbon为我们提供了丰富的负载均衡算法。
- 引入Ribbon:
- 加依赖: 此步骤省略,因为Nacos已经结合了Ribbon
- 写注解:
- 在RestTemplate的Bean上加@LoadBalanced
@Bean
@LoadBalanced
public RestTemplate restTemplate{
return new RestTemplate()
}
- 在RestTemplate的Bean上加@LoadBalanced
- 改名称:将restTemplate中调用的服务的IP改为服务名
2.4 Ribbon内置的负载均衡规则
2.5 Ribborn自定义细粒度配置
- Java代码配置 (注:非极端情况下不推荐用该方法配置)
- 创建配置类
- 在启动类包下创建 (不推荐,会被启动类扫描到,变成全局配置)
- 在与启动类包不同路径下创建
@Configuration
public class RibbonConfiguration{
@Bean
public IRule ribbonRule(){
// 随机
return new RandomRule();
}
}
- 在相应客户端的启动类上加上
@RibbonClients
@RibbonClients(value = {@RibbonClient(name="stock-service",configuration = {RibbonConfiguration.class})})
public class OrderServiceApp {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApp.class, args);
}
}
- 创建配置类
- yaml属性配置
- 在resource目录下的application.yml中添加配置:
xxx服务名称:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 想要的规则的类的所在全路径
- 在resource目录下的application.yml中添加配置:
- 饥饿加载
ribbon:
eager-load
enabled: true
clients: xxx服务名 # 多个服务,以 `,`号分割
三、声明式Http客户端-OpenFeign(Feign)
3.1 OpenFeign简介
Feign 是 RestTemplate 的升级版,进一步封装了 Ribbon,并且我们只需创建一个接口并使用注解的方式来配置它(以前是在 Dao 接口上面标注 Mapper 注解,现在是一个 Service 接口上面标注 Feign 注解),即可完成对服务提供方的接口绑定。而OpenFeign是Feign的进一步升级,支持了 SpringMVC 的注解如 @RequesMapping 等等,OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
3.2 OpenFeign的使用方式
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类开启Feign注解,即加上
@EnableFeignClients
- 创建接口
@FeignClient(name="Nacos中的服务名称")
public interface xxxFeignClient{
@xxxMapping("对应服务接口的url")
对应返回值 方法名随意(对应接口参数);
}
其实就是相当于把服务端你要调用的那个接口的接口复制过来。
3.3 OpenFeign的细粒度配置
- 日志配置(默认且推荐用属性配置)(可能会和 SpringBoot 的日志级别产生覆盖的问题)
在resource/appilcation.yml中进行配置:feign:
client:
config:
# 想要调用的微服务名称
user-center:
loggerLevel: full
- @FeignClient 标签的常用属性
- name:用于指定 Feign 客户端调用的服务名称,同时,如果指向该服务的 Feign 客户端唯一,那也是该Feign客户端的名称。
- url:用于指定服务的 URL,通常用于调试或服务未注册到服务发现时,且
url
属性会覆盖name
属性。 - configuration:用于指定自定义配置类,配置类可以用来定制 Feign 客户端的行为,如请求拦截器、编码器和解码器等。(因为涉及到配置类配置,所以不推荐)
- fallback:用于指定服务降级的实现类,当 Feign 客户端调用失败时,会调用 fallback 指定的类中的方法。
@FeignClient(name = "demo-user", fallback = UserClientFallback.class)
public interface UserClient {
// ...
}
@Component
public class UserClientFallback implements UserClient {
@Override
public User getUserById(Long id) {
return new User();
}
}
- fallbackFactory:用于指定服务降级的工厂类,该工厂类可以提供更多的上下文信息,例如异常信息。
@FeignClient(name = "demo-user", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
// ...
}
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUserById(Long id) {
// 返回一个默认的用户对象,并打印异常信息
System.out.println("Fallback cause: " + cause);
return new User();
}
};
}
}
- path:用于指定服务的统一前缀路径,在定义 Feign 接口的方法时可以省略该路径。
- contextId:用于在多 Feign 客户端实例中区分不同的上下文 ID,特别适用于多个 Feign 客户端指向同一服务时的配置区分。(相当于给指向同一个服务的不同 Feign 客户端实例取个别名,和 Servlet 的上下文不是同一个意思)
- 添加请求头信息
- 在方法参数上添加请求头信息
@FeignClient(name = "demo-user")
public interface UserClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id, @RequestHeader("Authorization") String token);
接着,在调用接口时,传递请求头参数即可。 - 使用 Feign 配置类定义请求拦截器(与Ribbon的配置类同理,不推荐)
创建一个 Feign 配置类,并定义请求拦截器:@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
if (token != null) {
template.header("Authorization", token);
}
}
}};
}
}
接着,在 Feign 客户端接口上指定配置类:@FeignClient(name = "demo-user", configuration = FeignConfig.class)
public interface UserClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
- 在Feign的属性文件中配置请求拦截器(优先级更高,推荐)
public class MyHeaderInterceptor implements RequestInterceptor {
private static String headerName = "token";
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
if (token != null) {
template.header("Authorization", token);
}
}}
}
接着,在配置文件中添加拦截器配置feign:
client:
config:
# 默认配置 如果不单独配置每个服务会走默认配置
default:
request-interceptors:
- com.demo.config.MyHeaderInterceptor
# 配置单独 FeignClient
# @FeignClient(value = "demo-user", contextId = "userInfoClient")
# 如果FeignClient注解设置了 contextId 这里就使用 userInfoClient 如果没有设置 contextId 就直接使用服务名称 demo-user
userInfoClient:
request-interceptors:
- com.demo.config.MyHeaderInterceptor
- 在方法参数上添加请求头信息
- Feign常见性能优化
- 配置连接池(HttpClient 或 okHttp,这里以 HttpClient 为例)
- 引入依赖:
<dependency>
<group>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- application.yml中进行配置:
feign:
client:
config:
# 全局配置
default:
loggerLevel: full
httpclient:
# 让Feign使用 apache httpclient做请求,而不是默认的urlHttp
enabled: true
# feign的最大连接数
max-connections:200
# feign单个路径的最大连接数
max-connections-per-route: 50
- 引入依赖:
- 降低日志级别
- 配置连接池(HttpClient 或 okHttp,这里以 HttpClient 为例)
四、服务容错-Sentinel
4.1 雪崩效应
在我们的系统中,当一个服务宕机后,其他服务如果需要来访问这个服务时,就会得不到结果,然后会一直等待此服务返回结果,直至调用超时。每个一个访问请求都是一个线程资源,当服务的调用次数过多,就会导致大量的资源得不到释放,可能就会导致消费服务方也宕机,这样类推会导致雪崩效应,就是由一个服务宕机导致其他服务系统资源被持续占用消耗得不到释放,从而引发一连串的级联失败。
4.2 常见容错方案
- 设置超时时间
- 设置限流
- 仓壁模式:比如一艘船,里面每个船舱都是独立的,当一个船舱进水了,也不会导致所有的船舱进水,从而使船沉没。每个Controller作为一个“船舱”。
- 断路器:
4.3 Sentinel使用方式
4.3.1 引入Sentinel控制台
- 下载Sentinel控制台:https://github.com/alibaba/Sentinel/releases
- 在sentinel-dashboard的jar包位置使用cmd命令启动jar包:java -jar sentinel-dashboard-x.x.x.jar
- 访问localhost:8080(账号密码默认都是 sentinel )
4.3.2 引入Sentinel核心库
- 引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>${spring-cloud-alibaba.version}</version>
(加版本是因为sentinel本身是独立于springcloud的)</dependency>
- 添加配置:(sentinel就是懒加载)
spring:
cloud:
sentinel:
transport:
# 指定sentinel 控制台地址
dashboard: localhost:8080
- 当被监控的服务的api被调用后,该服务的所有接口都会自动被注册进sentinel-dashboard中
4.4 Sentinel的控制规则
4.4.1 流控规则
流控规则的各个属性:
- 资源名: 唯一名称,默认请求路径,表示对该资源进行流控
- 针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单击阈值:
QPS:(每秒钟的请求数量):当调用该api的QPS达到阈值时,进行限流
线程数:当调用该api的线程数达到阈值的时候,进行限流 - 是否集群:不需要集群
- 流控模式:
直接: api达到限流条件时,直接限流
关联: 当关联的资源达到阈值时,就限流自己
链路: 只限制指定链路上的流量 - 流控效果:
快速失败: 直接失败,抛异常
Warm Up: 根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFctor,经过预热时长,才达到设置的QPS阈值。它可以让流量缓慢增加。(防止流量激增导致系统过载)
排队等待: 匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效。它可以让流量均匀分布。
4.4.2 熔断(降级)规则
熔断规则的几种策略:
- 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% – 100%。
- 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断
4.4.3 @SentinelResource注解的使用
@SentinelResource的常用属性:
- value:作用指定资源名称。
- blockHandler(流控异常处理器):指定用于处理流控异常BlockException的函数名称,函数要求为:
1. 必须是public
2. 返回类型与原方法一致
3. 参数类型需要和原方法相匹配,并在最后加上BlockException类型的参数
4. 默认需和原方法在同一个类中,如果希望使用其他类的函数,可配置blockHandlerClass,并指定blockHandlerClass里面的方法。 - blockHandlerClass:存放blockHandler的类。对应的处理函数必须static修饰,否则无法解析,其余要求:同blockHander。
- fallback(熔断异常处理器):用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了execptionsToIgnore 里面排除掉的异常类型)进行处理,函数要求为:
1. 返回类型与原方法一致
2. 参数类型需要和原方法相匹配,Sentinel 1.6版本之后,也可在方法最后加上Throwable类型的参数
3. 默认需和原方法在同一个类中,若希望使用其他类的函数,可配置fallbackClass,并指定fallbackClass里面的方法 - fallbackClass:存放fallback的类。对应的处理函数必须static修饰,否则无法解析,其他要求:同fallback。
- exceptionsToIgnore:指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
流控用法演示:(熔断同理)
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("getOrderNo")
@SentinelResource(value = "getOrderNoResource", blockHandler = "getOrderNoBlockHandler", blockHandlerClass = UserController.class)
public String getOrderNo(String userId, String tenantId, HttpServletRequest request){
return userService.getOrderNo(userId,tenantId,request);
}
public static String getOrderNoBlockHandler(String userId, String tenantId, HttpServletRequest request,BlockException e){
String msg = "不好意思,前方拥挤,请您稍后再试";
return msg;
}
}
4.4.4 热点规则
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的数据,并对其访问进行限制。
热点规则属性:
- 参数索引:方法中参数的索引第几个参数
- 单机阈值:每秒达到单机阈值的数量就会触发兜底方法
4.4.5 系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
4.5 Sentinel整合Feign
- 引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>sentinel-feign</artifactId>
</dependency>
- 添加配置:
feign:
sentinel:
enabled: true
- 在@FeignClient上定义一个统一的异常处理类作为备用方案
@FeignClient(name = "my-service", fallback = MyServiceFallback.class)
public interface MyServiceClient{
...
} - 通过 @SentinelResource 注解来为 Feign 具体接口的调用添加 Sentinel 的流控和熔断规则,进一步细化,用法与上述相同
@SentinelResource(value = "remoteData", fallback = "fallbackHandler", blockHandler = "blockHandler")
@GetMapping("/api/remote-data")
String getRemoteData();
4.6 Sentinel的持久化 // todo
四、网关-Gateway
4.1 为什么使用使用网关?
- 在不使用Gateway的情况下,当我们直接与微服务通信的情况下,需要每个服务都进行网关登录验证,同时需要解决各个服务的登录状态的同步等功能;
- 使用Gateway可以对外暴露一个域名,微服务无论增加多少,都只需要指向一个网关即可,它可以统一对外进行登录、校验、授权、以及一些拦截操作;
4.2 Gateway三大核心概念
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
- Predicate(断言):参考的是 Java8 的 java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
- Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
4.3 Gateway的使用方式
- 引入依赖:(聚合工程)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>x.x.x</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>202x.x.x</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
- 添加配置(也有用Java代码配置的,但推荐用属性配置)
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 让gateway通过服务发现组件找到其他的微服务
lower-case-service-id: true # 将请求路径上的服务名配置为小写routes: # 核心概念
- id: payment_routh # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址(原来是 uri: http://localhost:8001,lb 表示开启Gateway的负载均衡功能)
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址(原来是 uri: http://localhost:8001,lb 表示开启Gateway的负载均衡功能)
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
4.4 Predicate-断言的使用
1. 作用:如果请求与断言相匹配则进行路由,如果不匹配直接404。
2. 一共有12个断言工厂-Route Predicate Factories,可以应用在不同的场景中。
- After Route Predicate:设置的时间 之后 是可以访问这个路由的,在这个时间之前是访问不了的。
使用示例:spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- After=2022-08-20T00:10:15.434859+08:00[Asia/Shanghai]
- Before Route Predicater:Before就是设置的时间之前可以访问,过了时间之后不可以访问。
- Between Route Predicate:两个时间的区间是可以访问的,过了时间之后不可以访问。
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
- Cookie Route Predicate:Cookie路由谓词工厂有两个参数,cookie和namea regexp(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。不带cookie访问直接404。
- Cookie=username,zzyy
- Header Route Predicater:Header路由谓词工厂也有两个参数,和Cookie工厂类似,详细见示例。
- Header=X-Request-Id, \d+
- Host Route Predicate:路由谓词工厂采用Host一个参数:主机名列表patterns。
- Host=**.baidu.com
正确:curl http://localhost:9527/payment/lb -H “Host: www.baidu.com”
正确:curl http://localhost:9527/payment/lb -H “Host: java.baidu.com”
错误:curl http://localhost:9527/payment/lb -H “Host: java.baidu.net” - Method Route Predicate:设置了之后只有固定方法的请求会路由.
- Method=GET
- Path Route Predicate:关于path的上面示例当中我们就已经用到了。
- Query Route Predicate:支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。
- Query=username, \d+
示例:http://localhost:9527/payment/lb?username=31 - RemoteAddr Route Predicate:路由谓词工厂采用的RemoteAddr列表(最小大小为 1)sources,它们是 CIDR 表示法(IPv4 或 IPv6)字符串,例如192.168.0.1/16(其中192.168.0.1是 IP 地址和16子网掩码)。
- RemoteAddr=192.168.1.1/24
- Weight Route Predicate:Weight路由谓词工厂有两个参数:group和一个 int。以下示例配置权重路由谓词:
- XForwarded Remote Addr Route Predicate:这可以与反向代理一起使用,例如负载平衡器或 Web 应用程序防火墙,其中仅当请求来自这些反向代理使用的受信任的 IP 地址列表时才允许请求。(暂时不用过多纠结这个)
- XForwardedRemoteAddr=192.168.1.1/24
4.5 Filter的使用
1. 作用:在执行方法前和执行方法后进行过滤,所谓的过滤就是可以在请求上加一些操作,例如匹配到路由后可以在请求上添加个请求头,或者参数等等。
2. Gateway过滤器分为了两种:路由过滤器 和 全局过滤器。
- 路由过滤器:路由过滤器针对于某一个路由进行使用,其中官网给我们提供了30多种类型的路由过滤器。
- 全局的往往是我们经常会用到的,他和路由过滤器区别就是,他是针对于所有路由进行设置的过滤规则,实际开发当中很少会针对于某一个路由使用Filter,大部分都会采用全局过滤器。
3. 常用的GatewayFilter:
- AddRequestHeader GatewayFilter:相当于是给匹配到路由的 request 添加 Header
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
filters:
- AddRequestHeader=X-Request-red, blue
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- The RequestRateLimiter GatewayFilter Factory:通过这个Filter就可以利用redis来完成限流
4. 自定义全局过滤器
利用全局过滤器我们可以用来做统一网关鉴权,以及全局日志记录等等。
使用方式:
1. 建立一个xxxGateWayFilter类,实现 GlobalFilter, Ordered两个接口,然后重写两个方法,一个是filter方法,一个是getOrder方法。(全局过滤器可以存在多个,多个的时候根据getOrder方法的返回值大小就行排序执行,数字最小的过滤器优先执行)
@Component //必须加,必须加,必须加
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
System.out.println("****用户名为null,无法登录");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
// 这个就是继续执行的意思
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
2. 实现全局前置和后置过滤器(其实还是基于全局过滤器,只不过展现的方式不一样了,这里是直接通过bean注解来注入到容器,然后使用的是匿名类)
@Configuration
public class GateWayFilter {
@Bean
public GlobalFilter customGlobalFilter() {
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("请求前执行,这里可以放请求前的逻辑");
exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", "lisi").build();
return chain.filter(exchange);
}
};
}
@Bean
public GlobalFilter customGlobalPostFilter() {
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
System.out.println("请求后执行,这里是当网关拿到转发服务的请求响应后会执行");
//adds header to response
serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked" : "It did not work");
return serverWebExchange;
}).then();
}
};
}
}
3. 实现指定路由跳过全局过滤器
在设置了全局过滤器的情况下,再自定义一个局部过滤器GatewayFilter,
@Component
public class IgnoreGlobalFilterFactor extends AbstractGatewayFilterFactory<IgnoreGlobalFilterFactor.Config> {
public static final String ATTRIBUTE_IGNORE_GLOBAL_FILTER = "@ignoreGlobalFilter";
public IgnoreGlobalFilterFactor() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return this::filter;
}
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(ATTRIBUTE_IGNORE_GLOBAL_FILTER, true);
return chain.filter(exchange);
}
// 这个名称就是yml当中设置的名称,也就是这个过滤器会去yml当中获取,看看有在filters当中设置IgnoreGlobalFilter,如果设置了执行,执行相当于是赋值了一下,然后在全局过滤器根据是否能拿到这个值来决定是否跳过过滤器
@Override
public String name() {
return "IgnoreGlobalFilter";
}
public static class Config {
}
}
然后,再将要这个自定义过滤器配置在要跳过的路由里即可
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
filters:
- IgnoreGlobalFilter
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
5. 自定义局部过滤器 (GatewayFilter)
在 Spring Cloud Gateway 中,自定义局部过滤器(GatewayFilter)可以通过实现 GatewayFilter
接口或继承 AbstractGatewayFilterFactory
类来实现。
1:
public class CustomGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 在此处添加自定义逻辑
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 定义过滤器的执行顺序
}
}
2:
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
public CustomGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 在此处添加自定义逻辑
return chain.filter(exchange);
};
}
public static class Config {
// 定义配置属性
}
@Override
public String name() {
// 向外暴露局部过滤器名字
return "xxx";
}}
4.6 Http超时配置
1. 全局超时
- connect-timeout必须以毫秒为单位指定。
- response-timeout必须指定为 java.time.Duration
spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s
假如单个路由的不想受全局超时限制可以这样做:
- id: per_route_timeouts
uri: ...
predicates: ...
metadata:
response-timeout: -1
2. 单个路由超时
- id: per_route_timeouts
uri: ...
predicates: ...
metadata:
connect-timeout: 200
response-timeout: 200
4.7 日志级别
logging:
level:
org.springframework.cloud.gateway: trace
4.8 Gateway 整合 Sentinel 进行流控降级 // todo
五、分布式事务-Seata
1. Seata是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
2. Seata的三大角色
在 Seata 的架构中,一共有三个角色:
- TC (Transaction Coordinator) – 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。 - TM (Transaction Manager) – 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。 - RM (Resource Manager) – 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
3. AT 模式介绍
AT 模式是一种无侵入的分布式事务解决方案。阿里 Seata 框架,实现了该模式。
在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
3.1 AT模式实现说明
- 一阶段:
- Seata 会拦截” 业务SQL”,解析 SQL 语义
- 查询 “业务SQL” 要更新的业务数据,在业务数据被更新前,将其保存成 “before image”
- 执行 “业务SQL” ,更新业务数据
- 查询更新后的数据,将其保存成 “after image”
- 将 before image 和 after image 保存至 Undo Log 表中
- 生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段(提交):
- 因为 “业务SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
- 二阶段(回滚):
- 首先要校验脏写,对比“数据库当前业务数据”和 “after image”
- 如果两份数据完全一致就说明没有脏写,可以还原业务数据。
- 如果不一致就说明有脏写,出现脏写就需要转人工处理。(不太可能出现这种情况,因为加了行锁)
- 用“before image”还原业务数据
- 删除快照数据和行锁
- 首先要校验脏写,对比“数据库当前业务数据”和 “after image”
3.2 实例说明
下单操作中包含 对订单服务与库存服务的操作,分别修改订单状态,与库存数量。示例图如下:
4. 设计亮点
相比与其它分布式事务框架,Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
- 通过全局锁实现了写隔离与读隔离。
5. Seata 快速开始
5.1 Seata Server(TC)环境搭建
5.1.1 下载安装包
链接: https://github.com/seata/seata/releases
注意: Seata 和 Spring Cloud Alibaba 的版本匹配问题:spring-cloud-alibaba 版本说明文档
5.1.2 事务信息存储配置
Server 端存储模式(store.mode)支持三种方式:
- file:单机模式(默认为此模式),全局事务会话信息存储在内存中,读写并持久化至本地文件 root.data (bin\sessionStore\root.data) 中,性能较高。
- db(推荐):高可用模式(Mysql 5.7+),全局事务会话信息通过db共享,相应性能差些。
- redis:Seata-Server 1.3及以上版本支持,性能较高,但存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置。
使用 db 模式进行配置说明:
- 打开
conf/file.conf
文件 - 修改 mode = “db”
- 修改数据库连接信息(url\user\password)
- 创建数据库 seata-server
- 新建表:可以去seata提供的资源信息中下载:官网(选择对应版本)
client:存放 client 端 sql 脚本,参数配置
config-center:各个配置中心参数导入脚本,其中 config.txt (包含server和client)为通用参数文件
server:server 端数据库脚本及各个容器配置 - 运行 server\db\mysql.sql 文件:
global_table:存储全局事务的信息
branch_table:存储事务参与者的信息
lock_table:存储锁信息(锁的表信息,行信息)
5.1.3 Nacos配置(内容繁杂,不好描述,详情可见这篇博客:Seata 分布式事务搭建与使用详解,一篇搞定,手把手教会你 !)
注意:首先,conf/registry.conf 这个文件是 Seata 的配置文件,它是用于配置 Seata 客户端 如何与注册中心(如 Nacos)和配置中心交互的。
registry.conf 的作用
- 注册中心配置:告诉 Seata 客户端如何从注册中心(如 Nacos)获取 Seata Server 的地址。
- 配置中心配置:告诉 Seata 客户端从哪里加载事务相关的配置信息(例如分组、全局事务超时等)。
其次,文章中强调的 “使用nacos时也要注意group要和seata server中的group一致”,是指 Seata Server 的注册中心配置 register 和 Seata client 读取的配置中心配置 config ,两者的group要一致,这样 Seata client 才能正确读取到 Seata Server 在配置中心里的位置。
最后,为什么文章又强调seata/script/config-center/config.txt 文件里 service.vgroupMapping.my_test_tx_group = default 中的 my_test_tx_group 需要与客户端中的配置保持一致,又为什么其中的 default 必须要等于 registry.conf cluster = “default”?因为配置 seata.tx-service-group
其实就是在声明客户端的事务分组名(如 my_test_tx_group
),而 Seata 服务端需要将对应的客户端分组映射到指定的 Seata Server 集群中,这就是 vgroupMapping 的作用。而default是 Seata Server 在注册进 Nacos 时指定的集群名称,映射的 default 显然必须与注册时的 default 一致。
5.2 Seata Client 代码实现
5.2.1 启动Seata server端,Seata server使用nacos作为配置中心和注册中心(上一步已完成)
5.2.2 配置微服务整合seata
- 添加pom 依赖
<dependencies>
<!--nacos 服务注册与发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--导入openfeign 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- skywalking 工具类 可自定义链路追踪,跟服务版本一致-->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.12.0</version>
</dependency>
</dependencies>
- 在各微服务对应数据库中添加 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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- 修改application.yml配置
spring:
application:
name: seata-order
datasource:
url: jdbc:mysql://172.16.10.132:3306/seata_order?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
server-addr: 172.16.10.132:8847
discovery:
username: nacos
password: nacos
alibaba:
seata:
tx-service-group: my_test_tx_group # 配置事务分组,在 "seata\seata\script\config-center\config.txt" 中设置
server:
port: 8070
seata:
registry: # 注册中心
# 配置 seata 的注册中心, 告诉 seata client 怎么去访问 seata server 事务协调者进行通信
type: nacos
nacos:
server-addr: 172.16.10.132:8847 # seata-server 所在的注册中心地址
application: seata-server # 指定 seata-server 在注册中心的 服务名, (默认 seata-server)
username: nacos
password: nacos
group: SEATA_GROUP # 默认 SEATA_GROUP
config:
# 配置 seata 的配置中心,可以读取关于 seata client 的一些配置,即 "seata\seata\script\config-center\config.txt" 中的配置
type: nacos
nacos: # 配置中心
server-addr: 172.16.10.132:8847
username: nacos
password: nacos
group: SEATA_GROUP
- 在启动类或方法上添加
@GlobalTransactional
即可