Resilience4j是一个轻量级、易于使用的容错库,其灵感来自Netflix Hystrix,但专为Java 8和函数式编程设计。轻量级,因为库只使用Vavr,它没有任何其他外部库依赖项。相比之下,Netflix Hystrix对Archaius有一个编译依赖关系,Archaius有更多的外部库依赖关系,如Guava和Apache Commons。

Resilience4j提供高阶函数(decorators)来增强任何功能接口、lambda表达式或方法引用,包括断路器、速率限制器、重试或舱壁。可以在任何函数接口、lambda表达式或方法引用上使用多个装饰器。优点是您可以选择所需的装饰器,而无需其他任何东西。

有了Resilience4j,你不必全力以赴,你可以选择你需要的。

https://resilience4j.readme.io/docs/getting-started

概览

Resilience4j提供了两种舱壁模式(Bulkhead),可用于限制并发执行的次数:

  • SemaphoreBulkhead(信号量舱壁,默认),基于Java并发库中的Semaphore实现。
  • FixedThreadPoolBulkhead(固定线程池舱壁),它使用一个有界队列和一个固定线程池。

本文将演示在Spring Boot2中集成Resilience4j库,以及在多并发情况下实现如上两种舱壁模式。

引入依赖

在Spring Boot2项目中引入Resilience4j相关依赖

  1. <dependency>
  2. <groupId>io.github.resilience4j</groupId>
  3. <artifactId>resilience4j-spring-boot2</artifactId>
  4. <version>1.4.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.github.resilience4j</groupId>
  8. <artifactId>resilience4j-bulkhead</artifactId>
  9. <version>1.4.0</version>
  10. </dependency>

由于Resilience4j的Bulkhead依赖于Spring AOP,所以我们需要引入Spring Boot AOP相关依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

我们可能还希望了解Resilience4j在程序中的运行时状态,所以需要通过Spring Boot Actuator将其暴露出来

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-actuator</artifactId>
  4. </dependency>

实现SemaphoreBulkhead(信号量舱壁)

resilience4j-spring-boot2实现了对resilience4j的自动配置,因此我们仅需在项目中的yml/properties文件中编写配置即可。

SemaphoreBulkhead的配置项如下:

属性配置 默认值 含义
maxConcurrentCalls 25 舱壁允许的最大并行执行量
maxWaitDuration 0 尝试进入饱和舱壁时,应阻塞线程的最长时间。

添加配置

示例(使用yml):

  1. resilience4j.bulkhead:
  2. configs:
  3. default:
  4. maxConcurrentCalls: 5
  5. maxWaitDuration: 20ms
  6. instances:
  7. backendA:
  8. baseConfig: default
  9. backendB:
  10. maxWaitDuration: 10ms
  11. maxConcurrentCalls: 20

如上,我们配置了SemaphoreBulkhead的默认配置为maxConcurrentCalls: 5,maxWaitDuration: 20ms。并在backendA实例上应用了默认配置,而在backendB实例上使用自定义的配置。这里的实例可以理解为一个方法/lambda表达式等等的可执行单元。

编写Bulkhead逻辑

定义一个受SemaphoreBulkhead管理的Service类:

  1. @Service
  2. public class BulkheadService {
  3. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  4. @Autowired
  5. private BulkheadRegistry bulkheadRegistry;
  6. @Bulkhead(name = "backendA")
  7. public JsonNode getJsonObject() throws InterruptedException {
  8. io.github.resilience4j.bulkhead.Bulkhead.Metrics metrics = bulkheadRegistry.bulkhead("backendA").getMetrics();
  9. logger.info("now i enter the method!!!,{}<<<<<<{}", metrics.getAvailableConcurrentCalls(), metrics.getMaxAllowedConcurrentCalls());
  10. Thread.sleep(1000L);
  11. logger.info("now i exist the method!!!");
  12. return new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis());
  13. }
  14. }

