16088 字
80 分钟
SpringCloud学习笔记

前言#

现在其实感觉挺突兀的,我学习java的第一天,看着冗长的学习路线,心里其实是有一些激动的,因为马上就要踏上这条漫长的道路了,当时我看着路线最后的”分布式微服务“,Spring-Cloud-Alibaba,那时候心里是怎么想的呢?我已经记不清楚了,现在真正开始学习Spring-Cloud的时候,给我感觉就是,“啊,终于到这里了”。不多说闲话了,努力吧

outline

SpringCloud#

一、Consul#

为什么要引入Consul呢,首先在正常构建Web微服务应用的时候,各个微服务之间的api调用往往涉及到IP和端口的硬编码,所以引入Consul的第一个原因就是要解决硬编码。 第二个原因就是为了实现负载均衡,如果一个系统中出现了多个订单和支付微服务,那就无法实现负载均衡 第三个原因就是老生常谈的高并发,如果要部署很多的订单和支付微服务,还是硬编码,那对系统的并发性能是毁灭性的打击 除此以外,Consul还提供了分布式配置的功能,这能够大大减少程序员在对分布式微服务项目的配置文件的修改与维护 所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现

详情请见官网↓

Consul官网

①服务注册与发现#

使用Consul实现服务注册很简单

(1)首先,你需要拥有一台Consul,可以选择去Consul的官网下载并部署在你的本地或者服务器上

(2)然后,需要给微服务Module引入Consul的依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

(3)接着,要在Module的配置文件中指明Consul的配置信息,示例在application.yml中添加如下内容

spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}

(4)最后,需要在主启动类上加上SpringCloud的注解:@EnableDiscoveryClient ,来开启服务发现

②服务注册的CAP原则#

在服务注册中,评判一个注册中心优秀与否一般有三个因素

C : Consistency(一致性) A : Availability(可用性) P : Partition tolerance(分区容错性)

老牌注册中心Eureka就是典型的AP注册中心,在网络分区出现后,为了保证高可用, 牺牲了一致性。 每个对外暴露服务的节点的注册信息是不保证一致的,也就是说,如果A、B两个节点的数据不同,那么向A和B发送的请求中,是有一部分失效的,这就是牺牲了C,达到的AP

而现在使用的Consul亦或是Zookeeper,当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性,Consul 遵循CAP原理中的CP原则,保证了强一致性和分区容错性,且使用的是Raft算法,比zookeeper使用的Paxos算法更加简单。虽然保证了强一致性,但是可用性就相应下降了,例如服务注册的时间会稍长一些,因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功 ;在leader挂掉了之后,重新选举出leader之前会导致Consul 服务不可用。结论:违背了可用性A的要求,只满足一致性和分区容错,即CP

③服务配置与刷新#

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。

(1)首先要引入Consul关于分布式配置的依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

(2)新增bootstrap配置文件

applicaiton.yml是用户级的资源配置项 bootstrap.yml是系统级的,优先级更加高

Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap contextApplication Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。

application.yml文件改为bootstrap.yml,这是很关键的或者两者共存 因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml

(3)在Consul服务器上新增KV数据,按照:config/module-dev/data这种层级就行,分隔符可以在项目里设置

PS:但这也引发了两个问题,第一,在Consul服务器上对分支的数据进行修改,正在运行的服务无法立刻拿到更新的数据,第二,在Consul重启后,原先设置的KV数据全部清空了。

解决如下 ,第一个问题,可以通过在主启动类上添加@RefreshScope注解来使服务具备动态刷新的能力,并可以在配置文件中通过修改wait-time来定制刷新的间隔时间

第二个问题,就涉及到Consul配置的持久化,在下面单开一节来说

④服务配置的持久化#

在启动Consul服务器时候,在命令行后添加data-dir参数来指定持久化文件目录

Terminal window
consul agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect 1 -data-dir path

这样,在每次启动Consul服务器时,就会读取path路径下的文件来载入Consul的配置文件

二、LoadBalancer#

LB负载均衡(Load Balance)是什么

简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等

使用SpringCloud场景下的LoadBalancer将采用客户端负载均衡的方式实现LB

客户端负载均衡和服务端负载均衡的区别是什么?

Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。

LoadBalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

①负载均衡#

