Java应用【XVI】Retry 和 Fallback

时间:2021-01-30 00:44:20

一、Retry 和 Fallback 概述

Retry和Fallback是常见的容灾方案,用于处理应用程序中的故障和错误情况。Retry指的是在发生故障或错误时重试操作,而Fallback则是在操作无法正常执行时提供备用的返回值或操作。这两种容灾方案通常结合使用,以增强应用程序的可靠性和稳定性。在本篇文章中,我们将深入了解Retry和Fallback的概念,以及它们在实际应用中的应用场景和注意事项。

二、Retry  

2.1 Retry 概述  

Retry指的是在发生错误或者异常的情况下,重新尝试执行某个操作,直到操作执行成功或者达到重试次数的上限。在实际应用中,由于网络、系统负载等原因,有可能会出现一些临时性的错误,如果立即放弃重试,就可能会导致操作失败,因此可以使用Retry机制来保证操作的可靠性。

Retry机制通常包括以下几个方面:

  1. 定义重试策略:包括重试次数、重试间隔、重试条件等。
  2. 执行重试操作:当操作发生异常或错误时,根据重试策略进行重试,直到操作成功或达到重试次数上限。
  3. 记录重试结果:记录每次重试的结果,以便后续分析和优化。

Retry机制可以应用于各种场景,比如网络请求、数据库操作、文件读写等。常见的Retry库包括Spring Retry、Netflix Hystrix等。

2.2 Retry 的实现方式  

Retry 的实现方式主要有以下几种:

  1. 编写自定义重试逻辑:通过代码实现重试的逻辑,比较灵活,可以根据具体情况自定义重试次数、间隔时间等参数。
  2. 使用Spring Retry框架:Spring Retry框架为Java应用提供了重试机制的支持,通过注解或者代码方式即可进行配置和使用,使用起来较为方便。
  3. 使用Netflix Hystrix框架:Netflix Hystrix是一个开源的容错框架,它提供了重试、断路器等功能,能够帮助我们快速实现应用的容错功能。
  4. 使用Resilience4j框架:Resilience4j是一个轻量级的容错框架,与Spring Cloud集成较为方便,提供了重试、断路器、限流等功能。

以上这些方式都是比较常见的Retry实现方式,可以根据具体情况选择合适的方式进行实现。

2.3 Retry 的优缺点

Retry机制的优点包括:

  1. 增加了应用程序的健壮性和可靠性,因为在一些暂时性的异常情况下,应用程序可以自动重试,而不是直接失败。
  2. 降低了业务流程失败率,提高了业务处理的成功率。
  3. 可以减轻服务端的负担,避免了大量的请求同时涌入,导致服务端崩溃。

Retry机制的缺点包括:

  1. 增加了系统的复杂度和开销,因为需要编写额外的代码,来实现Retry机制。
  2. 可能会引起重复操作,如果重试次数设置过多或者重试时间过短,可能会导致一些重复操作的问题。
  3. 对于一些无法恢复的错误,Retry机制无法解决,只能让应用程序在重试多次后仍然失败。

因此,在使用Retry机制时,需要谨慎考虑重试的次数、时间间隔和重试的条件,以达到最优的效果。

三、Fallback  

3.1 Fallback 概述  

Fallback是指在系统出现异常或错误时,提供一个备用方案或者错误处理策略,以保证系统的可靠性和稳定性。Fallback一般用于服务降级或者容错处理,当主服务无法正常提供服务时,通过Fallback提供的备用服务或者处理方案保证系统的正常运行。

Fallback机制是一种被动的错误处理策略,一般作为Retry机制的补充。当Retry机制无法恢复系统正常运行时,Fallback提供一个备选方案,以避免系统因异常而彻底崩溃。

