使用Spring的@Retryable注解进行自动重试

简介: 在现代软件开发中,容错性和弹性至关重要。Spring框架提供的`@Retryable`注解为处理瞬时故障提供了一种声明式、可配置的重试机制,使开发者能够以简洁的方式增强应用的自我恢复能力。本文深入解析了`@Retryable`的使用方法及其参数配置,并结合`@Recover`实现失败回退策略,帮助构建更健壮、可靠的应用程序。

介绍

软件世界是高度不可预测的,其变量范围从网络延迟到第三方服务停机。因此,确保容错性和弹性对于开发健壮的应用程序至关重要。Spring 框架的@Retryable注释提供了一种优雅的方法来自动重试可能因瞬态问题而失败的方法。这篇文章旨在深入@Retryable研究注释的用法,它使基于 Spring 的应用程序能够优雅地处理故障。

Spring的简介@Retryable

在当今的互联世界中,应用程序通常需要与外部服务、数据库和其他资源进行交互。在此过程中,他们会遇到暂时性错误、网络延迟、超时或第三方服务停机,从而导致某些操作的执行不确定。如果您的应用程序的关键代码部分容易受到此类故障情况的影响,您将希望它具有弹性并能够自我恢复,至少对于暂时性问题是这样。这就是 Spring@Retryable注释发挥作用的地方,它为您的应用程序添加了一层容错能力。

重试操作的必要性

想象一个从远程 API 获取数据的服务。在理想情况下,您发出 HTTP 请求,数据就会返回。但在现实世界中,问题就会出现。远程服务器可能负载过重,您自己的服务可能遇到网络延迟,或者可能出现任何其他暂时性问题。如果您的应用程序不能很好地处理这些场景,您最终会导致操作失败、用户愤怒和业务损失。

重试该操作似乎是一个显而易见的解决方案,但在整个应用程序中手动实现它可能会导致代码臃肿且难以维护。这是一个基本的例子:

public class ManualRetryService {
    public String fetchDataFromRemote() {
        int attempts = 0;
        while(attempts < 3) {
            try {
                // Make the API call
                return "Success";
            } catch (Exception e) {
                attempts++;
            }
        }
        throw new MyCustomException("Failed after 3 attempts");
    }
}

在此示例中,我们使用循环手动实现重试逻辑while,这增加了代码的复杂性。随着您添加更多功能(例如不同的重试间隔、要捕获的不同类型的异常等),管理将变得越来越困难。

如何@Retryable简化流程

Spring 框架通过@Retryable注释简化了这一点。通过此注释,Spring 提供了一种将重试逻辑直接添加到组件中的声明式方法,从而消除了样板代码的需要。这是我们之前的示例的外观@Retryable:

import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
    @Retryable(MyCustomException.class)
    public String fetchDataFromRemote() {
        // Make the API call that might fail
        return "Success";
    }
}

使用此代码,fetchDataFromRemote如果抛出 .Spring 异常,Spring 将自动重试该方法MyCustomException。该方法变得更加清晰,我们可以使用更高级的重试选项轻松扩展它。

花絮

当您使用 注释方法时@Retryable,Spring 会围绕所注释的方法创建一个代理。这允许框架拦截对该方法的调用并透明地添加重试逻辑。这与 Spring 中使用代理的其他功能类似,例如使用@Transactional.

为什么选择@Retryable手动重试

  • 代码清洁度:您的业务逻辑与容错逻辑保持分离。
  • 可维护性:更容易扩展或修改重试配置,而无需触及业务代码。
  • 可读性:注释使开发人员的意图变得清晰,从而更容易理解方法的预期行为。

通过使用@Retryable注释,您可以向方法添加强大、灵活的重试逻辑,而不会使代码库复杂化。它让您可以专注于业务逻辑,同时框架处理容错,使您的应用程序具有弹性和可维护性。

配置@Retryable

注释@Retryable不是一刀切的解决方案,而是一种高度可定制的功能,可以适应多种场景。它的灵活性是通过一组丰富的配置参数实现的,这些参数使您可以微调重试的管理方式。无论您正在处理简单还是复杂的故障场景,@Retryable注释都能满足您的需求。

指定异常类型

您的方法可能会引发多种类型的异常,但您可能不想对所有异常重试该操作。例如,重试由于 a 而失败的操作NullPointerException可能毫无意义,因为异常可能是由编程错误引起的。另一方面,重试失败的网络操作是有意义的。

使用@Retryable,您可以使用其属性指定应触发重试的异常类型value。您可以通过以下方式告诉 Spring 仅在发生特定异常时重试方法:

@Retryable(value = { MyNetworkException.class, TimeoutException.class }) 
public String fetchRemoteData () { 
    // 可能失败的网络调用
    return  "Data" ; 
}

配置最大尝试次数

默认情况下,@Retryable注释将在放弃之前重试失败的操作最多 3 次。但是,您可以通过设置maxAttempts属性轻松自定义此行为:

@Retryable(value = MyNetworkException.class, maxAttempts = 5) 
public String fetchRemoteData () { 
    // 可能失败的网络调用
    return  "Data" ; 
}

重试之间的延迟

通常,在重试尝试之间引入延迟很有用。这在外部服务可能暂时过载的情况下很有帮助。Spring 允许您通过属性来配置它backoff,该属性接受@Backoff注释。以下示例指定重试尝试之间有两秒的延迟:

@Retryable(value = MyNetworkException.class, backoff = @Backoff(delay = 2000)) 
public String fetchRemoteData () { 
    // 可能失败的网络调用
    return  "Data" ; 
}

指数退避

在某些情况下,您可能需要使用指数退避策略,这会增加每次重试尝试之间的延迟。当您处理需要时间恢复或扩展的服务时,这会很有帮助。注释@Backoff通过其属性支持这一点multiplier:

@Retryable(value = MyNetworkException.class, backoff = @Backoff(delay = 1000, multiplier = 2)) 
public String fetchRemoteData () { 
    // 可能失败的网络调用
    return  "Data" ; 
}

组合多个参数

@Retryable当您开始组合这些属性时,真正的力量就会显现出来。下面是一个为微调重试策略设置多个属性的示例:

@Retryable(value = { MyNetworkException.class, TimeoutException.class }, 
           maxAttempts = 5, 
           backoff = @Backoff(delay = 1000, multiplier = 2)) 
public String fetchRemoteData () { 
    // 可能失败的网络调用
    return  "Data" ; 
}

此示例将重试最多 5 次,仅针对MyNetworkException和TimeoutException,从 1000 毫秒的延迟开始,并将每次后续重试之间的延迟加倍。

使其有条件

在某些情况下,您可能希望根据某些运行时条件或抛出的异常来控制是否动态地继续重试。您可以使用condition该属性来实现此目的,它接受 SpEL 表达式。

@Retryable(value = MyNetworkException.class, condition = "#{#root.args[0] != 'no-retry'}") 
public String fetchRemoteData (String controlFlag) { 
    // 可能失败的网络调用
    return  "Data" ; 
}

在此示例中,如果参数为“no-retry”,则不会继续重试controlFlag。

通过利用这些不同的属性,您可以创建一个高度复杂的重试机制,以满足特定的项目需求,而不会因为容错代码而扰乱您的业务逻辑。

了解参数

该@Retryable注释带有大量参数来自定义重试逻辑。这些参数协调一致,提供了一个开箱即用的强大重试机制。无论您想要具有固定间隔的简单重试,还是需要基于条件重试的指数退避等复杂机制,了解这些参数都将帮助您轻松实现。

value

该value参数指定哪些异常应触发重试。它采用类数组Throwable作为其值。默认情况下,它设置为重试所有扩展的异常Throwable。

@Retryable(value = { MyNetworkException.class, TimeoutException.class }) 
public Stringexecute ( ) { 
    // 代码
}

include

与 类似value,该include参数允许您指定应触发重试的异常。不同之处在于,include除了已定义的异常之外,还允许指定异常value。

@Retryable(value = MyNetworkException.class, include = TimeoutException.class) 
public Stringexecute ( ) { 
    // 代码
}

exclude

相反,该exclude参数允许您定义哪些异常不应触发重试。当您想要广泛捕获异常但排除特定异常时,这非常有用。

@Retryable(value = Exception.class, except = IllegalArgumentException.class) 
public Stringexecute ( ) { 
    // 代码
}

maxAttempts

该maxAttempts参数指示带注释的方法的最大尝试次数。默认值为3。

@Retryable(maxAttempts = 5) 
public Stringexecute ( ) { 
    // 代码
}

backoff

该backoff参数允许您在重试尝试之间实现延迟。这需要一个@Backoff注释,您可以在其中指定延迟(以毫秒为单位)和一个可选的指数退避乘数。

@Retryable(backoff = @Backoff(delay = 2000, multiplier = 2)) 
public Stringexecute ( ) { 
    // 代码
}

condition

该condition参数允许您指定计算结果为布尔值的 SpEL(Spring 表达式语言)表达式。仅当该表达式的计算结果为 时,才会激活重试逻辑true。

@Retryable(condition = "#{#arg > 100}") 
public String execute ( int arg) { 
    // 代码
}