(1)在消费服务中引入LoadBalancer依赖

<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

(2)在RestTemplate中加入注解来将RestTemplate注入成一个LoadBalancer客户类

@Configuration
@LoadBalancerClient(
//下面的value值大小写一定要和consul里面的名字一样,必须一样
value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig
{
@Bean
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

(3)在消费服务的Controller中使用**RestTemplate**来进行RPC远程调用

@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/pay/get/{id}")
public ResultData getPayInfo(@PathVariable Integer id)
{
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/"+id, ResultData.class, id);
}

②原理及算法实现#

负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。

List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");

如: List [0] instances = 127.0.0.1:8002

   List [1] instances = 127.0.0.1:8001

8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:

当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

在实验中可以看出,如果不做任何设置,那么LoadBalancer的默认负载均衡算法是使用轮询方式实现的。 其实在LoadBalancer内部,所有的算法都需要去实现ReactorServiceInstanceLoadBalancer这个接口

如果想要修改LoadBalancer的默认算法实现,可以修改RestTemplate的配置类来实现,将其他提供的算法或者自定义的算法注入到IOC容器来实现注入覆盖

@Configuration
@LoadBalancerClient(
//下面的value值大小写一定要和consul里面的名字一样,必须一样
value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig
{
@Bean
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}

三、OpenFeign#

OpenFeign和LoadBalancer一样,都是负责服务间互相调用的常用微服务组件,总的来说,其实二者各有优劣,但现在业内对这两个技术使用量都很大,所以这里也着重介绍一下OpenFeign

OpenFeign能做什么?

(1)可插拔的注解支持(Feign,JAX-RS…)

(2)可插拔的HTTP编码器、解码器

(3)支持Sentinel和它的Fallback

(4)支持SpringCloud LoadBalancer的负载均衡

(5)支持HTTP请求和响应的压缩

①实现架构及使用流程#

openFeign

(1)在消费服务和通用模块中引入OpenFeign依赖

<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

(2)在主启动类上加入@EnableFeignClients注解,以开启OpenFeign功能

(3)在通用模块中创建对应服务模块的接口

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{
/**
* 新增一条支付相关流水记录
* @param payDTO
* @return
*/
@PostMapping("/pay/add")
public ResultData addPay(@RequestBody PayDTO payDTO);
/**
* 按照主键记录查询支付流水信息
* @param id
* @return
*/
@GetMapping("/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id);
/**
* openfeign天然支持负载均衡演示
* @return
*/
@GetMapping(value = "/pay/get/info")
public String mylb();
}

(4)在Controller调用对应的接口来实现服务调用

@RestController
@Slf4j
public class OrderController
{
@Resource
private PayFeignApi payFeignApi;
@PostMapping("/feign/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO)
{
System.out.println("第一步:模拟本地addOrder新增订单成功(省略sql操作),第二步:再开启addPay支付微服务远程调用");
ResultData resultData = payFeignApi.addPay(payDTO);
return resultData;
}
@GetMapping("/feign/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id)
{
System.out.println("-------支付微服务远程调用,按照id查询订单支付流水信息");
ResultData resultData = payFeignApi.getPayInfo(id);
return resultData;
}
/**
* openfeign天然支持负载均衡演示
*
* @return
*/
@GetMapping(value = "/feign/pay/mylb")
public String mylb()
{
return payFeignApi.mylb();
}
}

②超时控制

OpenFeign自带了一个超时控制机制,即往一个服务发送的请求,如果长时间没有得到响应,那么将报错处理

可以通过修改module的配置文件来自定义超时时间

spring:
cloud:
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000

上面是全局设置,即模块下的所有服务都参考此配置

也可以通过自定组的方式来进行单个服务设置,如下

spring:
cloud:
openfeign:
client:
config:
# default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
#连接超时时间
connectTimeout: 4000
#读取超时时间
readTimeout: 4000
# 为serviceC这个服务单独配置超时时间,单个配置的超时时间将会覆盖全局配置
serviceC:
#连接超时时间
connectTimeout: 2000
#读取超时时间
readTimeout: 2000

③重试机制#

在OpenFeign默认中,是不具备重试功能的,即一次请求失败或报错后,不再进行重新请求,直接结束

但可以通过自定义Feign配置的方式来启动这个功能

创建FeignConfig配置类来为容器中注入Feign

@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
//return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100,1,3);
}
}

④底层通信协议替换#

在OpenFeign的默认配置中,是使用Java自带的HttpURLConnection来向其他服务发送请求,但实话说,效率是一般的,所以我们可以通过替换底层的通信协议来修改OpenFeign

因为OpenFeign官方推荐使用Apache的HttpClient5,所以我们接下来用它来演示

(1)引入hc5以及OpenFeign对hc5的支持

<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>

(2)在配置文件中启用hc5

# Apache HttpClient5 配置开启
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true

⑤请求响应压缩#

对请求和响应进行GZIP压缩

Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

通过下面的两个参数设置,就能开启请求与相应的压缩功能:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.response.enabled=true

细粒度化设置

对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,

只有超过这个大小的请求才会进行压缩:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型

spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

在配置文件下做如下修改即可启用压缩

spring:
cloud:
openfeign:
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true

⑥日志打印#

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,

从而了解 Feign 中 Http 请求的细节,

说白了就是对Feign接口的调用情况进行监控和输出

只需要一步即可,往IOC容器中注入对应的日志组件,并配置自定义的日志级别

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug

四、CircuitBreaker#

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.

“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

CircuitBreaker解决的问题

(1)服务熔断

(2)服务降级

(3)服务限流

(4)服务限时

(5)服务预热

(6)实时监控

(7)兜底处理

CircuitBreaker只是一套规范和接口,落地实现者是Resilience4J

Resilience4J#

Resilience4i是一个专为函数式编程设计的轻量级容错库。Resilience4i 提供高阶函数(装饰器),以通过断路器、速率限制器、重试或隔板增强任何功能接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆善多个装饰器。优点是您可以选择您需要的装饰器,而没有其他选择。 Resilience4j2 需要 Java 17

服务熔断降级(CircuitBreaker)#

①熔断器的三种状态及相互转换#

state

断路器通过有限状态机实现,有三个普通状态:关闭、开启、半开,还有两个特殊状态:禁用、强制开启。

断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。

当熔断器关闭时,所有的请求都会通过熔断器 如果失败率超过所设定的阈值,熔断器就会从关闭转换为打开,此时所有请求被拒绝 经过一段时间后,熔断器就会从打开状态转换至半开状态,这时会放入一定数量的请求,并重新计算失败率 如果失败率超过阈值,则重新变回打开状态,否则变为关闭状态

②熔断器参数配置#

failure-rate-threshold以百分比配置失败率峰值
sliding-window-type断路器的滑动窗口期类型 可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。
sliding-window-size若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。
slowCallRateThreshold以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。
slowCallDurationThreshold配置调用时间的峰值,高于该峰值的视为慢调用。
permitted-number-of-calls-in-half-open-state运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。
minimum-number-of-calls在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。
wait-duration-in-open-state从OPEN到HALF_OPEN状态需要等待的时间

== 1、COUNT_BASED窗口滑动的熔断器YAML配置

# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slidingWindowType: COUNT_BASED # 滑动窗口的类型
slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default

== 2、TIME_BASED 窗口滑动的熔断器YAML配置

# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default

③总结#

总的来说,熔断器触发的条件就是达到一定的峰值和失败率,然后会使断路器进入OPEN状态,当OPEN时,所有请求都不会调用主业务逻辑方法,而是走FALLBACK METHOD的兜底方法,过了一段时间后,熔断器会从OPEN状态进入HALF_OPEN状态,并放行几个请求,如果成功,就CLOSED,否则继续OPEN

服务隔离(BulkHead)#

bulkhead是一种用于提高系统弹性和隔离故障传播的设计模式,目的是通过限制资源的使用和隔离故障来保护系统免受单个组件故障的影响

BulkHead的两大功能

依赖隔离、负载保护

Resilience4j提供了两种实现隔离的方法:实现SemaphoreBulkhead或FixedThreadPoolBulkhead,来限制下游服务的并发数量

SemaphoreBulkhead#

信号量舱壁(SemaphoreBulkhead)原理

当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。

当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,

如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。

若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。

使用过程

(1)引入BulkHead的依赖

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>

(2)修改配置

resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底
instances:
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s

(3)在应用的控制器方法上加注解

/**
*(船的)舱壁,隔离
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myBulkhead(id);
}
public String myBulkheadFallback(Throwable t)
{
return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}

FixedThreadPoolBulkhead#

固定线程池舱壁(FixedThreadPoolBulkhead)

FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。

当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。

当线程池中无空闲时时,接下来的请求将进入等待队列,

若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,

在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法

使用过程

(1)引入BulkHead的依赖

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>

(2)修改配置

####resilience4j bulkhead -THREADPOOL的例子
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒
thread-pool-bulkhead:
configs:
default:
core-thread-pool-size: 1
max-thread-pool-size: 1
queue-capacity: 1
instances:
cloud-payment-service:
baseConfig: default
# spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离

(3)修改消费者服务的Controller

/**
* (船的)舱壁,隔离,THREADPOOL
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id)
{
System.out.println(Thread.currentThread().getName()+"\t"+"enter the method!!!");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"exist the method!!!");
return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
}
public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t)
{
return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}

值得注意的是,如果使用FixedThreadPoolBulkhead,那必须采用JUC内规定的形式返回

如CompletableFuture

服务限流(RateLimiter)#

限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。

比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。

所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

常见限流算法#

①Leaky Bucket (漏斗算法)

一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。

如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的

leakybucket

缺点=>这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率

②TokenBucket(令牌桶算法)

简言之就是在请求到达时,去判断桶中是否有令牌,没有令牌就直接被丢弃,有令牌则进入阻塞队列,等拿到令牌再处理请求,处理完回收令牌进桶,这也是SpringCloud默认使用的算法

tokenbucket

③TumblingTimeWindow(滚动时间窗口)

允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。

由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。

**缺点=>**由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击。

④SlidingTimeWindow(滑动时间窗口)

简言之,就是经典的滑动窗口理论,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:

- 窗口:需要定义窗口的大小

- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小

滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点

slidingtimewindow

使用过程

(1)引入ratelimiter的依赖

<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>

(2)修改配置

####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default

(3)在服务提供层加入限流方法

//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
return "Hello, myRatelimit欢迎到来 inputId: "+id+" \t " + IdUtil.simpleUUID();
}

(4)使用OpenFeign或者LoadBalancer保证服务调用可行性

/**
* Resilience4j Ratelimit 的例子 OpenFeign
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);

(5)在服务调用层编写Controller,使用@RateLimiter进行限流备注

@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t)
{
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}

五、Micrometer+ZipKin分布式链路追踪#

在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。

分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

Sleuth

之前业内用的一款分布式链路追踪监控工具,能做到分布式追踪、日志增强、采样策略吗,还能和其他SpringCloud组件进行联动,但随着技术的更新与迭代,它首先被整合进了SpringBoot Actuator,然后SpringCloud团队推荐使用新一代分布式追踪库Micrometer Tracing,所以现在基本不再使用Sleuth

Micrometer#

Micrometer 是一个用于度量指标收集的库,它旨在为各种监控系统提供统一的接口

micrometer

业内常见的解决方案#

tracing

流程及原理#

首先通过微服务之间的调用顺序生成链路,

然后链路通过TraceId唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来 (Span:表示调用链路来源,通俗的理解span就是一次请求信息)

ZipKin#

Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。

六、GateWay#

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。

实现功能#

①反向代理

②鉴权

③流量控制

④熔断

⑤日志监控

Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。

三大核心组件#

①Route

路由是网关的基本组件,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为真,则匹配路由并放行

②Predicate

参考Java8的Predicate,就是用来实现判断的组件,可以通过断言来对请求的一系列参数进行判断,如若匹配,就进行路由匹配

③Filter

就是Spring中GateWayFilter ,可以用过滤器在路由前后对请求进行修改

TIPS:总的来说,一个请求进来,先走一遍断言,这个请求对我来说是合法的,我就让他过去,然后过滤器,做一点修改,最后才让他匹配上我服务的地址,可能匹配完还得走过滤器

具体使用过程#

引入依赖

首先需要引入GateWay的坐标依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

②配置文件

server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由

③在服务调用者中将调用目标换成GateWay

@FeignClient(value = "cloud-gateway")//自己人内部,自己访问自己,写微服务名字OK
public interface PayFeignApi
{
/**
* GateWay进行网关测试案例01
* @param id
* @return
*/
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id);
/**
* GateWay进行网关测试案例02
* @return
*/
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo();
}

高级特性#

①Path动态获取URI地址#

可以让GateWay去服务注册中心去寻找提供服务的地址,前提是必须提供这个服务在中心注册的名字

gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由

②断言配置#

可以通过配置GateWay提供的一系列断言来实现对请求的拦截,其中GateWay自己内置了10个断言,也提供了抽象类和接口供开发者自定义断言

predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- After=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai] #只有在xx之后的发起的请求可以通过
- Before=2023-11-27T15:25:06.424566300+08:00[Asia/Shanghai] #超过规定时间不可访问
- Between=2023-11-21T17:38:13.586918800+08:00[Asia/Shanghai],2023-11-22T17:38:13.586918800+08:00[Asia/Shanghai]
- Cookie=username,zzyy
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Host=**.atguigu.com
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
- RemoteAddr=192.168.124.1/24 # 外部访问我的IP限制,最大跨度不超过32,目前是1~24它们是 CIDR 表示法。
- Metohd=GET,POST

自定义断言

①新建断言工厂,类名应遵从xxxRoutePredicateFactory,并集成AbstractRoutePredicateFactory

@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config>
{
}

②重写apply方法

@Override
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config)
{
return new Predicate<ServerWebExchange>()
{
@Override
public boolean test(ServerWebExchange serverWebExchange)
{
//检查request的参数里面,userType是否为指定的值,符合配置就通过
String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
if (userType == null) return false;
//如果说参数存在,就和config的数据进行比较
if(userType.equals(config.getUserType())) {
return true;
}
return false;
}
}
}

