上篇文章首先和大家大致说了下断路器 Hystrix ,然后详细说了下 Resilience4j 的用法,但是都是 JavaSE 环境下的用法,并没有涉及到在微服务中的使用。Resilience4j 最终我们还是要在微服务中体现它的价值,我们学习它使用它是为了解决微服务中的一些痛点,因此,本文我就来和大家分享下 Resilience4j 在微服务中的具体用法。
准备工作
首先我们需要先搭建一个测试环境,我这里创建一个名为 Resilience4j-SpringBoot 的父工程,然后在父工程中创建一个名为 eureka 的子项目来搭建服务注册中心,然后再创建一个名为 provider 的服务提供者,将服务提供者注册到 eureka 上,然后再在 provider 中提供一个 /hello 接口,内容如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(String name) {
String s = "hello " + name + " !";
System.out.println(s+">>>>>"+new Date());
int i = 1 / 0;
return s;
}
}
这里接口中,我特意设置了一个异常, consumer 在调用这个接口的时候会调用失败,方便我们去测试请求重试等功能。
接下来再来创建一个 consumer。consumer 在创建的过程中只需要加入两个基本的依赖即可,即 spring-boot-starter-web 和 spring-cloud-starter-netflix-eureka-client ,其它的依赖我们在下面具体的使用过程中来说,创建完成后,也将 consumer 注册到服务注册中心上。
这就是我们的准备工作,由于准备工作比较简单,大家不清楚的可以参考服务注册与消费一文,我这里就不再详细介绍。
Retry
首先要和大家介绍的功能就是重试功能,开发环境一般比较稳定。微服务之间的调用,除非是逻辑错误导致调用失败,一般来说可能很少会遇到网络原因导致的调用失败,但是在生产环境中,网络原因导致的调用失败却是一个不能够忽略的问题,由于网络抖动造成的调用失败,我们一定要进行请求重试。
上文介绍的 Resilience4j 中的功能,在微服务中的使用都有两种不同的用法,一种就是借助 Spring AOP ,直接通过一个注解来实现相关的功能;还有一种就是和上文一样,通过编程的方式来实现,这里分别来和大家介绍。
不过无论哪一种,我们都是需要首先添加依赖,不同于上篇文章我们根据 Resilience4j 提供的功能挨个加依赖,在微服务中,我们可以直接引用 resilience4j-spring-boot2 依赖,这个依赖中包含了 Resilience4j 中提供的所有功能,也包含了所需要的 Spring AOP 的依赖,如下图:

依赖代码如下:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>0.14.1</version>
</dependency>
AOP 式
依赖添加成功后,接下来在 application.yml 文件中配置 Retry 参数,如下:
resilience4j.retry:
retryAspectOrder: 399
backends:
retryBackendA:
maxRetryAttempts: 3
waitDuration: 600
eventConsumerBufferSize: 1
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
enableRandomizedWait: false
randomizedWaitFactor: 2
retryExceptionPredicate: com.justdojava.consumer.RecordFailurePredicate
retryExceptions:
- java.io.IOException
ignoreExceptions:
- com.justdojava.consumer.IgnoredException
关于这一段的配置,解释如下:
- retryAspectOrder 表示 Retry 的一个优先级。默认情况下, Retry 的优先级高于 bulkhead 、 Circuit breaker 以及 rateLimiter ,即 Retry 会先于另外三个执行。 Retry、 bulkhead 、 Circuit breaker 以及 rateLimiter 的优先级数值默认分别是 Integer.MAX_VALUE-3、Integer.MAX_VALUE-2、Integer.MAX_VALUE-1 以及 Integer.MAX_VALUE ,即数值越小,优先级越高;
- backends 属性中我们可以配置不同的 Retry 策略,给不同的策略分别取一个名字, retryBackendA 就是一个 Retry 策略的名字。在 Java 代码中,我们将直接通过指定 Retry 策略的名字来使用某一种 Retry 方案;
- maxRetryAttempts 表示最大重试次数;
- waitDuration 表示下一次重试等待时间,最小为100 ms ;
- eventConsumerBufferSize 表示重试事件缓冲区大小;
- enableExponentialBackoff 表示是否开启指数退避抖动算法,当一次调用失败后,如果在相同的时间间隔内发起重试,有可能发生连续的调用失败,因此可以开启指数退避抖动算法;
- exponentialBackoffMultiplier 表示时间间隔乘数;
- enableRandomizedWait 表示下次重试的时间间隔是否随机, enableRandomizedWait 和 enableExponentialBackoff 默认为 false ,并且这两个不可以同时开启;
- retryExceptionPredicate 类似于我们上文所说的什么样的异常会被认定为请求失败,这里的RecordFailurePredicate是一个自定义的类;
- retryExceptions 表示需要重试的异常;
- ignoreExceptions 表示忽略的异常。
RecordFailurePredicate 类的定义如下:
public class RecordFailurePredicate implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return true;
}
}
方便起见,我这里未做判断,直接返回 true 。
还有一个自定义的异常 IgnoredException ,如下:
public class IgnoredException extends Exception {
}
配置完成后,接下来再在项目启动类中配置一个 RestTemplate ,如下:
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
然后创建一个 HelloService 来进行远程调用,如下:
@Service
@Retry(name = "retryBackendA")
public class HelloService {
@Autowired
RestTemplate restTemplate;
public String hello(String name) {
return restTemplate.getForObject("http://provider/hello?name={1}", String.class, name);
}
}
HelloService 本身是一个非常常规的类,RestTemplate 相信大家也是再熟悉不过了,唯一和我们前面学过的不同的地方是这里类上多了一个 @Retry(name = “retryBackendA”) 注解,这个注解表示在当前所有类中开启请求失败重试功能,请求失败重试策略就是 retryBackendA ,当然这个注解也可以加在某一个具体的方法上,表示只有该方法开启请求失败重试功能。
做完这些之后,我们就可以分别启动 eureka、provider 以及 consumer 了,然后在浏览器中访问 consumer 接口,观察 provider 的日志输出,就可以看到日志一共打印了三次,这里发生了请求失败重试。
编程式
在框架中,通过这种面向切面编程的方式来引用 Resilience4j 中的 Retry 功能固然很方便。不过, Resilience4j 也支持编程式引用,编程式引用的方式就和我们上篇文章介绍的用法差不多了,举例如下:
@RestController
public class UseHelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello2")
public String hello2(String name) {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(5000))
.build();
Retry retry = Retry.of("id", config);
Try<String> result = Try.ofSupplier(Retry.decorateSupplier(retry, () -> helloService.hello(name)));
return result.get();
}
}
通过编程式来引用 Resilience4j 中的 Retry 功能,则不再需要 application.properties 中的 Retry 相关配置,也不需要在 HelloService 类上添加 @Retry 注解,所有关于重试的配置都是通过 Java 代码来实现,至于 Java 代码配置中的含义,则和上文介绍的一致,这里不再赘述。
CircuitBreaker
Resilience4j 中断路器的用法和上文也是基本一致,分为两种,可以通过 AOP 的方式使用,也可以通过编程式使用,我们分别来看。
AOP 式
由于在 Retry 中已经添加了 resilience4j-spring-boot2 依赖,这里我就不再重复说添加依赖的事了。在上个案例的基础上,我们继续在 application.yml 文件中添加如下配置:
resilience4j.circuitbreaker:
backends:
backendA:
ringBufferSizeInClosedState: 5
ringBufferSizeInHalfOpenState: 3
waitInterval: 5000
failureRateThreshold: 50
eventConsumerBufferSize: 10
registerHealthIndicator: true
recordFailurePredicate: com.justdojava.consumer.RecordFailurePredicate
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException
这里配置也很好理解,大部分参数和我们上篇文章介绍的一致:
- backendA 是断路器策略的命名,和 Retry 类似,一会也是通过注解来引用这个策略;
- ringBufferSizeInClosedState 表示断路器关闭状态下,环形缓冲区的大小;
- ringBufferSizeInHalfOpenState 表示断路器处于 HalfOpen 状态下,环形缓冲区的大小;
- waitInterval 表示断路器从 open 切换到 half closed 状态时,需要保持的时间;
- failureRateThreshold 表示故障率阈值百分比,超过这个阈值,断路器就会打开;
- eventConsumerBufferSize 表示事件缓冲区大小;
- registerHealthIndicator 表示开启健康检测。
和 Retry 类似,在 Circuit Breaker 中,我们也可以通过 circuitBreakerAspectOrder 属性来修改 Circuit Breaker 的执行优先级。
配置完成后,接下来我们来定义一个名为 HelloServiceCircuitBreaker 的类,在这个类中,来定义服务请求方法:
@Service
@CircuitBreaker(name = "backendA")
public class HelloServiceCircuitBreaker {
@Autowired
RestTemplate restTemplate;
public String hello(String name) {
return restTemplate.getForObject("http://provider/hello?name={1}", String.class, name);
}
}
这里通过 @CircuitBreaker 注解来启用断路器。最后,我们在 UseHelloController 中调用这个方法即可。
但是这种写法有一个问题,就是没法进行服务容错降级,如果希望进行服务容错降级,那么还是需要我们上篇文章提到的通过编程实现断路器功能。
编程式
通过编程实现断路器功能,就不再需要 application.yml 中的配置了,也不需要在类上添加 @CircuitBreaker(name = “backendA”) 注解,所有的相关配置都是在 Java 代码中完成,和上篇文章基本一样,如下:
public String hello2(String name) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.ringBufferSizeInHalfOpenState(20)
.ringBufferSizeInClosedState(20)
.build();
io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("backendA", circuitBreakerConfig);
Try<String> supplier = Try.ofSupplier(io.github.resilience4j.circuitbreaker.CircuitBreaker
.decorateSupplier(circuitBreaker,
() -> restTemplate.getForObject("http://provider/hello?name={1}", String.class, name)))
.recover(Exception.class, "有异常,访问失败!");
return supplier.get();
}
我来和大家捋一捋上面这段代码的思路:
- 首先利用 Java 代码创建一个 CircuitBreakerConfig 出来,然后配置一下故障率阈值,等待时间以及环形缓冲区大小等;
- 根据第一步创建出来的 CircuitBreakerConfig ,再去创建一个 CircuitBreaker 对象;
- 通过 Try.ofSupplier 方法去发送一个请求,如果请求抛出异常,则在 recover 方法中进行服务降级处理,recover 可以写多个。
最后,在 UseHelloController 中调用这里的 hello2 方法去访问 provider 中的接口。接口调用失败后, consumer 中自动进行服务降级,最终返回字符串为 有异常,访问失败! 。
RateLimiter
接下来我们再来说一说限流工具 RateLimiter ,限流工具的用法基本上和前两个差不多,可以通过 AOP 的方式使用,也可以通过编程式来使用,下面分别来介绍。
AOP 式
通过 AOP 的方式来使用限流工具,首先在 application.yml 配置文件中添加 RateLimiter 相关配置,如下:
resilience4j.ratelimiter:
limiters:
backendA:
limitForPeriod: 1
limitRefreshPeriodInMillis: 5000
timeoutInMillis: 5000
subscribeForEvents: true
registerHealthIndicator: true
eventConsumerBufferSize: 100
关于这段配置,我说如下几点:
- backendA 在这里依然表示配置的名称,在 Java 代码中,我们将通过指定限流工具的名称来使用某一种限流策略;
- limitForPeriod 表示请求频次的阈值;
- limitRefreshPeriodInMillis 表示频次刷新的周期;
- timeoutInMillis 许可期限的等待时间,默认为5秒;
- subscribeForEvents 表示开启事件订阅;
- registerHealthIndicator 表示开启健康监控;
- eventConsumerBufferSize 表示事件缓冲区大小。
配置完成后,创建一个 HelloServiceRateLimiter 类,内容如下:
@Service
@RateLimiter(name = "backendA")
public class HelloServiceRateLimiter {
@Autowired
RestTemplate restTemplate;
public String hello(String name) {
return restTemplate.getForObject("http://provider/hello?name={1}", String.class, name);
}
}
这里就是一个很常规的服务调用,然后在 UseHelloController 中调用该方法:
@GetMapping("/rl")
public void rateLimiter(String name) {
for (int i = 0; i < 5; i++) {
String hello = helloServiceRateLimiter.hello(name);
}
}
为了测试出效果,这里使用了一个 for 循环,循环中连续发送五次请求,我们发现服务端打印日志如下:

可以看到,限流已经生效。
编程式
当然,这里的效果也可以通过编程来实现。通过编程实现代码和上篇文章介绍的基本一致,同时,这里也不再需要在 application.yml 中添加配置,所有的条件通过 Java 代码来配置即可,如下:
public void hello2(String name) {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofMillis(5000))
.limitForPeriod(1)
.timeoutDuration(Duration.ofMillis(6000))
.build();
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);
RateLimiter rateLimiter = RateLimiter.of("backendB", config);
Supplier<String> supplier = RateLimiter.decorateSupplier(rateLimiter, () ->
restTemplate.getForObject("http://provider/hello?name={1}", String.class, name)
);
for (int i = 0; i < 5; i++) {
Try<String> aTry = Try.ofSupplier(supplier);
System.out.println(aTry.get());
}
}
这里的代码和我们前文所讲的基本一致,创建一个 Supplier 对象,然后使用 Try.of 方法执行调用,且调用多次。然后在 UseHelloController 中调用这个方法:
@GetMapping("/r2")
public void rateLimiter2(String name) {
helloServiceRateLimiter.hello2(name);
}
观察 provider 中的日志输出,我们可以看到,限流已经生效了。
小结
本文主要向大家介绍了 Resilience4j 在微服务中的应用。相对于 Hystrix ,Resilience4j 更加轻便简洁,而且到处充满了 JDK8 的元素,确实非常好用,也是未来处理微服务系统稳定性的一个方向。
本文作者:纯洁的微笑、江南一点雨