在实际应用中,Fallback的实现方式包括:

  1. 提供备用服务:当主服务不可用时,提供一个备选的服务,用于处理用户的请求。
  2. 提供缓存数据:当主服务无法提供服务时,提供缓存数据,避免因主服务异常而导致用户无法获取数据。
  3. 提供默认值:当主服务无法提供服务时,提供一个默认值,用于保证系统正常运行。
  4. 提供错误处理策略:当主服务无法提供服务时,提供一个错误处理策略,用于处理异常情况,以避免系统崩溃。

3.2 Fallback 的实现方式  

Fallback的实现方式可以有多种,常见的有以下几种:

  1. 代码实现:通过编写备选方案的代码来实现Fallback机制。例如,当主逻辑执行失败时,可以直接调用备选方案的函数或方法来获取结果。
  2. 服务降级:将部分功能或服务暂时停止或切换到备选方案,以减轻系统负载或保障系统正常运行。例如,当主逻辑执行失败时,可以暂时关闭某些功能或服务,或者切换到备用的数据源或缓存中获取数据。
  3. 熔断器模式:在主逻辑执行失败的时候,自动切换到备选方案,避免系统的大规模崩溃。例如,可以设置一个阈值,当主逻辑连续失败达到一定次数时,自动切换到备选方案。
  4. 数据降级:当主逻辑执行失败时,可以返回一些伪造的数据或默认值,以避免系统异常。例如,可以返回一些随机数据或默认值,以保障系统的正常运行。

3.3 Fallback 的优缺点

优点:

  • 提供了一种备选方案,保证了系统的可靠性和稳定性。
  • 与重试不同,fallback可以使用完全不同的方式来处理请求,例如,如果一项服务不可用,则可以将请求转发到备用服务。

缺点:

  • 因为Fallback是一种补偿机制,所以不能滥用。如果过度依赖Fallback,可能会掩盖真正的问题,导致系统问题的演变变得更加复杂。
  • 需要额外的代码实现和测试,增加了开发和维护的工作量。

Fallback应该谨慎使用,必须在必要时才应该考虑使用。同时,它应该作为系统的一部分,而不是作为主要的服务机制。最重要的是,Fallback必须经过充分测试和验证,以确保它们可以在系统故障时提供可靠的备用方案。

四、Retry 和 Fallback 的抉择  

4.1 根据业务需求选择  

在选择重试或回退的策略时,需要考虑业务需求和可接受的风险程度。对于一些不可重复或无法回退的操作,如金融交易或网络支付,需要非常谨慎地考虑重试或回退的策略。

4.2 重试与回退的结合使用  

在实际应用中,重试和回退策略往往不是二选一的情况,而是需要结合使用。在一些需要保证数据一致性和稳定性的场景中,可以先尝试回退操作,如果回退失败再考虑重试。

五、Spring Retry框架

5.1集成Spring Retry框架

5.1.1 添加依赖

在Maven项目中,添加以下依赖:

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>

5.1.2 配置重试注解

使用Spring Retry的第一步是在需要重试的方法上添加注解。可以使用@Retryable注解来标记需要重试的方法,可以指定重试的最大次数,重试的异常类型等。