如上,我们将@Bulkhead注解放到需要管理的方法上面。并且通过name属性指定该方法对应的Bulkhead实例名字(这里我们指定的实例名字为backendA,所以该方法将会利用默认的配置)。

定义接口类:

  1. @RestController
  2. public class BulkheadResource {
  3. @Autowired
  4. private BulkheadService bulkheadService;
  5. @GetMapping("/json-object")
  6. public ResponseEntity<JsonNode> getJsonObject() throws InterruptedException {
  7. return ResponseEntity.ok(bulkheadService.getJsonObject());
  8. }
  9. }

编写测试:

首先添加测试相关依赖

  1. <dependency>
  2. <groupId>io.rest-assured</groupId>
  3. <artifactId>rest-assured</artifactId>
  4. <version>3.0.5</version>
  5. <scope>test</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.awaitility</groupId>
  9. <artifactId>awaitility</artifactId>
  10. <version>4.0.2</version>
  11. <scope>test</scope>
  12. </dependency>

这里我们使用rest-assuredawaitility编写多并发情况下的API测试

  1. public class SemaphoreBulkheadTests extends Resilience4jDemoApplicationTests {
  2. @LocalServerPort
  3. private int port;
  4. @BeforeEach
  5. public void init() {
  6. RestAssured.baseURI = "http://localhost";
  7. RestAssured.port = port;
  8. }
  9. @Test
  10. public void 多并发访问情况下的SemaphoreBulkhead测试() {
  11. CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
  12. IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
  13. statusList.add(given().get("/json-object").statusCode());
  14. }
  15. ));
  16. await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
  17. System.out.println(statusList);
  18. assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(5);
  19. assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(3);
  20. }
  21. }

可以看到所有请求中只有前五个顺利通过了,其余三个都因为超时而导致接口报500异常。我们可能并不希望这种不友好的提示,因此Resilience4j提供了自定义的失败回退方法。当请求并发量过大时,无法正常执行的请求将进入回退方法。

首先我们定义一个回退方法

  1. private JsonNode fallback(BulkheadFullException exception) {
  2. return new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis());
  3. }

注意:回退方法应该和调用方法放置在同一类中,并且必须具有相同的方法签名,并且仅带有一个额外的目标异常参数。

然后在@Bulkhead注解中指定回退方法:@Bulkhead(name = "backendA", fallbackMethod = "fallback")

最后修改API测试代码:

  1. @Test
  2. public void 多并发访问情况下的SemaphoreBulkhead测试使用回退方法() {
  3. CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
  4. IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
  5. statusList.add(given().get("/json-object").statusCode());
  6. }
  7. ));
  8. await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
  9. System.out.println(statusList);
  10. assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
  11. }

运行单元测试,成功!可以看到,我们定义的回退方法,在请求过量时起作用了。

实现FixedThreadPoolBulkhead(固定线程池舱壁)

FixedThreadPoolBulkhead的配置项如下:

配置名称 默认值 含义
maxThreadPoolSize Runtime.getRuntime().availableProcessors() 配置最大线程池大小
coreThreadPoolSize Runtime.getRuntime().availableProcessors() - 1 配置核心线程池大小
queueCapacity 100 配置队列的容量
keepAliveDuration 20ms 当线程数大于核心时,这是多余空闲线程在终止前等待新任务的最长时间

添加配置

示例(使用yml):

  1. resilience4j.thread-pool-bulkhead:
  2. configs:
  3. default:
  4. maxThreadPoolSize: 4
  5. coreThreadPoolSize: 2
  6. queueCapacity: 2
  7. instances:
  8. backendA:
  9. baseConfig: default
  10. backendB:
  11. maxThreadPoolSize: 1
  12. coreThreadPoolSize: 1
  13. queueCapacity: 1

如上,我们定义了一段简单的FixedThreadPoolBulkhead配置,我们指定的默认配置为:maxThreadPoolSize: 4,coreThreadPoolSize: 2,queueCapacity: 2,并且指定了两个实例,其中backendA使用了默认配置而backendB使用了自定义的配置。