③新建内部类拱apply方法调用

@Validated
public static class Config{
@Setter
@Getter
@NotEmpty
private String userType; //钻、金、银等用户等级
}

④空参构造,调用super

public MyRoutePredicateFactory()
{
super(MyRoutePredicateFactory.Config.class);
}

⑤编写短例适配

@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("userType");
}

过滤器#

说白了就是SpringMVC中的Interceptor,我感觉和AOP一样,是对Servlet中的过滤器的另一种封装,可以对请求,在到达前后进行一些操作,修改

用途

请求鉴权、异常处理等,可以用于接口耗时统计,非常有用

分类

请求头

①The AddRequestHeader GateWayFilter Factory -6.1

filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1
- AddRequestHeader=X-Request-atguigu2,atguiguValue2

②The RemoveRequestHeader GateWayFilter Factory -6.18

- RemoveRequestHeader=sec-fetch-site

③The SetRequestHeader GateWayFilter Factory -6.29

- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy

请求参数

①The AddRequestParameter GateWayFilter Factory -6.3

②The RemoveRequestParameter GateWayFilter Factory -6.19

- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v
- RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null

响应头

①The AddResponseHeader GateWayFilter Factory -6.4

- AddResponseHeader=X-Response-atguigu, BlueResponse # 新增请求参数X-Response-atguigu并设值为BlueResponse

