1. 概述

Resilience4j 是一个轻量级的容错库,为 Web 应用提供多种容错和稳定性模式。本文将通过一个简单的 Spring Boot 应用演示如何使用这个库

2. 项目搭建

本节重点介绍Spring Boot 项目的关键配置

2.1. Maven 依赖

首先添加 spring-boot-starter-web 依赖来启动 Web 应用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

接着添加 resilience4j-spring-boot2spring-boot-starter-aop 依赖,以便在 Spring Boot 中通过注解使用 Resilience4j 的功能

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

此外,添加 spring-boot-starter-actuator 依赖,通过暴露的接口监控应用状态

<dependency>
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

最后添加 wiremock-jre8 依赖,用于测试 REST API

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <scope>test</scope>
</dependency>

2.2. RestController 和外部 API 调用器

使用 Resilience4j 时,应用需要与外部 API 交互。先创建 RestTemplate Bean:

@Bean
public RestTemplate restTemplate() {
    return new RestTemplateBuilder().rootUri("http://localhost:9090")
      .build();
}

定义 ExternalAPICaller 组件类:

@Component
public class ExternalAPICaller {
    private final RestTemplate restTemplate;

    @Autowired
    public ExternalAPICaller(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
}

创建 ResilientAppController 类,暴露 REST 接口并调用外部 API

@RestController
@RequestMapping("/api/")
public class ResilientAppController {
    private final ExternalAPICaller externalAPICaller;
}

2.3. Actuator 接口配置

通过 Spring Boot Actuator 暴露健康检查接口,在 application.properties 中添加:

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

management.health.circuitbreakers.enabled=true
management.health.ratelimiters.enabled=true

后续会根据需要添加特定功能的配置。

2.4. 单元测试

使用 WireMockExtension 模拟外部服务。在测试类中定义:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ResilientAppControllerUnitTest {

    @RegisterExtension
    static WireMockExtension EXTERNAL_SERVICE = WireMockExtension.newInstance()
      .options(WireMockConfiguration.wireMockConfig()
      .port(9090))
      .build();

添加 TestRestTemplate 实例:

@Autowired
private TestRestTemplate restTemplate;

2.5. 异常处理器

Resilience4j 会抛出特定异常,需要转换为有意义的 HTTP 状态码。定义 ApiExceptionHandler 类:

@ControllerAdvice
public class ApiExceptionHandler {
}

后续会根据不同容错模式添加处理器。

3. 熔断器

熔断器模式通过限制上游服务调用,保护下游服务。暴露 /api/circuit-breaker 接口并添加 @CircuitBreaker 注解:

@GetMapping("/circuit-breaker")
@CircuitBreaker(name = "CircuitBreakerService")
public String circuitBreakerApi() {
    return externalAPICaller.callApi();
}

ExternalAPICaller 中实现 callApi() 方法:

public String callApi() {
    return restTemplate.getForObject("/api/external", String.class);
}

application.properties 中配置熔断器:

resilience4j.circuitbreaker.instances.CircuitBreakerService.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.CircuitBreakerService.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.CircuitBreakerService.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.wait-duration-in-open-state=5s
resilience4j.circuitbreaker.instances.CircuitBreakerService.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-size=10
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-type=count_based

⚠️ 当失败率达到 50% 时熔断器打开,后续请求抛出 CallNotPermittedException。添加异常处理器:

@ExceptionHandler({CallNotPermittedException.class})
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public void handleCallNotPermittedException() {
}

测试熔断器行为:

@Test
public void testCircuitBreaker() {
    EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
      .willReturn(serverError()));

    IntStream.rangeClosed(1, 5)
      .forEach(i -> {
          ResponseEntity response = restTemplate.getForEntity("/api/circuit-breaker", String.class);
          assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
      });

    IntStream.rangeClosed(1, 5)
      .forEach(i -> {
          ResponseEntity response = restTemplate.getForEntity("/api/circuit-breaker", String.class);
          assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
      });
    
    EXTERNAL_SERVICE.verify(5, getRequestedFor(urlEqualTo("/api/external")));
}

✅ 前 5 次调用失败后熔断器打开,后续请求直接返回 503 状态码。

4. 重试机制

重试模式通过自动重试恢复瞬时故障。添加 /api/retry 接口:

@GetMapping("/retry")
@Retry(name = "retryApi", fallbackMethod = "fallbackAfterRetry")
public String retryApi() {
    return externalAPICaller.callApi();
}

提供降级方法:

public String fallbackAfterRetry(Exception ex) {
    return "all retries have exhausted";
}

配置重试行为:

resilience4j.retry.instances.retryApi.max-attempts=3
resilience4j.retry.instances.retryApi.wait-duration=1s
resilience4j.retry.metrics.legacy.enabled=true
resilience4j.retry.metrics.enabled=true

测试重试逻辑:

@Test
public void testRetry() {
    EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
      .willReturn(ok()));
    ResponseEntity<String> response1 = restTemplate.getForEntity("/api/retry", String.class);
    EXTERNAL_SERVICE.verify(1, getRequestedFor(urlEqualTo("/api/external")));