编写Bulkhead逻辑

定义一个受FixedThreadPoolBulkhead管理的方法:

  1. @Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL)
  2. public CompletableFuture<JsonNode> getJsonObjectByThreadPool() throws InterruptedException {
  3. io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
  4. logger.info("now i enter the method!!!,{}", metrics);
  5. Thread.sleep(1000L);
  6. logger.info("now i exist the method!!!");
  7. return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
  8. }

如上定义和SemaphoreBulkhead的方法大同小异,其中@Bulkhead显示指定了type的属性为Bulkhead.Type.THREADPOOL,表明其方法受FixedThreadPoolBulkhead管理。由于@Bulkhead默认的BulkheadSemaphoreBulkhead,所以在未指定type的情况下为SemaphoreBulkhead。另外,FixedThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。

定义接口类方法

  1. @GetMapping("/json-object-with-threadpool")
  2. public ResponseEntity<JsonNode> getJsonObjectWithThreadPool() throws InterruptedException, ExecutionException {
  3. return ResponseEntity.ok(bulkheadService.getJsonObjectByThreadPool().get());
  4. }

编写测试代码

  1. @Test
  2. public void 多并发访问情况下的ThreadPoolBulkhead测试() {
  3. CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
  4. IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
  5. statusList.add(given().get("/json-object-with-threadpool").statusCode());
  6. }
  7. ));
  8. await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
  9. System.out.println(statusList);
  10. assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(6);
  11. assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(2);
  12. }

测试中我们并行请求了8次,其中6次请求成功,2次失败。根据FixedThreadPoolBulkhead的默认配置,最多能容纳maxThreadPoolSize+queueCapacity次请求(根据我们上面的配置为6次)。

同样,我们可能并不希望这种不友好的提示,那么我们可以指定回退方法,在请求无法正常执行时使用回退方法。

  1. private CompletableFuture<JsonNode> fallbackByThreadPool(BulkheadFullException exception) {
  2. return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis()));
  3. }
  4. @Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "fallbackByThreadPool")
  5. public CompletableFuture<JsonNode> getJsonObjectByThreadPoolWithFallback() throws InterruptedException {
  6. io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
  7. logger.info("now i enter the method!!!,{}", metrics);
  8. Thread.sleep(1000L);
  9. logger.info("now i exist the method!!!");
  10. return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
  11. }

编写测试代码

  1. @Test
  2. public void 多并发访问情况下的ThreadPoolBulkhead测试使用回退方法() {
  3. CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
  4. IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
  5. statusList.add(given().get("/json-object-by-threadpool-with-fallback").statusCode());
  6. }
  7. ));
  8. await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
  9. System.out.println(statusList);
  10. assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
  11. }

由于指定了回退方法,所有请求的响应状态都为正常了。


总结

本文首先简单介绍了Resilience4j的功能及使用场景,然后具体介绍了Resilience4j中的Bulkhead。演示了如何在Spring Boot2项目中引入Resilience4j库,使用代码示例演示了如何在Spring Boot2项目中实现Resilience4j中的两种Bulkhead(SemaphoreBulkhead和FixedThreadPoolBulkhead),并编写API测试验证我们的示例。

本文示例代码地址:https://github.com/cg837718548/resilience4j-demo


欢迎访问笔者博客:blog.dongxishaonian.tech

关注笔者公众号,推送各类原创/优质技术文章 ️