②The SetResponseHeader GateWayFilter Factory -6.30

- SetResponseHeader=Date,2099-11-11 # 设置回应头Date值为2099-11-11

③The RemoveResponseHeader GatewayFilter Factory -6.20

- RemoveResponseHeader=Content-Type # 将默认自带Content-Type回应属性删除

前缀路径

①The PrefixPath GatewayFilter Factory -6.14

- Path=/gateway/filter/**
filters:
- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter

②The SetPath GateWayFilter Factory -6.29

- Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代
- SetPath=/pay/gateway/{segment} # {segment}表示占位符,你写abc也行但要上下一致

③The RedirectTo GateWayFilter Factory -6.16

- Path=/pay/gateway/filter/** # 真实地址
- RedirectTo=302, http://www.atguigu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/

自定义全局过滤器

Example-自定义全局接口调用耗时统计

① 新建过滤器类并实现GlobalFilter和Ordered两个接口

package com.atguigu.cloud.mygateway;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered
{
/**
* 数字越小优先级越高
* @return
*/
@Override
public int getOrder()
{
return 0;
}
private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null){
log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("我是美丽分割线: ###################################################");
System.out.println();
}
}));
}
}

②在yml中配置

server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- After=2023-12-30T23:02:39.079979400+08:00[Asia/Shanghai]
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
- id: pay_routh3 #pay_routh3
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由,默认正确地址
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 # 请求头kv,若一头含有多参则重写一行设置

