Spring Boot2+Resilience4j实现容错之Bulkhead
Resilience4j是一个轻量级、易于使用的容错库,其灵感来自Netflix Hystrix,但专为Java 8和函数式编程设计。轻量级,因为库只使用Vavr,它没有任何其他外部库依赖项。相比之下,Netflix Hystrix对Archaius有一个编译依赖关系,Archaius有更多的外部库依赖关系,如Guava和Apache Commons。
Resilience4j提供高阶函数(decorators)来增强任何功能接口、lambda表达式或方法引用,包括断路器、速率限制器、重试或舱壁。可以在任何函数接口、lambda表达式或方法引用上使用多个装饰器。优点是您可以选择所需的装饰器,而无需其他任何东西。
有了Resilience4j,你不必全力以赴,你可以选择你需要的。
概览
Resilience4j提供了两种舱壁模式(Bulkhead),可用于限制并发执行的次数:
- SemaphoreBulkhead(信号量舱壁,默认),基于Java并发库中的Semaphore实现。
- FixedThreadPoolBulkhead(固定线程池舱壁),它使用一个有界队列和一个固定线程池。
本文将演示在Spring Boot2中集成Resilience4j库,以及在多并发情况下实现如上两种舱壁模式。
引入依赖
在Spring Boot2项目中引入Resilience4j相关依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
<version>1.4.0</version>
</dependency>
由于Resilience4j的Bulkhead依赖于Spring AOP,所以我们需要引入Spring Boot AOP相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我们可能还希望了解Resilience4j在程序中的运行时状态,所以需要通过Spring Boot Actuator将其暴露出来
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
实现SemaphoreBulkhead(信号量舱壁)
resilience4j-spring-boot2实现了对resilience4j的自动配置,因此我们仅需在项目中的yml/properties文件中编写配置即可。
SemaphoreBulkhead的配置项如下:
属性配置 | 默认值 | 含义 |
---|---|---|
maxConcurrentCalls | 25 | 舱壁允许的最大并行执行量 |
maxWaitDuration | 0 | 尝试进入饱和舱壁时,应阻塞线程的最长时间。 |
添加配置
示例(使用yml):
resilience4j.bulkhead:
configs:
default:
maxConcurrentCalls: 5
maxWaitDuration: 20ms
instances:
backendA:
baseConfig: default
backendB:
maxWaitDuration: 10ms
maxConcurrentCalls: 20
如上,我们配置了SemaphoreBulkhead的默认配置为maxConcurrentCalls: 5,maxWaitDuration: 20ms
。并在backendA实例上应用了默认配置,而在backendB实例上使用自定义的配置。这里的实例可以理解为一个方法/lambda表达式等等的可执行单元。
编写Bulkhead逻辑
定义一个受SemaphoreBulkhead管理的Service类:
@Service
public class BulkheadService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private BulkheadRegistry bulkheadRegistry;
@Bulkhead(name = "backendA")
public JsonNode getJsonObject() throws InterruptedException {
io.github.resilience4j.bulkhead.Bulkhead.Metrics metrics = bulkheadRegistry.bulkhead("backendA").getMetrics();
logger.info("now i enter the method!!!,{}<<<<<<{}", metrics.getAvailableConcurrentCalls(), metrics.getMaxAllowedConcurrentCalls());
Thread.sleep(1000L);
logger.info("now i exist the method!!!");
return new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis());
}
}
如上,我们将@Bulkhead
注解放到需要管理的方法上面。并且通过name
属性指定该方法对应的Bulkhead实例名字(这里我们指定的实例名字为backendA,所以该方法将会利用默认的配置)。
定义接口类:
@RestController
public class BulkheadResource {
@Autowired
private BulkheadService bulkheadService;
@GetMapping("/json-object")
public ResponseEntity<JsonNode> getJsonObject() throws InterruptedException {
return ResponseEntity.ok(bulkheadService.getJsonObject());
}
}
编写测试:
首先添加测试相关依赖
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
这里我们使用rest-assured和awaitility编写多并发情况下的API测试
public class SemaphoreBulkheadTests extends Resilience4jDemoApplicationTests {
@LocalServerPort
private int port;
@BeforeEach
public void init() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = port;
}
@Test
public void 多并发访问情况下的SemaphoreBulkhead测试() {
CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
statusList.add(given().get("/json-object").statusCode());
}
));
await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
System.out.println(statusList);
assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(5);
assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(3);
}
}
可以看到所有请求中只有前五个顺利通过了,其余三个都因为超时而导致接口报500异常。我们可能并不希望这种不友好的提示,因此Resilience4j提供了自定义的失败回退方法。当请求并发量过大时,无法正常执行的请求将进入回退方法。
首先我们定义一个回退方法
private JsonNode fallback(BulkheadFullException exception) {
return new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis());
}
注意:回退方法应该和调用方法放置在同一类中,并且必须具有相同的方法签名,并且仅带有一个额外的目标异常参数。
然后在@Bulkhead
注解中指定回退方法:@Bulkhead(name = "backendA", fallbackMethod = "fallback")
最后修改API测试代码:
@Test
public void 多并发访问情况下的SemaphoreBulkhead测试使用回退方法() {
CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
statusList.add(given().get("/json-object").statusCode());
}
));
await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
System.out.println(statusList);
assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
}
运行单元测试,成功!可以看到,我们定义的回退方法,在请求过量时起作用了。
实现FixedThreadPoolBulkhead(固定线程池舱壁)
FixedThreadPoolBulkhead的配置项如下:
配置名称 | 默认值 | 含义 |
---|---|---|
maxThreadPoolSize | Runtime.getRuntime().availableProcessors() |
配置最大线程池大小 |
coreThreadPoolSize | Runtime.getRuntime().availableProcessors() - 1 |
配置核心线程池大小 |
queueCapacity | 100 | 配置队列的容量 |
keepAliveDuration | 20ms | 当线程数大于核心时,这是多余空闲线程在终止前等待新任务的最长时间 |
添加配置
示例(使用yml):
resilience4j.thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 4
coreThreadPoolSize: 2
queueCapacity: 2
instances:
backendA:
baseConfig: default
backendB:
maxThreadPoolSize: 1
coreThreadPoolSize: 1
queueCapacity: 1
如上,我们定义了一段简单的FixedThreadPoolBulkhead配置,我们指定的默认配置为:maxThreadPoolSize: 4,coreThreadPoolSize: 2,queueCapacity: 2
,并且指定了两个实例,其中backendA使用了默认配置而backendB使用了自定义的配置。
编写Bulkhead逻辑
定义一个受FixedThreadPoolBulkhead管理的方法:
@Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<JsonNode> getJsonObjectByThreadPool() throws InterruptedException {
io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
logger.info("now i enter the method!!!,{}", metrics);
Thread.sleep(1000L);
logger.info("now i exist the method!!!");
return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
}
如上定义和SemaphoreBulkhead的方法大同小异,其中@Bulkhead
显示指定了type的属性为Bulkhead.Type.THREADPOOL
,表明其方法受FixedThreadPoolBulkhead管理。由于@Bulkhead
默认的Bulkhead是SemaphoreBulkhead,所以在未指定type的情况下为SemaphoreBulkhead。另外,FixedThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。
定义接口类方法
@GetMapping("/json-object-with-threadpool")
public ResponseEntity<JsonNode> getJsonObjectWithThreadPool() throws InterruptedException, ExecutionException {
return ResponseEntity.ok(bulkheadService.getJsonObjectByThreadPool().get());
}
编写测试代码
@Test
public void 多并发访问情况下的ThreadPoolBulkhead测试() {
CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
statusList.add(given().get("/json-object-with-threadpool").statusCode());
}
));
await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
System.out.println(statusList);
assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(6);
assertThat(statusList.stream().filter(i -> i == 500).count()).isEqualTo(2);
}
测试中我们并行请求了8次,其中6次请求成功,2次失败。根据FixedThreadPoolBulkhead的默认配置,最多能容纳maxThreadPoolSize+queueCapacity次请求(根据我们上面的配置为6次)。
同样,我们可能并不希望这种不友好的提示,那么我们可以指定回退方法,在请求无法正常执行时使用回退方法。
private CompletableFuture<JsonNode> fallbackByThreadPool(BulkheadFullException exception) {
return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("errorFile", System.currentTimeMillis()));
}
@Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "fallbackByThreadPool")
public CompletableFuture<JsonNode> getJsonObjectByThreadPoolWithFallback() throws InterruptedException {
io.github.resilience4j.bulkhead.ThreadPoolBulkhead.Metrics metrics = threadPoolBulkheadRegistry.bulkhead("backendA").getMetrics();
logger.info("now i enter the method!!!,{}", metrics);
Thread.sleep(1000L);
logger.info("now i exist the method!!!");
return CompletableFuture.supplyAsync(() -> new ObjectMapper().createObjectNode().put("file", System.currentTimeMillis()));
}
编写测试代码
@Test
public void 多并发访问情况下的ThreadPoolBulkhead测试使用回退方法() {
CopyOnWriteArrayList<Integer> statusList = new CopyOnWriteArrayList<>();
IntStream.range(0, 8).forEach(i -> CompletableFuture.runAsync(() -> {
statusList.add(given().get("/json-object-by-threadpool-with-fallback").statusCode());
}
));
await().atMost(1, TimeUnit.MINUTES).until(() -> statusList.size() == 8);
System.out.println(statusList);
assertThat(statusList.stream().filter(i -> i == 200).count()).isEqualTo(8);
}
由于指定了回退方法,所有请求的响应状态都为正常了。
总结
本文首先简单介绍了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的更多相关文章
- 图书-技术-SpringBoot:《Spring Boot2 + Thymeleaf 企业应用实战》
ylbtech-图书-技术-SpringBoot:<Spring Boot2 + Thymeleaf 企业应用实战> <Spring Boot 2+Thymeleaf企业应用实战&g ...
- Spring Boot2.0 设置拦截器
所有功能完成 配置登录认证 配置拦截器 在spring boot2.0 之后 通过继承这个WebMvcConfigurer类 就可以完成拦截 新建包com.example.interceptor; 创 ...
- Spring Boot2.0 静态资源被拦截问题
在Spring Boot2.0+的版本中,只要用户自定义了拦截器,则静态资源会被拦截.但是在spring1.0+的版本中,是不会拦截静态资源的. 因此,在使用Spring Boot2.0+时,配置拦截 ...
- Spring Boot2.0使用Spring Security
一.Spring Secutity简介 Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性 ...
- 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 ...
- spring boot2.0(一 ) 基础环境搭建
1.基础配置 开发环境:window jdk版本:1.8(spring boot2.0最低要求1.8) 开发工具:eclipse 构建方式:maven3 2.POM配置文件 <project x ...
- Spring Boot2.0 整合 Kafka
Kafka 概述 Apache Kafka 是一个分布式流处理平台,用于构建实时的数据管道和流式的应用.它可以让你发布和订阅流式的记录,可以储存流式的记录,并且有较好的容错性,可以在流式记录产生时就进 ...
- Spring Boot2.0自定义配置文件使用
声明: spring boot 1.5 以后,ConfigurationProperties取消locations属性,因此采用PropertySource注解配合使用 根据Spring Boot2. ...
- Spring boot2.0 设置文件上传大小限制
今天把Spring boot版本升级到了2.0后,发现原来的文件上传大小限制设置不起作用了,原来的application.properties设置如下: spring.http.multipart.m ...
随机推荐
- 完美解决报错Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'
Failed to convert value of type 'java.lang.String' to required type 'java.util.Date' 首先这个错误的意思是 前台页面 ...
- 前端和Nodejs的关系 简单理解
前端使用JS脚本语言进行开发. JS脚本语言需要依赖一个平台运行,从而生成可视化的东西. Node.js提供这个平台,同时提供JS运行需要的一些插件.库.包.轮子.组件.功能等等. JavaScrip ...
- Java实现 LeetCode 836 矩形重叠(暴力)
836. 矩形重叠 矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 为左下角的坐标,(x2, y2) 是右上角的坐标. 如果相交的面积为正,则称两矩形重叠.需要明确的 ...
- Java实现 蓝桥杯 算法训练 多阶乘计算
试题 算法训练 多阶乘计算 问题描述 我们知道,阶乘n!表示n*(n-1)(n-2)-21, 类似的,可以定义多阶乘计算,例如:5!!=531,依次可以有n!..!(k个'!',可以简单表示为n(k) ...
- Java实现 LeetCode 657 机器人能否返回原点(暴力大法)
657. 机器人能否返回原点 在二维平面上,有一个机器人从原点 (0, 0) 开始.给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束. 移动顺序由字符串表示.字符 move[i ...
- Java实现 LeetCode 88 合并两个有序数组
88. 合并两个有序数组 给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组. 说明: 初始化 nums1 和 nums2 的元 ...
- Java实现 LeetCode 86 分割链表
86. 分隔链表 给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前. 你应当保留两个分区中每个节点的初始相对位置. 示例: 输入: head = 1 ...
- Java实现 洛谷 P1583 魔法照片
import java.util.*; class Main{ public static void main(String[] args) { Scanner in = new Scanner(Sy ...
- java实现 蓝桥杯 算法训练 操作格子
问题描述 有n个格子,从左到右放成一排,编号为1-n. 共有m次操作,有3种操作类型: 1.修改一个格子的权值, 2.求连续一段格子权值和, 3.求连续一段格子的最大值. 对于每个2.3操作输出你所求 ...
- java实现第三届蓝桥杯排日程
排日程 [编程题](满分34分) 某保密单位机要人员 A,B,C,D,E 每周需要工作5天,休息2天. 上级要求每个人每周的工作日和休息日安排必须是固定的,不能在周间变更. 此外,由于工作需要,还有如 ...