JAVA 如何优雅实现接口调用重试 + 降级?基于 Resilience4j 的实战

好的,让我们开始吧。

Resilience4j:优雅地实现接口调用重试与降级

大家好,今天我们来聊聊如何在 Java 应用中优雅地实现接口调用重试与降级。在分布式系统中,服务调用失败是常态,网络抖动、服务过载、依赖服务故障等都可能导致调用失败。为了提高系统的稳定性和可用性,我们需要对接口调用进行重试和降级处理。

Resilience4j 是一个轻量级、易于使用的容错库,提供了重试、断路器、限流、隔离舱、时间限制等多种容错机制。它基于 Java 8+ 函数式编程设计,可以很好地与 Spring Boot 集成。

为什么选择 Resilience4j?

  • 轻量级: 依赖少,性能开销小。
  • 易于使用: API 简洁明了,易于集成。
  • 功能丰富: 提供了多种容错机制,满足不同场景的需求。
  • 与 Spring Boot 集成良好: 提供了 Spring Boot Starter,方便在 Spring Boot 应用中使用。
  • 监控和指标: 内置对 Micrometer 的支持,方便监控和度量容错策略的执行情况。

核心概念

在深入代码之前,我们需要了解 Resilience4j 的几个核心概念:

概念 描述
Retry 重试:当接口调用失败时,自动重试多次,直到成功或达到最大重试次数。
CircuitBreaker 断路器:当接口调用失败率达到一定阈值时,断路器会打开,阻止后续的调用,避免雪崩效应。当断路器处于打开状态一段时间后,会进入半开状态,尝试允许部分请求通过,如果请求成功,则断路器关闭,否则保持打开状态。
RateLimiter 限流:限制接口的调用频率,防止服务被过载。
Bulkhead 隔离舱:将接口调用隔离到不同的线程池中,防止一个接口的故障影响到其他接口。
TimeLimiter 时间限制:限制接口调用的最长时间,如果超过时间限制,则抛出异常。
Fallback 降级:当接口调用失败或被断路器阻止时,执行降级逻辑,返回一个默认值或执行其他操作。

实战:使用 Resilience4j 实现接口调用重试与降级

我们将模拟一个简单的场景:一个订单服务调用支付服务进行支付。如果支付服务出现故障,我们将使用 Resilience4j 进行重试和降级处理。

1. 添加 Maven 依赖

首先,在 pom.xml 文件中添加 Resilience4j 的依赖:

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

2. 配置 Resilience4j

application.yml 文件中配置 Resilience4j 的重试和断路器策略:

resilience4j:
  retry:
    instances:
      paymentRetry:
        maxAttempts: 3 # 最大重试次数
        waitDuration: 1000 # 重试间隔时间,单位毫秒
        retryOnException:
          - java.io.IOException # 指定需要重试的异常类型
          - org.springframework.web.client.HttpServerErrorException
  circuitbreaker:
    instances:
      paymentCircuitBreaker:
        registerHealthIndicator: true
        failureRateThreshold: 50 # 失败率阈值,超过此阈值断路器打开
        minimumNumberOfCalls: 10 # 在计算失败率之前,至少需要多少次调用
        automaticTransitionFromOpenToHalfOpenEnabled: true
        waitDurationInOpenState: 5000 # 断路器打开后,等待多长时间进入半开状态,单位毫秒
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态下允许通过的请求数量
        slidingWindowSize: 10 # 滑动窗口大小,用于计算失败率
        slidingWindowType: COUNT_BASED # 滑动窗口类型,基于调用次数
        ignoreExceptions:
          - java.lang.IllegalStateException # 指定不需要触发断路器的异常类型

3. 定义支付服务接口

public interface PaymentService {

    /**
     * 支付接口
     * @param orderId 订单ID
     * @return 支付结果
     */
    String pay(String orderId);
}

4. 实现支付服务

import org.springframework.stereotype.Service;
import java.util.Random;
import java.io.IOException;

@Service
public class PaymentServiceImpl implements PaymentService {

    private final Random random = new Random();

    @Override
    public String pay(String orderId) {
        // 模拟支付过程,有一定概率失败
        if (random.nextDouble() < 0.3) {
            throw new RuntimeException("支付失败:模拟支付服务异常");
            // 可以模拟不同的异常类型,比如 IO 异常
            // throw new IOException("模拟IO异常");
        }
        return "支付成功,订单ID:" + orderId;
    }
}

5. 使用 Resilience4j 的 Retry 和 CircuitBreaker

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private PaymentService paymentService;

    @Retry(name = "paymentRetry", fallbackMethod = "payFallback")
    @CircuitBreaker(name = "paymentCircuitBreaker", fallbackMethod = "payFallback")
    public String placeOrder(String orderId) {
        System.out.println("调用支付服务,订单ID:" + orderId);
        return paymentService.pay(orderId);
    }

    /**
     * 降级方法
     * @param orderId 订单ID
     * @param ex 异常信息
     * @return 降级结果
     */
    public String payFallback(String orderId, Throwable ex) {
        System.err.println("支付服务降级,订单ID:" + orderId + ",异常信息:" + ex.getMessage());
        return "支付服务繁忙,请稍后再试 (订单ID:" + orderId + ")";
    }

    /**
     * 如果你需要针对不同的异常采取不同的降级策略,可以定义多个降级方法,并在 CircuitBreaker 或 Retry 注解中指定对应的异常类型。
     * 例如:
     */
    @CircuitBreaker(name = "paymentCircuitBreaker", fallbackMethod = "ioExceptionFallback")
    @Retry(name = "paymentRetry", fallbackMethod = "ioExceptionFallback")
    public String placeOrderWithIOException(String orderId) {
        System.out.println("调用支付服务 (模拟IO异常),订单ID:" + orderId);
        try {
            // 模拟 IO 异常
            throw new java.io.IOException("模拟 IO 异常");
        } catch (java.io.IOException e) {
            throw new RuntimeException(e); // 因为placeOrderWithIOException没有声明throws IOException,所以需要包装一下
        }
    }

    public String ioExceptionFallback(String orderId, Throwable ex) {
        System.err.println("IO 异常降级,订单ID:" + orderId + ",异常信息:" + ex.getMessage());
        return "IO 异常降级:支付服务网络异常,请检查网络连接 (订单ID:" + orderId + ")";
    }
}

