好的,让我们开始吧。
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类型,用于接收异常信息。- 如果需要针对不同的异常采取不同的降级策略,可以定义多个降级方法,并在
CircuitBreaker或Retry注解中指定对应的异常类型。 例如,可以定义一个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 集成的功能,可以方便地监控和度量容错策略的执行情况。
- 添加 Micrometer 的依赖:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
- 在
application.yml文件中开启 Micrometer 的监控:
management:
endpoints:
web:
exposure:
include: prometheus
metrics:
tags:
application: resilience4j-demo
- 访问
/actuator/prometheus端点,可以查看 Resilience4j 的监控指标。 例如:resilience4j_circuitbreaker_calls_seconds_max,resilience4j_retry_calls_seconds_count。
一些最佳实践
- 谨慎选择重试策略: 重试策略应该根据具体的业务场景进行选择。例如,对于幂等性操作,可以进行多次重试;对于非幂等性操作,需要谨慎重试,避免重复执行。
- 合理配置断路器: 断路器的阈值和等待时间需要根据服务的实际情况进行调整,避免误判。
- 提供有意义的降级方案: 降级方案应该提供有意义的替代方案,例如返回默认值、显示友好提示、调用备用服务等。
- 监控和度量容错策略的执行情况: 通过监控指标,可以了解容错策略的有效性,并及时进行调整。
- 避免过度使用容错机制: 容错机制虽然可以提高系统的稳定性,但是也会增加系统的复杂性。应该根据实际需要,选择合适的容错机制。
- 考虑 bulkhead 隔离舱策略 隔离不同的业务服务,避免级联影响。
- 使用 RateLimiter 限流 防止突发流量,保护后端服务。
总结
Resilience4j 是一个功能强大、易于使用的容错库,可以帮助我们优雅地实现接口调用重试与降级。 通过合理配置 Resilience4j 的重试、断路器等策略,可以提高系统的稳定性和可用性。 并结合 Micrometer 进行监控,可以更好地了解容错策略的执行情况,并及时进行调整。