Spring Boot2+Resilience4j实现容错之Bulkhead的更多相关文章

  1. 图书-技术-SpringBoot:《Spring Boot2 + Thymeleaf 企业应用实战》

    ylbtech-图书-技术-SpringBoot:<Spring Boot2 + Thymeleaf 企业应用实战> <Spring Boot 2+Thymeleaf企业应用实战&g ...

  2. Spring Boot2.0 设置拦截器

    所有功能完成 配置登录认证 配置拦截器 在spring boot2.0 之后 通过继承这个WebMvcConfigurer类 就可以完成拦截 新建包com.example.interceptor; 创 ...

  3. Spring Boot2.0 静态资源被拦截问题

    在Spring Boot2.0+的版本中,只要用户自定义了拦截器,则静态资源会被拦截.但是在spring1.0+的版本中,是不会拦截静态资源的. 因此,在使用Spring Boot2.0+时,配置拦截 ...

  4. Spring Boot2.0使用Spring Security

     一.Spring Secutity简介     Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性 ...

  5. spring boot2 kafka

    一.软件版本 1.linux:centos6 2.zookeeper:zookeeper-3.4.1 3.kafka:kafka_2.12-2.2.0 4.jdk:1.8 5.instelliJ Id ...

  6. spring boot2.0(一 ) 基础环境搭建

    1.基础配置 开发环境:window jdk版本:1.8(spring boot2.0最低要求1.8) 开发工具:eclipse 构建方式:maven3 2.POM配置文件 <project x ...

  7. Spring Boot2.0 整合 Kafka

    Kafka 概述 Apache Kafka 是一个分布式流处理平台,用于构建实时的数据管道和流式的应用.它可以让你发布和订阅流式的记录,可以储存流式的记录,并且有较好的容错性,可以在流式记录产生时就进 ...

  8. Spring Boot2.0自定义配置文件使用

    声明: spring boot 1.5 以后,ConfigurationProperties取消locations属性,因此采用PropertySource注解配合使用 根据Spring Boot2. ...

  9. Spring boot2.0 设置文件上传大小限制

    今天把Spring boot版本升级到了2.0后,发现原来的文件上传大小限制设置不起作用了,原来的application.properties设置如下: spring.http.multipart.m ...

随机推荐

  1. java 单列集合总结

    Collection 接口 add() remove() contains() clear(); size(); 迭代器遍历(普通迭代器,不能再遍历过程中修改集合的长度) List接口 单列集合 有序 ...

  2. Beta冲刺 ——5.27

    这个作业属于哪个课程 软件工程 这个作业要求在哪里 Beta冲刺 这个作业的目标 Beta冲刺 作业正文 正文 github链接 项目地址 其他参考文献 无 一.会议内容 1.组员一起学习Git分支管 ...

  3. 50个SQL语句(MySQL版) 问题十六

    --------------------------表结构-------------------------- student(StuId,StuName,StuAge,StuSex) 学生表 tea ...

  4. 容器技术之Dockerfile(二)

    前文我们聊到了什么是dockerfile,它的主要作用以及dockerfile的一些基本指令的使用方法,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/13019 ...

  5. Java实现 LeetCode 697 数组的度(类似于数组的map)

    697. 数组的度 给定一个非空且只包含非负数的整数数组 nums, 数组的度的定义是指数组里任一元素出现频数的最大值. 你的任务是找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度. 示 ...

  6. Java实现 LeetCode 31下一个排列

    31. 下一个排列 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许 ...

  7. Java实现 LeetCode 10 正则表达式匹配

    10. 正则表达式匹配 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. '.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配, ...

  8. java实现第五届蓝桥杯格子放鸡蛋

    格子放鸡蛋 X星球的母鸡很聪明.它们把蛋直接下在一个 N * N 的格子中,每个格子只能容纳一枚鸡蛋.它们有个习惯,要求:每行,每列,以及每个斜线上都不能有超过2个鸡蛋.如果要满足这些要求,母鸡最多能 ...

  9. 三、TCP协议

    TCP(Transmission Control Protocol)传输控制协议:顾名思义就是对数据的传输进行控制 TCP报头 序号:相当于编号,当TCP数据包过大的时候会进行分段,分段之后按序号顺序 ...

  10. javascript 面向对象学习(二)——原型与继承

    什么是原型? 首先我们创建一个简单的空对象,再把它打印出来 var example = {} console.log(example) 结果如下: { __proto__: { constructor ...