stateful

该stateful参数指定重试应该是有状态的还是无状态的。在有状态重试中,会记住第一次失败尝试的状态,并据此进行后续重试。另一方面,无状态重试是相互独立的。

@Retryable ( stateful = true) 
public Stringexecute () { // 代码}

listeners

该listeners参数允许您指定将在每次重试尝试时收到通知的 bean。该 bean 必须实现该RetryListener接口。这对于日志记录、指标或其他副作用很有用。

@Retryable(listeners = "myRetryListenerBean") 
public Stringexecute ( ) { 
    // 代码
}

把它们放在一起

当这些参数结合使用来创建复杂的重试机制时,真正的魔力就会发生。这是一个例子:

@Retryable(value = { MyNetworkException.class, TimeoutException.class }, 
           maxAttempts = 5, 
           backoff = @Backoff(delay = 2000, multiplier = 2), 
           condition = "#{#arg != 'no-retry'}") 
public Stringexecute (String arg) { //代码}

MyNetworkException仅当抛出or时,此示例才会重试该方法最多 5 次TimeoutException。重试将以 2000 毫秒的初始延迟进行,每次尝试后延迟加倍,并且仅当参数arg不是时才会继续'no-retry'。

通过深入了解这些参数及其相互作用,您就可以@Retryable以最有效的方式使用这些参数,从而确保您的应用程序尽可能具有弹性和容错能力。

结合@Retryable @Recover

使用@Retryable注释时,必须考虑如果所有重试都失败会发生什么情况。虽然重试可以增加操作成功的机会,但不能保证成功。这就是@Recover注释发挥作用的地方。

@Recover的作用

该@Recover注释允许您定义一个后备方法,当配置的所有重试尝试都用完时,将调用该方法@Retryable。回退方法旨在执行替代逻辑,例如发送错误消息、尝试连接到备份服务或更新应用程序状态以反映故障。

下面用一个简单的例子来说明它的使用:

import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
    @Retryable(MyNetworkException.class)
    public String fetchDataFromRemote() {
        // Network call that might fail
        return "Data";
    }
    @Recover
    public String recover(MyNetworkException e) {
        // Fallback logic
        return "Default Data";
    }
}

在此示例中,如果该fetchDataFromRemote方法抛出 aMyNetworkException并耗尽所有重试尝试,recover则将调用该方法,并返回“默认数据”作为后备。

匹配异常类型

该@Recover方法的参数列表必须与该@Retryable方法的参数列表匹配,但要为要从中恢复的异常类型添加一个附加的第一个参数。

例如,如果该@Retryable方法采用两个这样的参数:

@Retryable(MyNetworkException.class) 
public String fetchData (String param1, int param2) { 
    // 网络调用
}

然后,@Recover方法签名可能如下所示:

@Recover 
public String recovery (MyNetworkException e, String param1, int param2) { 
    // 回退逻辑
}

多种恢复路径

您可以@Recover为不同类型的异常定义多种方法。这样,您可以根据导致所有重试失败的异常类型执行不同的恢复逻辑。您可以这样设置:

@Retryable(value = { MyNetworkException.class, TimeoutException.class }) 
public String fetchDataFromRemote () { 
    // 网络调用
} 
@Recover 
public String recovery (MyNetworkException e) { 
    return  "Default Data for MyNetworkException" ; 
} 
@Recover 
public String recovery (TimeoutException e) { 
    return  "TimeoutException 的默认数据" ; 
}

在此示例中,有两种@Recover方法:一种是 for MyNetworkException,另一种是 for TimeoutException。@Recover根据导致重试次数耗尽的异常,将调用适当的方法。

使其有条件

与 类似@Retryable,您还可以@Recover使用带有 SpEL(Spring 表达式语言)表达式的参数向方法添加条件condition。这使您可以根据动态条件甚至微调您的后备行为。

@Recover 
public String recovery (MyNetworkException e, String param1) { 
    if ( "special_case" .equals(param1)) { 
        return  "特殊恢复逻辑" ; 
    } 
    return  "通用恢复逻辑" ; 
}

何时使用@Recover

虽然@Retryable可以帮助从暂时性故障中恢复,@Recover但当您必须处理更持久的问题或在所有重试尝试失败后想要执行“B 计划”时,它就会发挥作用。

通过与 结合使用@Retryable,@Recover您可以构建一个强大的、自我恢复的系统,能够处理暂时性和更持久的问题,确保更高水平的容错能力并改善整体用户体验。

用例

远程服务调用

当您的应用程序依赖于可能暂时不可用或面临间歇性问题的远程服务时,使用@Retryable可以增加成功完成操作的可能性。

@Retryable(MyNetworkException.class) 
public String fetchFromRemoteService () { 
    // 对外部 API 的 HTTP 请求
    return  "Data" ; 
}

分布式系统

在微服务或分布式架构中,网络故障或服务临时不可用是很常见的。@Retryable可以确保您的系统能够抵御此类故障。

@Retryable(TimeoutException.class) 
public  void  sendMessageToQueue (String message) { 
    // 发送消息到消息队列
}

数据库

有时,数据库操作可能会由于死锁或临时连接问题而失败。重试事务通常可以解决这些问题。

@Retryable(DatabaseException.class) 
public  void  updateDatabaseRecord () { 
    // 更新数据库记录
}

文件操作

文件操作可能会由于缺乏权限或磁盘空间等各种原因而失败。解决导致失败的具体问题后,重试可能会有效。

@Retryable(IOException.class) 
public  void  writeFile () { 
    // 写入文件
}

复杂条件重试

您可以使用该condition参数根据运行时条件实现复杂的重试逻辑,使其非常灵活。

@Retryable(value = CustomException.class, condition = "#{#someArg > 100}") 
public  void  complexConditionMethod ( int someArg) { 
    // 做某事
}

局限性

性能开销

每次重试都会消耗资源,无论是 CPU 周期、内存,甚至是网络带宽。过多的重试可能会导致性能瓶颈。

并不适合所有错误

并非所有类型的错误都可以重试。例如,由于“找不到文件”异常而重试失败的操作可能会导致重复失败。

级联故障

微服务架构中的重试次数过多可能会导致级联故障,其中一项失败的服务也会导致其他服务失败。

有状态系统的复杂性

在维护状态的系统中,更改状态的失败操作可能会使重试变得复杂。

错误处理

使用@Recover方法可能会导致错误处理逻辑分散,这在较大的代码库中可能难以管理。

结论

Spring 中的和注释提供了一种优雅的声明式方法来向应用程序添加重@Retryable试@Recover逻辑和容错能力。虽然它们提供了丰富的可定制选项,但明智地使用它们非常重要,同时牢记它们的用例和限制。通过了解其功能的深度并明智地应用它们,您可以显着增强应用程序的弹性和稳健性。

相关文章
|
2月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
418 2
|
3月前
|
缓存 监控 Java
SpringBoot @Scheduled 注解详解
使用`@Scheduled`注解实现方法周期性执行,支持固定间隔、延迟或Cron表达式触发,基于Spring Task,适用于日志清理、数据同步等定时任务场景。需启用`@EnableScheduling`,注意线程阻塞与分布式重复问题,推荐结合`@Async`异步处理,提升任务调度效率。
585 128
|
3月前
|
XML Java 数据格式
常用SpringBoot注解汇总与用法说明
这些注解的使用和组合是Spring Boot快速开发和微服务实现的基础,通过它们,可以有效地指导Spring容器进行类发现、自动装配、配置、代理和管理等核心功能。开发者应当根据项目实际需求,运用这些注解来优化代码结构和服务逻辑。
310 12
消息中间件 Java Kafka
249 0
|
3月前
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
200 0
探索Spring Boot的@Conditional注解的上下文配置
|
3月前
|
智能设计 Java 测试技术
Spring中最大化@Lazy注解,实现资源高效利用
本文深入探讨了 Spring 框架中的 `@Lazy` 注解,介绍了其在资源管理和性能优化中的作用。通过延迟初始化 Bean,`@Lazy` 可显著提升应用启动速度,合理利用系统资源,并增强对 Bean 生命周期的控制。文章还分析了 `@Lazy` 的工作机制、使用场景、最佳实践以及常见陷阱与解决方案,帮助开发者更高效地构建可扩展、高性能的 Spring 应用程序。
147 0
Spring中最大化@Lazy注解,实现资源高效利用
|
2月前
|
Java 测试技术 数据库连接
【SpringBoot(四)】还不懂文件上传?JUnit使用?本文带你了解SpringBoot的文件上传、异常处理、组件注入等知识!并且带你领悟JUnit单元测试的使用!
Spring专栏第四章,本文带你上手 SpringBoot 的文件上传、异常处理、组件注入等功能 并且为你演示Junit5的基础上手体验
871 2
|
2月前
|
JavaScript Java Maven
【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
SpringBoot专栏第二章,从本章开始正式进入SpringBoot的WEB阶段开发,本章先带你认识yaml配置文件和资源的路径配置原理,以方便在后面的文章中打下基础
321 3
|
5月前
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
552 0
第07课:Spring Boot集成Thymeleaf模板引擎
|
5月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
939 0