自定义条件过滤器

①新建Filter类,命名要遵从规则,以GatewayFilterFactory结尾并集成AbstractGatewayFilterFactory类

@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
}

②新建xxxGatewayFilterFactory.Config内部类

@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
public static class Config {
@Setter @Getter
private String status;
}
}

③重写apply、shortcutFieldOrder方法

@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config)
{
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
System.out.println("进入自定义网关过滤器MyGatewayFilterFactory,status===="+config.getStatus());
if(request.getQueryParams().containsKey("atguigu")) {
return chain.filter(exchange);
}else {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
}
};
}
@Override
public List<String> shortcutFieldOrder() {
List<String> list = new ArrayList<String>();
list.add("status");
return list;
}

④空参构造,调用super

public MyGatewayFilterFactory() {
super(MyGatewayFilterFactory.Config.class);
}

SpringCloud Alibaba#

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

组件一览

Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。

Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

alibaba

一、Nacos#

就是sca里的consul,也是一个注册中心兼配置发放中心,官网说它是⼀个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,给我感觉就是我们中国人自己的Consul,中国人不骗中国人

nacos

服务注册中心#

①引入依赖

<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

②写配置

server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址

配置中心#

①引入依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

②写配置

bootstrap.yml

# nacos配置
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml

application.yml

server:
port: 3377
spring:
profiles:
active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境