    EXTERNAL_SERVICE.resetRequests();

    EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
      .willReturn(serverError()));
    ResponseEntity<String> response2 = restTemplate.getForEntity("/api/retry", String.class);
    Assert.assertEquals(response2.getBody(), "all retries have exhausted");
    EXTERNAL_SERVICE.verify(3, getRequestedFor(urlEqualTo("/api/external")));
}

✅ 正常情况单次调用成功,故障时重试 3 次后触发降级。

5. 超时限制

超时限制模式为异步调用设置超时阈值。添加 /api/time-limiter 接口:

@GetMapping("/time-limiter")
@TimeLimiter(name = "timeLimiterApi")
public CompletableFuture<String> timeLimiterApi() {
    return CompletableFuture.supplyAsync(externalAPICaller::callApiWithDelay);
}

模拟延迟调用:

public String callApiWithDelay() {
    String result = restTemplate.getForObject("/api/external", String.class);
    try {
        Thread.sleep(5000);
    } catch (InterruptedException ignore) {
    }
    return result;
}

配置超时限制:

resilience4j.timelimiter.metrics.enabled=true
resilience4j.timelimiter.instances.timeLimiterApi.timeout-duration=2s
resilience4j.timelimiter.instances.timeLimiterApi.cancel-running-future=true

超时后抛出 TimeoutException,添加处理器:

@ExceptionHandler({TimeoutException.class})
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public void handleTimeoutException() {
}

测试超时行为:

@Test
public void testTimeLimiter() {
    EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external").willReturn(ok()));
    ResponseEntity<String> response = restTemplate.getForEntity("/api/time-limiter", String.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.REQUEST_TIMEOUT);
    EXTERNAL_SERVICE.verify(1, getRequestedFor(urlEqualTo("/api/external")));
}

❌ 调用耗时超过 2 秒时返回 408 状态码。

6. 舱壁隔离

舱壁模式限制外部服务的最大并发调用数。添加 /api/bulkhead 接口:

@GetMapping("/bulkhead")
@Bulkhead(name="bulkheadApi")
public String bulkheadApi() {
    return externalAPICaller.callApi();
}

配置舱壁参数:

resilience4j.bulkhead.metrics.enabled=true
resilience4j.bulkhead.instances.bulkheadApi.max-concurrent-calls=3
resilience4j.bulkhead.instances.bulkheadApi.max-wait-duration=1

超出限制时抛出 BulkheadFullException,添加处理器:

@ExceptionHandler({ BulkheadFullException.class })
@ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
public void handleBulkheadFullException() {
}

测试并发限制:

@Test
void testBulkhead() throws Exception {
  EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
      .willReturn(ok()));
  Map<Integer, Integer> responseStatusCount = new ConcurrentHashMap<>();
  ExecutorService executorService = Executors.newFixedThreadPool(5);
  CountDownLatch latch = new CountDownLatch(5);

  IntStream.rangeClosed(1, 5)
      .forEach(i -> executorService.execute(() -> {
          ResponseEntity response = restTemplate.getForEntity("/api/bulkhead", String.class);
          int statusCode = response.getStatusCodeValue();
          responseStatusCount.merge(statusCode, 1, Integer::sum);
          latch.countDown();
      }));
  latch.await();
  executorService.shutdown();

  assertEquals(2, responseStatusCount.keySet().size());
  LOGGER.info("Response statuses: " + responseStatusCount.keySet());
  assertTrue(responseStatusCount.containsKey(BANDWIDTH_LIMIT_EXCEEDED.value()));
  assertTrue(responseStatusCount.containsKey(OK.value()));
  EXTERNAL_SERVICE.verify(3, getRequestedFor(urlEqualTo("/api/external")));
}

