接口调用总失败?试试Spring官方重试框架Spring-Retry

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

在现代分布式系统和微服务架构中,服务间的网络调用变得无比频繁。然而,网络是不可靠的,它可能因为瞬时故障(如网络抖动、服务短暂过载、资源临时不可用等)导致调用失败。简单地一失败就放弃,会严重降低系统的健壮性和用户体验尤其调用第三方接口时表现最明显。

因此,重试机制 成为了构建 resilient(弹性)系统不可或缺的一环。Spring Retry 作为 Spring 生态系统中的一员,提供了一种声明式、注解驱动的高效重试解决方案,能让你的代码在应对短暂故障时变得更加健壮和优雅。

02 简介

Spring Retry 的核心思想是 AOP(面向切面编程) 。它通过代理(Proxy)机制,在目标方法的外部包裹一层重试逻辑。

当你调用一个被 @Retryable 注解的方法时,实际上是在调用一个由 Spring 生成的代理对象,这个代理对象负责在方法执行失败时,根据你配置的策略进行重试。

03 最佳实践

因为Spring Retry的核心思想是AOP,所以声明式重试需要依赖AOP

3.1 Maven依赖

Spring Boot项目依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

如果是传统的Spring项目,则需要如下依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.5</version> <!-- 请使用最新版本 -->
</dependency>
<!-- Spring AOP 依赖 (Spring Boot项目一般已包含) -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<!-- 如果你使用@Recover,还需要AspectJ的依赖 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

3.2 启用重试功能

@EnableRetry

在项目的配置类或者主启动类上添加 @EnableRetry 注解,以启用 Spring Retry 的自动代理功能。

java 复制代码
@SpringBootApplication
@EnableRetry
public class RetryApplication {

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

3.3 方法添加@Retryable

@Retryable是最核心的注解,用于标注哪些方法在发生异常时需要重试。

基本参数说明:

  • value / include: 指定需要重试的异常类型数组。默认为空(重试所有异常)。
  • maxAttempts: 最大重试次数(包括第一次调用)。例如设置为 3,意味着最多会调用 3 次方法(1次初始调用 + 2次重试)。
  • backoff: 设置重试退避策略,通常与 @Backoff 注解联用。
  • recover: 服务降级的方法。

3.4 案例代码

我们通过传递参数status来控制异常,重试参数:

  • retryFor = RuntimeException.class:只针对RuntimeException重试。
  • maxAttempts = 4:包含第一次请求总共请求4次
  • delay = 1000:延迟1s重试
  • multiplier = 2.0:延迟时间的乘数(用于指数退避),步长,延迟序列为:1s, 2s, 4s...
java 复制代码
@Service
public class RetryServiceImpl implements RetryService{

    @Override
    @Retryable(retryFor = RuntimeException.class,
                maxAttempts = 4,
                backoff = @Backoff(delay = 1000,
                        multiplier = 2.0
                )
    )
    public String invokeMethod(Integer status) {
        System.out.println("invokeMethod 调用时间:"
                + LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyy-dd-MM HH:mm:ss")));
        // 模拟异常
        if (status != null) {
            throw new RuntimeException("服务中断了......");
        }
        return "SUCCESS";
    }
}

效果:

3.5 服务降级

如果重试完成之后还是响应失败,那么就可以服务降级,返回指定的数据。

这是需要一个注解@Recover来指定降级的方法。

这里的方法名可以随意取,默认会被@Retryable识别,也可以通过recover参数指定方法。

@Recover方法不被执行的原因:

  • 未加@Recover注解

  • recover参数指定错误

  • @Recover修改的方法参数必须是retryFor的异常以及原方法的参数。

    如:

    java 复制代码
    @Retryable(retryFor = RuntimeException.class)
    public String test(Integer status) {
        // ...  
    }
    
    @Recover
    public String testRecover(RuntimeException e, Integer status) {
      	// ...
    }

04 编程式重试

编程式重试的核心是RetryTemplate,使用更加灵活,不受AOP限制。

java 复制代码
@Test
void test() throws Throwable {
    RetryTemplate retryTemplate = RetryTemplate.builder()
            .maxAttempts(4)
            .exponentialBackoff(1000, 2, 10000)
            .retryOn(RuntimeException.class)
            .build();


    String execute = retryTemplate.execute(
            (RetryCallback<String, Throwable>) context -> {
        RetryService retryService = new RetryServiceImpl();
        return retryService.invokeMethod(1);
    }, context -> "服务降级了!");

    System.out.println(execute);
}

效果:

05 小结

Spring Retry 提供了 声明式编程式 两种风格来满足不同场景的需求:

  • 无脑上注解 :对于大多数通用的重试场景,使用 @Retryable + @Backoff + @Recover 是最高效、最清晰的选择。
  • 动手写代码 :当你需要极致灵活动态控制 时,RetryTemplate 配合各种 RetryPolicyBackOffPolicy 是你的不二之选。

快去试试水吧!

相关推荐
Cache技术分享2 小时前
191. Java 异常 - 捕获与处理异常
前端·后端
努力的小郑2 小时前
从一次分表实践谈起:我们真的需要复杂的分布式ID吗?
分布式·后端·面试
RoyLin2 小时前
TypeScript设计模式:模板方法模式
前端·后端·node.js
咖啡Beans2 小时前
SpringCloud网关Gateway功能实现
java·spring cloud
GeekAGI2 小时前
如何重复执行 curl 请求:8种实用方法详解
后端
杨杨杨大侠2 小时前
Atlas Mapper 案例 01:初级开发者 - 电商订单系统开发
java·开源·github
华仔啊2 小时前
Java 8都出了这么多年,Optional还是没人用?到底卡在哪了?
java
LH_R2 小时前
OneTerm开源堡垒机实战(三):功能扩展与效率提升
运维·后端·安全
用户092 小时前
Gradle Cache Entries 深度探索
android·java·kotlin