@Retryable(value = {IOException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void retryMethod() throws IOException {
//...
}

在这个示例中,retryMethod()方法最多重试3次,每次重试之间间隔1秒,仅当IOException被抛出时才会重试。

注意:使用@Retryable注解需要开启Spring Retry的自动代理支持。可以使用@EnableRetry注解来启用自动代理支持。

@SpringBootApplication
@EnableRetry
public class DemoApplication {

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

}

5.2 @Retryable注解

@Retryable是Spring Retry框架提供的重试机制注解之一,用于指示方法需要重试。在方法抛出特定异常时,将按照指定的策略执行多次重试,直到方法成功执行或达到最大重试次数。下面是@Retryable注解的详细解释:

@Retryable的作用范围

@Retryable注解可以应用于方法级别和类级别,用于指示需要重试的方法或类。

@Retryable的参数

@Retryable注解有多个可选参数,用于指定重试的策略。下面是一些重要的参数:

  1. value: 指定当方法抛出哪些异常时应该重试。默认值为Throwable.class,即任何异常都会触发重试。
  2. maxAttempts: 指定重试的最大次数。默认值为3。
  3. backoff: 指定重试之间的退避策略。默认为@Backoff(delay = 1000, maxDelay = 10000, multiplier = 2),即在重试之间等待一秒钟,最长等待十秒钟,等待时间指数增加,即等待时间逐渐增加。
  4. stateful: 指定是否应该在重试期间保留方法的状态。默认为false。

@Retryable的使用示例

下面是一个简单的使用@Retryable注解的示例:

@Service
public class MyService {

@Retryable(value = {SQLException.class}, maxAttempts = 2)
public void myMethod() throws SQLException {
// ...
}

}

在上面的示例中,MyService类中的myMethod()方法使用@Retryable注解指示需要重试。当该方法抛出SQLException时,将执行最多2次重试。在重试期间,Spring Retry框架会使用默认的退避策略等待一段时间。

@Retryable注解的注意事项

  1. 如果要在类级别上使用@Retryable注解,则必须在方法级别上重写该注解。
  2. @Retryable注解只能用于public方法。

5.3 监控重试情况

5.3.1 添加Spring AOP依赖

为了使用Spring AOP监控重试情况,需要在项目中添加Spring AOP依赖。可以在Maven项目的pom.xml文件中添加如下依赖:

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

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

5.3.2 定义切面监控重试情况

添加依赖后,我们需要定义一个切面,监控​​@Retryable​​注解的使用情况,并记录重试的次数和异常信息。代码如下:

@Aspect
@Component
public class RetryAspect {

private static final Logger logger = LoggerFactory.getLogger(RetryAspect.class);

@Pointcut("@annotation(org.springframework.retry.annotation.Retryable)")
public void retryable() {
}

@Around("retryable()")
public Object doRetryable(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Retryable retryable = signature.getMethod().getAnnotation(Retryable.class);

int maxAttempts = retryable.maxAttempts();
long backoff = retryable.backoff();
Class<? extends Throwable>[] include = retryable.include();
Class<? extends Throwable>[] exclude = retryable.exclude();

Throwable lastException = null;
for (int i = 0; i < maxAttempts; i++) {
try {
return joinPoint.proceed();
} catch (Throwable ex) {
lastException = ex;
if (!isRetryableException(ex, include, exclude)) {
throw ex;
}
logger.warn("Caught exception during retry: ", ex);
Thread.sleep(backoff);
}
}
throw new RetryFailedException("Retry failed after " + maxAttempts + " attempts.", lastException);
}

private boolean isRetryableException(Throwable ex, Class<? extends Throwable>[] include, Class<? extends Throwable>[] exclude) {
return (isInclude(ex, include) || include.length == 0) && !isExclude(ex, exclude);
}

private boolean isInclude(Throwable ex, Class<? extends Throwable>[] include) {
for (Class<? extends Throwable> exClass : include) {
if (exClass.isAssignableFrom(ex.getClass())) {
return true;
}
}
return false;
}

private boolean isExclude(Throwable ex, Class<? extends Throwable>[] exclude) {
for (Class<? extends Throwable> exClass : exclude) {
if (exClass.isAssignableFrom(ex.getClass())) {
return true;
}
}
return false;
}

}

这个切面定义了一个​​retryable​​​的切点,监控带有​​@Retryable​​​注解的方法。在​​doRetryable​​​方法中,我们使用反射获取​​@Retryable​​​注解上的属性值,并进行重试。如果重试的过程中抛出了不在重试范围内的异常,则直接抛出异常;如果达到最大重试次数,仍然未成功,则抛出​​RetryFailedException​​异常。

在这个切面中,我们可以将重试的次数和异常信息记录到日志中,也可以通过其他方式进行监控。