✅ 3 个请求成功,其余请求返回 509 状态码。

7. 速率限制

速率限制模式控制对资源的请求速率。添加 /api/rate-limiter 接口:

@GetMapping("/rate-limiter")
@RateLimiter(name = "rateLimiterApi")
public String rateLimitApi() {
    return externalAPICaller.callApi();
}

配置速率限制:

resilience4j.ratelimiter.metrics.enabled=true
resilience4j.ratelimiter.instances.rateLimiterApi.register-health-indicator=true
resilience4j.ratelimiter.instances.rateLimiterApi.limit-for-period=5
resilience4j.ratelimiter.instances.rateLimiterApi.limit-refresh-period=60s
resilience4j.ratelimiter.instances.rateLimiterApi.timeout-duration=0s
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

超出限制时抛出 RequestNotPermitted,添加处理器:

@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public void handleRequestNotPermitted() {
}

测试速率限制:

@Test
public void testRatelimiter() {
    EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
      .willReturn(ok()));
    Map<Integer, Integer> responseStatusCount = new ConcurrentHashMap<>();

    IntStream.rangeClosed(1, 50)
      .parallel()
      .forEach(i -> {
          ResponseEntity<String> response = restTemplate.getForEntity("/api/rate-limiter", String.class);
          int statusCode = response.getStatusCodeValue();
          responseStatusCount.put(statusCode, responseStatusCount.getOrDefault(statusCode, 0) + 1);
      });

    assertEquals(2, responseStatusCount.keySet().size());
    assertTrue(responseStatusCount.containsKey(TOO_MANY_REQUESTS.value()));
    assertTrue(responseStatusCount.containsKey(OK.value()));
    EXTERNAL_SERVICE.verify(5, getRequestedFor(urlEqualTo("/api/external")));
}

❌ 5 个请求成功,其余返回 429 状态码。

8. Actuator 监控接口

通过 Actuator 接口监控容错模式行为。首先访问 /actuator 获取所有可用接口:

http://localhost:8080/actuator/
{
    "_links" : {
        "self" : {...},
        "bulkheads" : {...},
        "circuitbreakers" : {...},
        "ratelimiters" : {...},
        ...
    }
}

查看重试相关接口:

"retries": {
  "href": "http://localhost:8080/actuator/retries",
  "templated": false
},
"retryevents": {
  "href": "http://localhost:8080/actuator/retryevents",
  "templated": false
},
"retryevents-name": {
  "href": "http://localhost:8080/actuator/retryevents/{name}",
  "templated": true
},
"retryevents-name-eventType": {
  "href": "http://localhost:8080/actuator/retryevents/{name}/{eventType}",
  "templated": true
}

获取重试实例列表:

http://localhost:8080/actuator/retries
{
    "retries" : [ "retryApi" ]
}

观察重试事件:

{
    "retryEvents": [
    {
        "retryName": "retryApi",
        "type": "RETRY",
        "creationTime": "2022-10-16T10:46:31.950822+05:30[Asia/Kolkata]",
        "errorMessage": "...",
        "numberOfAttempts": 1
    },
    {
        "retryName": "retryApi",
        "type": "RETRY",
        "creationTime": "2022-10-16T10:46:32.965661+05:30[Asia/Kolkata]",
        "errorMessage": "...",
        "numberOfAttempts": 2
    },
    {
        "retryName": "retryApi",
        "type": "ERROR",
        "creationTime": "2022-10-16T10:46:33.978801+05:30[Asia/Kolkata]",
        "errorMessage": "...",
        "numberOfAttempts": 3
    }
  ]
}

✅ 下游服务故障时,按配置进行 3 次重试,间隔 1 秒。

9. 总结

本文介绍了在 Spring Boot 应用中使用 Resilience4j 的方法,重点演示了熔断器、速率限制、超时限制、舱壁隔离和重试机制等容错模式。完整源代码可在 GitHub 获取。


原始标题:Guide to Resilience4j With Spring Boot | Baeldung