写在前文:hystrix停止维护,不做总结;
本文主要总结sentinel和resilience4j这两个框架;另外额外补充面试可能会问到的限流算法;
其他文章
服务容错治理框架resilience4j&sentinel基础应用---微服务的限流/熔断/降级解决方案-CSDN博客
快速搭建对象存储服务 - Minio,并解决临时地址暴露ip、短链接请求改变浏览器地址等问题-CSDN博客
使用LangGraph构建多代理Agent、RAG-CSDN博客
大模型LLMs框架Langchain之链详解_langchain.llms.base.llm详解-CSDN博客
大模型LLMs基于Langchain+FAISS+Ollama/Deepseek/Qwen/OpenAI的RAG检索方法以及优化_faiss ollamaembeddings-CSDN博客
大模型LLM基于PEFT的LoRA微调详细步骤---第二篇:环境及其详细流程篇-CSDN博客
大模型LLM基于PEFT的LoRA微调详细步骤---第一篇:模型下载篇_vocab.json merges.txt资源文件下载-CSDN博客 使用docker-compose安装Redis的主从+哨兵模式_使用docker部署redis 一主一从一哨兵模式 csdn-CSDN博客
docker-compose安装canal并利用rabbitmq同步多个mysql数据_docker-compose canal-CSDN博客
目录
[Config配置 --- 后续这个不生效,使用的是application.yml中的配置](#Config配置 --- 后续这个不生效,使用的是application.yml中的配置)
[Java代码中的配置RateLimiterRegistry Bean](#Java代码中的配置RateLimiterRegistry Bean)
[Step4、隔板Bulkhead --- 也可以当并发限制](#Step4、隔板Bulkhead --- 也可以当并发限制)
限流算法
漏桶算法
原理:系统内部维护一个固定的容器,请求到达时,如果桶未满,就存入桶中,如果桶满了,就拒绝请求;以恒定速率处理请求;
优势:速率可控,输出流量平滑;
缺点:无法应对合理突发流量、临界值突变依然有问题
计数器算法
原理:将时间划分为固定窗口(比如1s一个窗口),在窗口内,维护一个计数器,每次请求来时,计数器+1,如果没有超过限制阈值那么就允许通过,否则就拒绝请求;时间窗口滑动时重置计数器;
缺点:如果0.8s到1s请求来了100个,其余时间为0,1s~1.2s请求来了100个其余时间为0,那么此时0.8s~1.2s总供处理了200个请求,但是0~0.8以及1.2~2s并没有处理;
令牌桶算法
原理:令牌生成器以固定速率向桶中添加令牌- --- 比如桶最大容量限制100个,有请求来时,获取获取一个令牌token,如果有令牌token那么允许通过,如果没有那么就拒绝请求/排队等待;
优势:可以应对突发流量(比如令牌生成速度为1s一个,桶容量为100个,那么峰值就是100个,往后即便流量再多也只能每秒获取一个token)
特征 | 计数器算法 | 漏桶算法 | 令牌桶算法 |
---|---|---|---|
流量突发处理 | ❌ 窗口临界问题 | ❌ 严格限制 | ✅ 允许合理突发 |
流量平滑性 | ❌ 窗口突变 | ✅ 绝对平滑 | ✅ 可配置平滑度 |
系统资源消耗 | ✅ 低 | ✅ 低 | ✅ 中等 |
实现复杂度 | ✅ 简单 | ✅ 中等 | ⚠️ 较高 |
实时调整能力 | ❌ 困难 | ⚠️ 一般 | ✅ 灵活 |
典型应用 | 简单接口限流 | 硬件设备控制 | 互联网高并发系统 |
resilience4j与sentinel
sentinel比较重(功能比较全面,但是配置项复杂、部署维护成本比较大),而resilience4j是轻量化(功能虽少,但是核心功能--降级/熔断/限流/重试/并发隔离功能都有);
在业务中如何选择,可以从下面几个方面考虑:
1、系统是否采用springcloud alibaba技术体系;
2、团队人数;
3、学习成本;
resilience4j+springboot3环境搭建
Step1、环境准备
本文所有配置、依赖外部服务均在这里,后续不做详细解释
引入依赖
XML
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 要使用resilience4j就必须要引入下面的AOP不然不会生效 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
配置application.yml
java
server:
port: 8080
servlet:
context-path: /fault
spring:
application:
name: fault-guard
# 如果要使用type = Bulkhead.Type.THREADPOOL线程隔离需要配置,不然系统会采用默认参数
# task:
# execution:
# pool:
# core-size: 5
# max-size: 10
# queue-capacity: 100
management:
endpoint:
health:
show-details: always # 设置为"always"意味着健康检查详情总是显示给所有访问者,包括详细的状态信息。
endpoints:
web:
exposure:
# 配置完下面以后就可以查看具体状态:
### 查看Resilience4j熔断器状态:GET http://localhost:8080/fault/4j/actuator/circuitbreakers
### 查看Resilience4j限流器状态:GET http://localhost:8080/fault/actuator/ratelimiters
### 查看Resilience4j隔板状态:GET http://localhost:8080/fault/actuator/bulkheads
### 查看Resilience4j重试状态:GET http://localhost:8080/fault/actuator/retries
### 查看Resilience4j超时状态: GET http://localhost:8080/fault/actuator/timelimiters
include: health,circuitbreakers,ratelimiters,bulkheads,retries,timelimiters # 指定要通过HTTP暴露的端点列表。
resilience4j:
# 限速 - Rate Limiter
ratelimiter:
metrics:
enabled: true
instances:
rateLimiterApi: # 自定义的限流器实例名称,该名称会在代码中引用。使用时:@RateLimiter(name="rateLimiterApi")
register-health-indicator: true # 注册健康指标,可用于健康检查端点
# 60s以内,超过1次,即第2次就会被限流
limit-for-period: 1 # 每个时间段允许的请求数(即限流阈值)
limit-refresh-period: 60s # 限流刷新时间间隔(这里是60秒)
timeout-duration: 0s # 获取许可的最大等待时间为0秒(即不等待)
allow-health-indicator-to-fail: true # 即使限流触发失败,健康检查也视为通过
subscribe-for-events: true # 启用事件订阅,可用于监听限流事件
event-consumer-buffer-size: 50 # 限流事件消费者的缓冲区大小
# baseConfig: default # 可以基础配置为default,这样就可以不用上面一项一项配置
# compositeApi: # 配置多个,在使用的时候就可以@RateLimiter(name="compositeApi")
# baseConfig: default
# 熔断 - Circuit breaker
circuitbreaker:
metrics:
enabled: true # 启用指标收集,可以与Micrometer等监控系统集成
instances:
circuitBreakerApi: # 自定义的断路器实例名称,该名称会在代码中引用。使用时:@CircuitBreaker(name = "circuitBreakerApi", fallbackMethod = "circuitBreakerFallback")
# 过去10次请求中,如果超过了50%(至少6次)失败,则断路器变为OPEN状态;此时服务只能进入到Controller,无法进入到Service
failure-rate-threshold: 50 # 触发断路的失败请求比例阈值(%)。例如设置为50表示失败率达到50%时触发断路
minimum-number-of-calls: 5 # 在一个滑动窗口内最少需要多少次调用才会计算失败率
# 当断路器处于OPEN状态时,所有请求直接失败,不真实调用业务逻辑。并且5秒后自动变为 HALF-OPEN 状态。
automatic-transition-from-open-to-half-open-enabled: true # 是否启用自动从 OPEN 状态切换到 HALF-OPEN 状态
wait-duration-in-open-state: 5s # 当断路器处于 OPEN 状态时,等待多久后尝试进入 HALF-OPEN 状态
# 在 HALF-OPEN 状态下。最多允许 3个请求通过 去测试系统是否恢复。如果这3次请求都成功,则变为 CLOSED 状态;如果还有失败,则重新回到 OPEN。
permitted-number-of-calls-in-half-open-state: 3 # 在 HALF-OPEN 状态下允许处理的请求数
sliding-window-size: 10 # 滑动窗口的长度,取决于 sliding-window-type 是次数还是时间
sliding-window-type: count_based # 滑动窗口类型,可选值:count_based(基于请求次数) time_based(基于时间窗口)
# baseConfig: default
# compositeApi:
# baseConfig: default
# 隔板 - Bulkhead
bulkhead:
metrics:
enabled: true
instances:
bulkheadApi: # 自定义的舱壁实例名称,该名称会在代码中引用 @Bulkhead(name = "bulkheadApi", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "bulkheadFallback")
max-concurrent-calls: 2 # 允许的最大并发调用数量。意味着最多同时有2个线程可以进入并执行被保护的方法或服务调用。
max-wait-duration: 1 # 请求在进入舱壁前等待的最大时间,如果隔板已满,每个线程只能等待 1 毫秒
# baseConfig: default
# compositeApi:
# baseConfig: default
# 超时 - Time Limiter
timelimiter:
metrics:
enabled: true
instances:
timeLimiterApi: # 自定义的超时实例名称,该名称会在代码中引用@TimeLimiter(name = "timeLimiterApi", fallbackMethod = "timeLimiterFallback")
timeout-duration: 2s # 设置超时时间为2秒,即当操作超过这个时间还未完成,则认为超时。
cancel-running-future: true # 当设置为true时,若操作超时,则尝试取消正在执行的操作(如果支持的话)。
# baseConfig: default
# compositeApi:
# baseConfig: default
# 重试 - Retry
retry:
metrics:
enabled: true # 是否开启度量统计,默认为false。如果设置为true,则会收集并报告此Retry的相关指标。
legacy:
enabled: true # 是否启用遗留的度量方式。这通常用于向后兼容旧版本的监控和度量系统。
instances:
retryApi: # 自定义的重试实例名称,该名称会在代码中引用
max-attempts: 3 # 最大重试次数,包含初次尝试在内共尝试3次。
wait-duration: 1s # 每次重试之间等待的时间间隔,此处设置为1秒。
# baseConfig: default
# compositeApi:
# baseConfig: default
Config配置 --- 后续这个不生效,使用的是application.yml中的配置
只做展示
java
/**
* 可以直接全部在YML中配置,而不使用Resilience4jConfig;
* 我们一旦统一定义了**Registry以后,我们在使用的时候貌似只能同时使用一个..
* 比如:当我们定义了RateLimiterRegistry以后,我们在使用的时候的就可以"@RateLimiter(name="xxx")",这个xxx随便什么名字都会被应用到这个bean的配置,好像也只能这一个配置...
*/
//@Configuration
public class Resilience4jConfig {
// 限流配置
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(60)) // 刷新周期是多少秒
.limitForPeriod(1) // 每个周期内允许多少个请求
.timeoutDuration(Duration.ofMillis(0)) // 等待时间
.build();
// return RateLimiterRegistry.of(config);
// 手动注册实例 "rateLimiterApi" --- 但是我测试以后,系统好像会自动注入...不需要自己单独再定义,rateLimiterApi即可生效
// 如果是想使用自定义实例名称,那么使用下面的方法即可
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
registry.rateLimiter("rateLimiterApi(自定义名称)", config);
return registry;
}
// 熔断器配置
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值百分比
.waitDurationInOpenState(Duration.ofSeconds(5)) // 熔断后等待时间
.permittedNumberOfCallsInHalfOpenState(2) // 半开状态允许的调用次数
.slidingWindowSize(5) // 滑动窗口大小
.recordExceptions(Exception.class) // 记录哪些异常
.build();
return CircuitBreakerRegistry.of(config);
}
// 超时配置
@Bean
public TimeLimiterRegistry timeLimiterRegistry() {
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2)) // 超时时间2秒
.build();
return TimeLimiterRegistry.of(config);
}
// 隔板配置
@Bean
public BulkheadRegistry bulkheadRegistry() {
BulkheadConfig config = BulkheadConfig.custom()
.maxConcurrentCalls(3) // 最大并发数
.maxWaitDuration(Duration.ofMillis(500)) // 等待时间
.build();
return BulkheadRegistry.of(config);
}
// 重试配置
@Bean
public RetryRegistry retryRegistry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3) // 最大重试次数
.waitDuration(Duration.ofMillis(500)) // 重试间隔
.retryExceptions(RuntimeException.class) // 重试哪些异常
.build();
return RetryRegistry.of(config);
}
}
定义全局处理类
通过@RestControllerAdvice定义全局异常处理类
java
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler({CallNotPermittedException.class})
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public String handleCallNotPermittedException() {
System.out.println("熔断异常全局处理");
return "熔断异常全局处理";
}
@ExceptionHandler({TimeoutException.class})
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public void handleTimeoutException() {
System.out.println("超时异常全局处理");
}
@ExceptionHandler({BulkheadFullException.class})
@ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
public void handleBulkheadFullException() {
System.out.println("隔板异常全局处理");
}
@ExceptionHandler({RequestNotPermitted.class})
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public String handleRequestNotPermitted() {
System.out.println("限流异常全局处理");
return "限流异常全局处理";
}
}
定义模拟外部API调用类
java
@Service
public class ExternalApiService {
// 模拟外部API调用,有概率失败或延迟
public String callExternalApi(boolean shouldFail) throws InterruptedException {
Random random = new Random();
int delay = random.nextInt(10000); // 随机延迟0-10秒 --- 测试时,可以根据接口适当调动
System.out.println("进入ExternalApiService");
TimeUnit.MILLISECONDS.sleep(delay);
if (shouldFail) {
throw new RuntimeException("External API call failed");
}
return "External API response after " + delay + "ms";
}
}
Step2、限流RateLimiter
本文只展示限流这一种比较详细的使用,其余的都是直接上代码以及开发中遇到的坑
配置参数有2种配置方法,一种是在application.yml中配置,一种是在Java代码中配置"*Registry ";
配置优先级:代码中的 *Registry Bean会覆盖了YAML配置 ---- 创建*RegistryBean配置方法只展示限流,其余不展示;
在 Spring Boot 中,如果你通过 @Bean 手动创建了 RateLimiterRegistry,YAML 中的配置会被完全忽略。此时 application.yml 中的 resilience4j.ratelimiter 配置不会生效
要使用YML配置生效,我们只需要把RateLimiterRegistry注销即可,不注入Spring即可。
Application.yml中有两个配置
1、一种是配置baseConfig为default默认值;
resilience4j.ratelimiter.metrics.enabled=true
resilience4j.ratelimiter.instances.rateLimiterApi(自定义的限流器实例名称).baseConfig=default
resilience4j.ratelimiter.instances.otherApi(自定义的限流器实例名称).baseConfig=default
2、一种是直接配置具体的参数:
java
# 注意:rateLimiterApi是限流器实例名称,可以任意取;同样的可以配置多个实例
resilience4j.ratelimiter.metrics.enabled=true # 是否启用
resilience4j.ratelimiter.instances.rateLimiterApi.register-health-indicator=true # 注册健康指标,可用于健康检查端点
resilience4j.ratelimiter.instances.rateLimiterApi.limit-for-period=1 # 每个时间段允许的请求数(即限流阈值)
resilience4j.ratelimiter.instances.rateLimiterApi.limit-refresh-period=60s # 限流刷新时间间隔(这里是60秒)
resilience4j.ratelimiter.instances.rateLimiterApi.timeout-duration=0s # 获取许可的最大等待时间为0秒(即不等待)
resilience4j.ratelimiter.instances.rateLimiterApi.allow-health-indicator-to-fail=true # 即使限流触发失败,健康检查也视为通过
resilience4j.ratelimiter.instances.rateLimiterApi.subscribe-for-events=true # 启用事件订阅,可用于监听限流事件
resilience4j.ratelimiter.instances.rateLimiterApi.event-consumer-buffer-size=50 # 限流事件消费者的缓冲区大小
resilience4j.ratelimiter.instances.otherApi1.register-health-indicator=true # 注册健康指标,可用于健康检查端点
resilience4j.ratelimiter.instances.otherApi1.limit-for-period=5 # 每个时间段允许的请求数(即限流阈值)
resilience4j.ratelimiter.instances.otherApi1.limit-refresh-period=60s # 限流刷新时间间隔(这里是60秒)
resilience4j.ratelimiter.instances.otherApi1.timeout-duration=0s # 获取许可的最大等待时间为0秒(即不等待)
resilience4j.ratelimiter.instances.otherApi1.allow-health-indicator-to-fail=true # 即使限流触发失败,健康检查也视为通过
resilience4j.ratelimiter.instances.otherApi1.subscribe-for-events=true # 启用事件订阅,可用于监听限流事件
resilience4j.ratelimiter.instances.otherApi1.event-consumer-buffer-size=50 # 限流事件消费者的缓冲区大小
resilience4j.ratelimiter.instances.otherApi2.register-health-indicator=true # 注册健康指标,可用于健康检查端点
resilience4j.ratelimiter.instances.otherApi2.limit-for-period=5 # 每个时间段允许的请求数(即限流阈值)
resilience4j.ratelimiter.instances.otherApi2.limit-refresh-period=60s # 限流刷新时间间隔(这里是60秒)
resilience4j.ratelimiter.instances.otherApi2.timeout-duration=0s # 获取许可的最大等待时间为0秒(即不等待)
resilience4j.ratelimiter.instances.otherApi2.allow-health-indicator-to-fail=true # 即使限流触发失败,健康检查也视为通过
resilience4j.ratelimiter.instances.otherApi2.subscribe-for-events=true # 启用事件订阅,可用于监听限流事件
resilience4j.ratelimiter.instances.otherApi2.event-consumer-buffer-size=50 # 限流事件消费者的缓冲区大小
Java代码中的配置RateLimiterRegistry Bean
java
/**
* 可以直接全部在YML中配置,而不使用Resilience4jConfig;
* 因为我们一旦统一定义了**Registry以后,我们在使用的时候貌似只能同时使用这一个配置...
* 比如:当我们定义了RateLimiterRegistry以后,我们在使用的时候的就可以"@RateLimiter(name="xxx")",这个xxx随便什么名字都会被应用到这个bean的配置,好像也只能这一个配置...
*/
@Configuration
public class Resilience4jConfig {
// 限流配置
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(60)) // 刷新周期是多少秒
.limitForPeriod(1) // 每个周期内允许多少个请求
.timeoutDuration(Duration.ofMillis(0)) // 等待时间
.build();
// return RateLimiterRegistry.of(config);
// 手动注册实例 "rateLimiterApi" --- 但是我测试以后,系统好像会自动注入...不需要定义...rateLimiterApi
// 如果是想使用自定义实例名称,那么使用下面的方法即可
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
registry.rateLimiter("rateLimiterApi(自定义名称)", config);
return registry;
}
}
限流器使用
coontroller
请求地址:XXX/rate-limiter
效果展示:60s内连续请求2次,第二次将被限流;
限流器状态:/actuator/ratelimiters
java
// 正常情况下,按照上面limitRefreshPeriod=60,limitForPeriod=1的配置,1分钟内连续请求1次就会被限制;
@RestController
public class Resilience4JController {
@Autowired
private Resilience4JService resilience4JService;
@GetMapping("/rate-limiter")
public String rateLimiter() {
String uid = UUID.randomUUID().toString().replace("-", "");
long time = System.currentTimeMillis();
System.out.println(uid + ",请求时间: " + time);
String result = resilience4JService.rateLimiterExample(time, uid);
if (("限流示例:" + uid).equals(result)) {
return uid + ",未被限流:" + time;
}
return uid + ",限流:" + time;
}
}
service
如果被限流,那么系统是不会进入到service方法内部...
java
@Service
public class Resilience4JService {
@RateLimiter(name="rateLimiterApi") //如果使用的是创建RateLimiterRegistry,那么此处名字好像无所谓...否则要和application.yml中对应
public String rateLimiterExample(Long time,String uid) {
System.out.println(uid+" 未被限流... " + time);
return "限流示例:"+uid;
}
}
另外一种写法
此种方法如果被限流的话,那么系统返回的是"限流异常全局处理"是全局异常处理获取到的;并且不会进入rateLimiter方法内部
java
@GetMapping("/rate-limiter")
@RateLimiter(name="rateLimiterApi") //如果使用的是创建RateLimiterRegistry,那么此处名字好像无所谓...否则要和application.yml中对应
public String rateLimiter() {
String uid = UUID.randomUUID().toString().replace("-", "");
long time = System.currentTimeMillis();
return uid + ",未被限流:" + time;
}
Step3、熔断CircuitBreaker
请求地址:xxxx/circuit-breaker?shouldFail=true
效果:我们的外部服务模拟直接抛出异常,所以系统会进入到异常处理,返回异常处理信息;
熔断器状态:actuator/circuitbreakers
coontroller
java
@GetMapping("/circuit-breaker")
public String circuitBreaker(@RequestParam(defaultValue = "false") boolean shouldFail) throws InterruptedException {
System.out.println("进入Resilience4JController");
return resilience4JService.circuitBreakerExample(shouldFail);
}
service
本地异常处理、全局异常处理
java
// 熔断示例
@CircuitBreaker(name = "circuitBreakerApi") // 回到全局异常
public String circuitBreakerExample(boolean shouldFail) throws InterruptedException {
System.out.println("进入全局 Resilience4JService");
return externalApiService.callExternalApi(shouldFail);
}
// 使用本地异常
@CircuitBreaker(name = "circuitBreakerApi", fallbackMethod = "circuitBreakerFallback")
public String circuitBreakerLocalFallbackExample(boolean shouldFail) throws InterruptedException {
System.out.println("进入本地loalfallback Resilience4JService");
return externalApiService.callExternalApi(shouldFail);
}
public String circuitBreakerFallback(boolean shouldFail, Exception e) {
return "熔断回退: 本地返回服务不可用";
}
Step4、隔板Bulkhead --- 也可以当并发限制
请求地址:/bulkhead (不使用JMeter);/bulkhead_jmeter(使用JMeter)
状态:/actuator/bulkheads
效果:不使用JMeter时----本文使用的是信号量方式,并且支持并发数为2---即只允许同时处理2个请求,且每个线程只等待1ms,1ms以后没有进入到系统,就会被抛出异常处理里面的内容,并且在通过浏览器直接访问接口的时候,系统会直接返回"测试...";
注意:
要测试bulkhead就需要用JMeter之类的测试工具才有效果,不然的话就可以使用for循环的形式,但是这种方法就没法使用全局异常了...;
resilience4j的Bulkhead默认使用的是Bulkhead.Type.SEMAPHORE信号量,如果要使用Bulkhead.Type.THREADPOOL可以在使用时设置type=Bulkhead.Type.THREADPOOL,但是这种方法需要设置系统默认的线程池参数....
像@Bulkhead、@TimeLimiter这个是强制要求返回值类型为"CompletionStage"不然要报错---这是强制要求
io.github.resilience4j.spring6.timelimiter.configure.IllegalReturnTypeException: java.lang.String com.lting.resilience4j.service.Resilience4JService#timeLimiterExample has unsupported by @TimeLimiter return type. CompletionStage expected. at io.github.resilience4j.spring6.timelimiter.configure.TimeLimiterAspect.proceed(TimeLimiterAspect.java:106) ~[resilience4j-spring6-2.3.0.jar:2.3.0]
controller
java
@GetMapping("/bulkhead/")
public CompletableFuture<String> bulkhead() throws ExecutionException, InterruptedException {
// 因为本例为了测试并发,使用for循环同时发送多个请求;
// 而全局异常拦截的话要同步返回才行,所以在这个案例中的全局异常是不生效的.
for (int i = 0; i < 5; i++) {
resilience4JService.bulkheadLocalFallbackExample().whenComplete((result, throwable) -> {
if (throwable == null) {
System.out.println("返回值:" + result);
} else {
System.out.println("错误:" + throwable.getMessage());
}
});
}
return CompletableFuture.completedFuture("测试...");
}
// 可以使用JMeter工具测试这个接口 --- 这是同步返回结果
@GetMapping("/bulkhead_jmeter/")
public CompletableFuture<String> bulkhead1() throws ExecutionException, InterruptedException {
return resilience4JService.bulkheadExample();
}
service
java
/**
* 隔板示例 -- 并发
*
* @return
* @throws InterruptedException
* @Bulkhead(name = "bulkheadApi") //使用全局异常处理 ----
* @Bulkhead(name = "bulkheadApi", type= Bulkhead.Type.THREADPOOL, fallbackMethod = "bulkheadFallback")
* type: 默认使用的是 Bulkhead.Type.SEMAPHORE,如果使用Bulkhead.Type.THREADPOOL需要配置线程池相关参数:
* 线程池参考:
* spring:
* task:
* execution:
* pool:
* core-size: 5
* max-size: 10
* queue-capacity: 100
* 注意,Bulkhead要求的返回值类型为"CompletionStage",如果返回其他会报错,
* 本案例是为了测试并发,所以在controller中使用的是for循环,请求进来后会直接返回字符串"测试...",是相当于异步并发请求了service;
* 但是全局异常要同步返回才行,所以在本案例中,如果要使用全局异常处理就不能使用"CompletableFuture.supplyAsync";或者将返回值修改为同步,并使用JMeter之类的并发测试工具才能看到效果;
* 不然全局异常要失效,无法拦截,
*
* 具体原因如下: BulkheadFullException 是在 异步线程supplyAsync中抛出的异常,而我的 @ExceptionHandler 只能处理 同步请求主线程中的异常。
* 所以,即使 Resilience4j 抛出了 BulkheadFullException,它也是发生在我的异步执行的那个线程里(比如 ForkJoinPool 或者自己定义的线程池),而不是 Spring MVC 的控制器主线程中。
* 即: 因为是异步,控制器方法 bulkhead() 很快就返回了 。所有异常都发生在异步回调中,不会进入 Spring MVC 的异常处理流程(@ControllerAdvice / @ExceptionHandler)。
*/
// 本地异常返回
// @Bulkhead(name = "bulkheadApi",type = Bulkhead.Type.THREADPOOL, fallbackMethod = "bulkheadFallback")
@Bulkhead(name = "bulkheadApi", fallbackMethod = "bulkheadFallback")
public CompletableFuture<String> bulkheadLocalFallbackExample() throws InterruptedException {
System.out.println("局部并发拦截 进入成功...");
return CompletableFuture.supplyAsync(() -> {
try {
return externalApiService.callExternalApi(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
public CompletableFuture<String> bulkheadFallback(Exception e) {
return CompletableFuture.completedFuture("Bulkhead 回退: 并发请求过多,本地fallback返回");
}
@Bulkhead(name = "bulkheadApi") // 使用JMeter就可以使用全局异常
public CompletableFuture<String> bulkheadExample() throws InterruptedException {
System.out.println("全局并发拦截 进入成功...");
// 同步返回
// return CompletableFuture.completedFuture(externalApiService.callExternalApi(false));
// 异步返回
return CompletableFuture.supplyAsync(() -> {
try {
return externalApiService.callExternalApi(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
Step5、超时
请求地址:/time-limiter
效果: ---- 测试本例时,可以将ExternalApiService里面的休息时间设置长一点,本文设置的超时时间为2s如果2s没有返回那么系统回返回全局异常/本地异常处理里面的内容
状态:/actuator/timelimiters
controller
java
@GetMapping("/time-limiter")
public CompletableFuture<String> timeLimiter() throws InterruptedException {
return resilience4JService.timeLimiterExample();
}
service
java
// 超时示例
/**
* 注意:TimeLimiterl类要求强制返回CompletionStage,如果返回的是String\List什么的话要报错...
* 报错:
* io.github.resilience4j.spring6.timelimiter.configure.IllegalReturnTypeException: xxxxx timeLimiterExample has unsupported by @TimeLimiter return type. CompletionStage expected.
* at io.github.resilience4j.spring6.timelimiter.configure.TimeLimiterAspect.proceed(TimeLimiterAspect.java:106) ~[resilience4j-spring6-2.3.0.jar:2.3.0]
* @return
* @throws InterruptedException
*/
@TimeLimiter(name = "timeLimiterApi") // 使用全局异常
public CompletableFuture<String> timeLimiterExample() throws InterruptedException {
return CompletableFuture.supplyAsync(()->{
try {
return externalApiService.callExternalApi(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
// 超时使用本地异常返回
@TimeLimiter(name = "timeLimiterApi", fallbackMethod = "timeLimiterFallback")
public CompletableFuture<String> timeLimiterLocalFallbackExample() throws InterruptedException {
return CompletableFuture.supplyAsync(()->{
try {
return externalApiService.callExternalApi(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
public CompletableFuture<String> timeLimiterFallback(Exception e) {
return CompletableFuture.completedFuture("时间限制器回退:操作超时,本地返回");
}
Step6、重试Retry
请求地址:/retry?shouldFail=true
效果: 因为我们设置的shouldFail=true,模拟第三方API会一直失败,所以系统重试三次后就会返回异常处理里面的内容;
状态:/actuator/retries
controller
java
@GetMapping("/retry")
public String retry(@RequestParam(defaultValue = "false") boolean shouldFail) throws InterruptedException {
return resilience4JService.retryExample(shouldFail);
}
service
java
// 重试示例
// @Retry(name = "retryApi", fallbackMethod = "retryFallback") // 局部异常
@Retry(name = "retryApi") // 全局异常
public String retryExample(boolean shouldFail) throws InterruptedException {
return externalApiService.callExternalApi(shouldFail);
}
public String retryFallback(boolean shouldFail, Exception e) {
return "重试回退: 太多重试错误";
}
Step7、综合使用
--- 这一步优化
controller
java
@GetMapping("/composite")
public CompletableFuture<String> composite(@RequestParam(defaultValue = "false") boolean shouldFail) {
return resilience4JService.compositeExample(shouldFail);
}
service
注意:compositeApi的参数在application.yml中配置,已经暂时注释掉了,如果有需要开启即可。
java
// 组合使用示例
@CircuitBreaker(name = "compositeApi", fallbackMethod = "compositeFallback")
@RateLimiter(name = "compositeApi")
@Bulkhead(name = "compositeApi")
@Retry(name = "compositeApi", fallbackMethod = "compositeFallback")
@TimeLimiter(name = "compositeApi", fallbackMethod = "compositeFallback")
public CompletableFuture<String> compositeExample(boolean shouldFail) {
return CompletableFuture.supplyAsync(() -> {
try {
return externalApiService.callExternalApi(shouldFail);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
public CompletableFuture<String> compositeFallback(boolean shouldFail, Exception e) {
return CompletableFuture.completedFuture("综合使用回退: " + e.getMessage());
}
Sentinel
.........算了算了。..太多了。不写了。下次写。
瑞士白~~~~...