数据模型NameSpace-Group-DataId#

问题1:

实际开发中,通常一个系统会准备

dev开发环境

test测试环境

prod生产环境。

如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,

每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…

那怎么对这些微服务配置进行分组和命名空间管理呢?

由此便提出了数据模型NameSpace-Group-DataId

nacosdm

其实没什么好解释的,就是按照命名空间、分组、数据id来给配置划分,在yml中给nacos做配置就行,如果使用了NameSpace 和 GroupID,那记得要给nacos写配置,不然只会去默认的deafult和public去找配置文件

二、Sentinel#

Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

其实就是Alibaba的CircuitBreaker,干的也是一样的事情

实现功能#

①(避免)服务雪崩

​ 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

​ 所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务降级

服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。

服务熔断

在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。

服务限流

服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。

限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。

服务隔离

有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。

服务超时

形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。

流量控制#

流控模式#

直接

资源名即是访问路径,根据路径决定流控的资源

关联

根据关联路径的资源决定当前资源的流控

链路

光说比较复杂,就是在项目中,标明@SentinelResource的接口会被Sentinel认定为一个资源,然后使用或经过这个资源的调用链将会被认定为一个链路,可以对每一条链路设定具体流控

流控效果#

快速失败

流控达效,直接返回失败数据 (PS:可自定义兜底)

②WarmUp

预热流控,会有一个coldFactor,初始值为3,这个讲起来也挺抽象的,如果你设置的单机阈值为10,预热时长是5,那在5s内,每次的单击阈值只能为10/3=3,qps超过3直接报错,然后到了3s后,就会恢复到10,相当于做了一个慢速适应

③ **排队等待 **

就是严格遵从单机阈值,如果大量请求打过来,如果单机阈值为10,那么只允许100ms内通过一个请求,所主要用于处理间隔性突发的流量,例如消息队列,所以不支持QPS>1000的场景

Extra:并发线程数

如果选择并发线程数,那么将无法选择流控效果,因为并发线程数默认选择快速失败作为流控效果

熔断降级#

①慢比例调用#

image-20241006154549294

对指定路径的资源进行熔断,当对此资源访问的请求响应时间超过最大RT时,视为一次慢调用,在统计时长内,如果请求的数量超过了最小请求数,并且其中慢调用的数量占比超过了比例阈值,那么将会触发对此资源的熔断,持续熔断时长秒。

②异常比例调用#

image-20241006163349279

当调用触发异常,在统计时长内,如果异常调用占总调用数的比例超过比例阈值,并且总调用数大于最小请求数,就会熔断,持续熔断时长。

③异常数#

更不用说了,在统计时长中,如果总调用数大于最小请求数,并且异常调用大于异常数,就会触发熔断,持续熔断时长。