解释:

  • @Retry(name = "paymentRetry", fallbackMethod = "payFallback"): 使用 paymentRetry 这个重试配置,如果调用失败,则执行 payFallback 方法。
  • @CircuitBreaker(name = "paymentCircuitBreaker", fallbackMethod = "payFallback"): 使用 paymentCircuitBreaker 这个断路器配置,如果调用失败,则执行 payFallback 方法。
  • payFallback(String orderId, Throwable ex): 降级方法,接收订单 ID 和异常信息作为参数。 注意:降级方法的参数必须包含原始方法的参数,并且最后一个参数必须是 Throwable 类型,用于接收异常信息。
  • 如果需要针对不同的异常采取不同的降级策略,可以定义多个降级方法,并在 CircuitBreakerRetry 注解中指定对应的异常类型。 例如,可以定义一个 ioExceptionFallback 方法专门处理 IOException 异常。

6. 测试

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Resilience4jDemoApplication implements CommandLineRunner {

    @Autowired
    private OrderService orderService;

    public static void main(String[] args) {
        SpringApplication.run(Resilience4jDemoApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        for (int i = 0; i < 20; i++) {
            String orderId = "ORDER-" + i;
            String result = orderService.placeOrder(orderId);
            System.out.println("订单 " + orderId + " 结果: " + result);

            // 测试 IO 异常的降级
            // String ioResult = orderService.placeOrderWithIOException(orderId);
            // System.out.println("订单 " + orderId + " (IO异常) 结果: " + ioResult);

            Thread.sleep(500); // 稍微等待一下,模拟并发请求
        }
    }
}

运行程序,可以看到控制台输出重试和降级的信息。 当支付服务出现异常时,placeOrder 方法会根据 paymentRetry 配置进行重试。如果重试失败,或者断路器打开,则会执行 payFallback 方法,返回降级结果。

7. 使用 AOP 配置

除了使用注解,还可以使用 AOP 来配置 Resilience4j。 这种方式更加灵活,可以统一管理容错策略。

首先,定义一个 Aspect:

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Resilience4jAspect {

    @Autowired
    private RetryRegistry retryRegistry;

    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;

    @Around("@annotation(com.example.resilience4jdemo.annotation.Resilient)")
    public Object handleResilience(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        Retry retry = retryRegistry.retry("paymentRetry");
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentCircuitBreaker");

        return Retry.decorateCheckedSupplier(retry, CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> {
            try {
                return joinPoint.proceed(args);
            } catch (Throwable e) {
                throw e;
            }
        })).get();
    }
}

然后,定义一个自定义注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resilient {
}

最后,在需要进行容错处理的方法上添加 @Resilient 注解:

import com.example.resilience4jdemo.annotation.Resilient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private PaymentService paymentService;

    @Resilient
    public String placeOrder(String orderId) {
        System.out.println("调用支付服务,订单ID:" + orderId);
        return paymentService.pay(orderId);
    }
}

这种方式的优点是可以将容错逻辑与业务逻辑分离,提高代码的可维护性。

监控和度量

Resilience4j 提供了与 Micrometer 集成的功能,可以方便地监控和度量容错策略的执行情况。

  1. 添加 Micrometer 的依赖:
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
  1. application.yml 文件中开启 Micrometer 的监控:
management:
  endpoints:
    web:
      exposure:
        include: prometheus
  metrics:
    tags:
      application: resilience4j-demo
  1. 访问 /actuator/prometheus 端点,可以查看 Resilience4j 的监控指标。 例如: resilience4j_circuitbreaker_calls_seconds_maxresilience4j_retry_calls_seconds_count

一些最佳实践

  • 谨慎选择重试策略: 重试策略应该根据具体的业务场景进行选择。例如,对于幂等性操作,可以进行多次重试;对于非幂等性操作,需要谨慎重试,避免重复执行。
  • 合理配置断路器: 断路器的阈值和等待时间需要根据服务的实际情况进行调整,避免误判。
  • 提供有意义的降级方案: 降级方案应该提供有意义的替代方案,例如返回默认值、显示友好提示、调用备用服务等。
  • 监控和度量容错策略的执行情况: 通过监控指标,可以了解容错策略的有效性,并及时进行调整。
  • 避免过度使用容错机制: 容错机制虽然可以提高系统的稳定性,但是也会增加系统的复杂性。应该根据实际需要,选择合适的容错机制。
  • 考虑 bulkhead 隔离舱策略 隔离不同的业务服务,避免级联影响。
  • 使用 RateLimiter 限流 防止突发流量,保护后端服务。

总结

Resilience4j 是一个功能强大、易于使用的容错库,可以帮助我们优雅地实现接口调用重试与降级。 通过合理配置 Resilience4j 的重试、断路器等策略,可以提高系统的稳定性和可用性。 并结合 Micrometer 进行监控,可以更好地了解容错策略的执行情况,并及时进行调整。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注