服务容错治理框架resilience4jsentinel基础应用---微服务的限流/熔断/降级解决方案
写在前文:hystrix停止维护,不做总结;
本文主要总结sentinel和resilience4j这两个框架;另外额外补充面试可能会问到的限流算法;
目录
限流算法
漏桶算法
计数器算法
令牌桶算法
resilience4j与sentinel
resilience4j+springboot3环境搭建
Step1、环境准备
引入依赖
配置application.yml
Config配置 --- 后续这个不生效,使用的是application.yml中的配置
定义全局处理类
定义模拟外部API调用类
Step2、限流RateLimiter
Application.yml中有两个配置
Java代码中的配置RateLimiterRegistry Bean
限流器使用
coontroller
service
另外一种写法
Step3、熔断CircuitBreaker
coontroller
service
Step4、隔板Bulkhead --- 也可以当并发限制
controller
service
Step5、超时
controller
service
Step6、重试Retry
controller
service
Step7、综合使用
controller
service
Sentinel
限流算法
漏桶算法
原理:系统内部维护一个固定的容器,请求到达时,如果桶未满,就存入桶中,如果桶满了,就拒绝请求;以恒定速率处理请求;
优势:速率可控,输出流量平滑;
缺点:无法应对合理突发流量、临界值突变依然有问题
计数器算法
原理:将时间划分为固定窗口(比如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、环境准备
本文所有配置、依赖外部服务均在这里,后续不做详细解释
引入依赖
<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
server:port: 8080servlet: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/timelimitersinclude: health,circuitbreakers,ratelimiters,bulkheads,retries,timelimiters # 指定要通过HTTP暴露的端点列表。
resilience4j:
# 限速 - Rate Limiterratelimiter:metrics:enabled: trueinstances: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 breakercircuitbreaker:metrics:enabled: true # 启用指标收集,可以与Micrometer等监控系统集成instances:circuitBreakerApi: # 自定义的断路器实例名称,该名称会在代码中引用。使用时:@CircuitBreaker(name = "circuitBreakerApi", fallbackMethod = "circuitBreakerFallback")# 过去10次请求中,如果超过了50%(至少6次)失败,则断路器变为OPEN状态;此时服务只能进入到Controller,无法进入到Servicefailure-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
# 隔板 - Bulkheadbulkhead:metrics:enabled: trueinstances: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 Limitertimelimiter:metrics:enabled: trueinstances:timeLimiterApi: # 自定义的超时实例名称,该名称会在代码中引用@TimeLimiter(name = "timeLimiterApi", fallbackMethod = "timeLimiterFallback")timeout-duration: 2s # 设置超时时间为2秒,即当操作超过这个时间还未完成,则认为超时。cancel-running-future: true # 当设置为true时,若操作超时,则尝试取消正在执行的操作(如果支持的话)。# baseConfig: default# compositeApi:# baseConfig: default
# 重试 - Retryretry: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中的配置
只做展示
/*** 可以直接全部在YML中配置,而不使用Resilience4jConfig;* 我们一旦统一定义了**Registry以后,我们在使用的时候貌似只能同时使用一个..* 比如:当我们定义了RateLimiterRegistry以后,我们在使用的时候的就可以“@RateLimiter(name="xxx")”,这个xxx随便什么名字都会被应用到这个bean的配置,好像也只能这一个配置...*/
//@Configuration
public class Resilience4jConfig {// 限流配置@Beanpublic 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;}// 熔断器配置@Beanpublic CircuitBreakerRegistry circuitBreakerRegistry() {CircuitBreakerConfig config = CircuitBreakerConfig.custom().failureRateThreshold(50) // 失败率阈值百分比.waitDurationInOpenState(Duration.ofSeconds(5)) // 熔断后等待时间.permittedNumberOfCallsInHalfOpenState(2) // 半开状态允许的调用次数.slidingWindowSize(5) // 滑动窗口大小.recordExceptions(Exception.class) // 记录哪些异常.build();return CircuitBreakerRegistry.of(config);}// 超时配置@Beanpublic TimeLimiterRegistry timeLimiterRegistry() {TimeLimiterConfig config = TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)) // 超时时间2秒.build();return TimeLimiterRegistry.of(config);}// 隔板配置@Beanpublic BulkheadRegistry bulkheadRegistry() {BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(3) // 最大并发数.maxWaitDuration(Duration.ofMillis(500)) // 等待时间.build();return BulkheadRegistry.of(config);}// 重试配置@Beanpublic RetryRegistry retryRegistry() {RetryConfig config = RetryConfig.custom().maxAttempts(3) // 最大重试次数.waitDuration(Duration.ofMillis(500)) // 重试间隔.retryExceptions(RuntimeException.class) // 重试哪些异常.build();return RetryRegistry.of(config);}
}
定义全局处理类
通过@RestControllerAdvice定义全局异常处理类
@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调用类
@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、一种是直接配置具体的参数:
# 注意: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
/*** 可以直接全部在YML中配置,而不使用Resilience4jConfig;* 因为我们一旦统一定义了**Registry以后,我们在使用的时候貌似只能同时使用这一个配置...* 比如:当我们定义了RateLimiterRegistry以后,我们在使用的时候的就可以“@RateLimiter(name="xxx")”,这个xxx随便什么名字都会被应用到这个bean的配置,好像也只能这一个配置...*/
@Configuration
public class Resilience4jConfig {// 限流配置@Beanpublic 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
// 正常情况下,按照上面limitRefreshPeriod=60,limitForPeriod=1的配置,1分钟内连续请求1次就会被限制;
@RestController
public class Resilience4JController {@Autowiredprivate 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方法内部...
@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方法内部
@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
@GetMapping("/circuit-breaker")public String circuitBreaker(@RequestParam(defaultValue = "false") boolean shouldFail) throws InterruptedException {System.out.println("进入Resilience4JController");return resilience4JService.circuitBreakerExample(shouldFail);}
service
本地异常处理、全局异常处理
// 熔断示例@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
@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
/*** 隔板示例 -- 并发** @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
@GetMapping("/time-limiter")public CompletableFuture<String> timeLimiter() throws InterruptedException {return resilience4JService.timeLimiterExample();}
service
// 超时示例/*** 注意: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
@GetMapping("/retry")public String retry(@RequestParam(defaultValue = "false") boolean shouldFail) throws InterruptedException {return resilience4JService.retryExample(shouldFail);}
service
// 重试示例
// @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
@GetMapping("/composite")public CompletableFuture<String> composite(@RequestParam(defaultValue = "false") boolean shouldFail) {return resilience4JService.compositeExample(shouldFail);}
service
注意:compositeApi的参数在application.yml中配置,已经暂时注释掉了,如果有需要开启即可。
// 组合使用示例@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
.........算了算了。..太多了。不写了。下次写。
瑞士白~~~~...