@SentinelResource#

SentinelResource是一个流量防卫防护组件的注解,用于指定防护资源,对配置的资源进行流量控制、熔断降级等功能

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
//资源名称
String value() default "";
//entry类型,标记流量的方向,取值IN/OUT,默认是OUT
EntryType entryType() default EntryType.OUT;
//资源分类
int resourceType() default 0;
//处理BlockException的函数名称,函数要求:
//1. 必须是 public
//2.返回类型 参数与原方法一致
//3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
String blockHandler() default "";
//存放blockHandler的类,对应的处理函数必须static修饰。
Class<?>[] blockHandlerClass() default {};
//用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所
//有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
//1. 返回类型与原方法一致
//2. 参数类型需要和原方法相匹配
//3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
String fallback() default "";
//存放fallback的类。对应的处理函数必须static修饰。
String defaultFallback() default "";
//用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进
//行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
//1. 返回类型与原方法一致
//2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
//3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
Class<?>[] fallbackClass() default {};
//需要trace的异常
Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
//指定排除忽略掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

流控最佳实践

@SentinelResource + 资源名称限流 + 自定义限流返回 + 服务降级处理

@GetMapping("/rateLimit/doAction/{p1}")
@SentinelResource(value = "doActionSentinelResource",
blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
public String doAction(@PathVariable("p1") Integer p1) {
if (p1 == 0){
throw new RuntimeException("p1等于零直接异常");
}
return "doAction";
}
public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){
log.error("sentinel配置自定义限流了:{}", e);
return "sentinel配置自定义限流了";
}
public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){
log.error("程序逻辑异常了:{}", e);
return "程序逻辑异常了"+"\t"+e.getMessage();
}

在遇到异常的时候,使用降级处理,在遇到高峰qps的时候,选择流控处理。

热点规则#

热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作

使用很简单,就是针对某一个接口,对他的某个入参进行标注,声明这是我的热点,如果携带热点访问,则会接受流控处理

hotpoint

热点例外项#

就是为某个热点设置例外,虽然携带了热点,但是如果热点的值为xx,则不受流控处理

hotpointExclude

授权规则#

在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。

在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型。白放行、黑禁止

其实给我感觉就是Sentinel的鉴权

实现起来也很简单,只需要往Spring里丢一个实现了RequestOriginParser的bean就可以

@Component
public class MyRequestOriginParser implements RequestOriginParser
{
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("serverName");
}
}

然后再Sentinel的控制台里对某个簇点链路进行授权配置,要写入由Parser解析出的值是哪些被设定为黑/白名单,这里可玩性就很强了,因为可以通过请求拿到各种数据,自定义性极强

auth

授权持久化#

​ 每次重启微服务,Sentinel规则都会消失,所以在生产环境下要对规则进行持久

主要采用nacos存储Sentinel的规则

①引入依赖

<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

②写配置

spring:
application:
name: cloudalibaba-sentinel-service #8401微服务提供者后续将会被纳入阿里巴巴sentinel监管
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType

③写nacos配置

persis

[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
resource:资源名称;
limitApp:来源应用;
grade:阈值类型,0表示线程数,1表示QPS;
count:单机阈值;
strategy:流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
clusterMode:是否集群。

OpenFeign集成Sentinel实现FallBack服务降级#

如果使用OpenFeign实现远程服务调用,那么fallback的实现必须在调用方法旁写fallback降级方法,但如果业务复杂,有很多远程调用方法,那么就要写很多fallback方法,但一般来说很多降级方法都是很通用的,无非是告诉用户再等等,安慰一下,所以我们要把它抽离出来

①在服务提供者、接口模块、服务消费者中都引入Sentinel和OpenFeign

<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

②在提供者业务层,只编写流控相关的降级

③ 在接口模块中,编写并为接口指定fallback服务降级处理类

值得注意的是,必须先确定好SpringBoot和SpringCloudAlibaba的版本,因为SpringCloudAlibaba的版本更新缓慢,常常会出现不支持的现象,这种现象在Sentinel和OpenFeign整合的场景出现得最为频繁

GateWay集成Sentinel实现服务限流#

为了解决和实现从网关层面的限流,Sentinel一般都是有服务跑过才能看到簇点链路,再进行流控等配置,但这在生产上明显不符合常理,应该从代码层面就进行配置,而且有一般的微服务架构都有网关,也就是说真正服务的提供端口根本不会被访问,所以更迫切地需要GateWay和Sentinel之间的整合

①重构网关服务

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>

引入Gateway、Sentinel的适配和servlet

server:
port: 9528
spring:
application:
name: cloudalibaba-sentinel-gateway # sentinel+gataway整合Case
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:9001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/** # 断言,路径相匹配的进行路由

编写微服务配置

②编写配置

/**
* 使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer)
{
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct //javax.annotation.PostConstruct
public void doInit() {
initBlockHandler();
}
//处理/自定义返回的例外信息
private void initBlockHandler() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String,String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流(sentinel+gataway整合Case)");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}
}

三、Seata#

Seata出现的原因只有一个——为了解决分布式微服务场景下事务的统一性一致性问题。 在微服务场景下,基本不可能只使用一个数据库,如果一个业务涉及到多个数据库的事务操作,那就会很混乱很糟糕。因为现在的数据库如mysql,大多都是单机事务,所以迫切需要一门能统一业务内事务管理的技术,seata就应运而生了

举个栗子

orderExample

在订单支付成功后,交易中心会调用订单中心的服务把订单状态更新,并调用物流中心的服务通知商品发货,同时还要调用积分中心的服务为用户增加相应的积分。如何保障分布式事务一致性,成为了确保订单业务稳定运行的核心诉求之一。

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,

业务操作需要调用三个服务来完成。

此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

Seata内部结构#

在Seata工作的时候,一般将它运行的三个重要组成部分分成TC、TM、RM

XID:全局事务的唯一标识,它可以在服务的调用链路中被传递,绑定到事务的事务上下文中

TC:Transaction Coordinator 事务调停者 ,Seata最重要的部分,负责维护全局事务和分支事务的状态,驱动全局事务的提交或回滚

TM:Transaction Manager 事务管理者,说白了就是打注解 @GlobalTransactionl 的地方,是整个分布式事务的入口,是事务的发起者,负责定义全局事务的范围,并根据TC维护点的全局事务和分支事务状态来参与事务提交和回滚的决议

RM:Resource Manager 资源管理者,说白了就是一个个数据库,提供数据资源,管理数据资源的地方,负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚

Seata工作流程#

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;

  2. XID 在微服务调用链路的上下文中传播;

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;

  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

seataProcedure

Seata事务模式#

Seata 支持多种事务模式来满足不同的业务场景需求

  1. AT (Automatic Transaction) 模式
    • 用途:适用于基于 JDBC 的应用,不需要对业务代码进行大量改造。
    • 特点:自动处理事务边界内的 SQL 语句,通过代理数据源实现两阶段提交(2PC)。
    • 工作原理:在第一阶段,Seata 会记录事务中的 SQL 操作,并保存回滚日志;在第二阶段,根据全局事务的状态决定是提交还是回滚。
  2. TCC (Try-Confirm-Cancel) 模式
    • 用途:适用于不使用数据库资源管理器或者需要更精细控制事务流程的场景。
    • 特点:用户需要定义三个操作:Try 用于预留资源,Confirm 用于确认执行,Cancel 用于取消预留。
    • 工作原理:在 Try 阶段锁定所需资源;Confirm 阶段真正执行业务逻辑;如果出现异常,则 Cancel 阶段释放 Try 阶段预留的资源。
  3. SAGA 模式
    • 用途:适合于长事务、涉及多个服务调用的场景。
    • 特点:将长事务分解成一系列本地短事务,每个短事务都有对应的补偿事务。
    • 工作原理:当某一步失败时,通过执行前面各步骤对应的补偿事务来回滚已完成的操作,从而达到最终一致性。
  4. XA 模式
    • 用途:传统方式下实现分布式事务的一种标准方法。
    • 特点:直接利用数据库本身的 XA 接口来管理跨多个数据库的事务。
    • 工作原理:符合 X/Open XA 规范,采用两阶段提交协议确保所有参与的资源管理者要么全部提交,要么全部回滚。
SpringCloud学习笔记
https://fuwari.vercel.app/posts/spring_cloud_learn/
作者
Simon
发布于
2024-10-17
许可协议
CC BY-NC-SA 4.0