【微服务】SpringBoot 重试常用解决方案汇总与使用详解

目录

一、前言

二、微服务重试介绍

[2.1 重试介绍](#2.1 重试介绍)

[2.1.1 为什么引入重试](#2.1.1 为什么引入重试)

[2.2 引入重试后需要注意的问题](#2.2 引入重试后需要注意的问题)

[2.3 重试的最佳实践](#2.3 重试的最佳实践)

三、微服务中常用的重试解决方案

[3.1 Spring-Retry 实现方案](#3.1 Spring-Retry 实现方案)

[3.1.1 Spring-Retry 常用注解](#3.1.1 Spring-Retry 常用注解)

[3.1.2 Spring-Retry 最佳实践](#3.1.2 Spring-Retry 最佳实践)

[3.2 Guava Retryer 实现方案](#3.2 Guava Retryer 实现方案)

[3.2.1 Guava Retryer介绍](#3.2.1 Guava Retryer介绍)

[3.2.2 Guava Retryer适用场景](#3.2.2 Guava Retryer适用场景)

[3.3 自定义注解 + AOP 实现方案](#3.3 自定义注解 + AOP 实现方案)

[3.4 Resilience4j 重试实现方案](#3.4 Resilience4j 重试实现方案)

[3.4.1 Resilience4j 是什么](#3.4.1 Resilience4j 是什么)

[3.4.2 Resilience4j 核心模块](#3.4.2 Resilience4j 核心模块)

[3.4.3 Resilience4j 使用场景](#3.4.3 Resilience4j 使用场景)

四、几种核心重试组件的具体集成和使用

[4.1 Spring-Retry 集成和使用过程](#4.1 Spring-Retry 集成和使用过程)

[4.1.1 导入依赖](#4.1.1 导入依赖)

[4.1.2 启用重试功能](#4.1.2 启用重试功能)

[4.1.3 创建一个服务类并使用@Retryable](#4.1.3 创建一个服务类并使用@Retryable)

[4.1.4 增加一个测试接口](#4.1.4 增加一个测试接口)

[4.1.5 效果测试](#4.1.5 效果测试)

[4.2 Resilience4j 集成与使用](#4.2 Resilience4j 集成与使用)

[4.2.1 导入依赖](#4.2.1 导入依赖)

[4.2.2 增加配置文件](#4.2.2 增加配置文件)

[4.2.3 增加一个模拟重试的业务方法](#4.2.3 增加一个模拟重试的业务方法)

[4.2.4 增加一个测试接口](#4.2.4 增加一个测试接口)

[4.2.5 效果验证](#4.2.5 效果验证)

[4.3 Guava Retryer 集成与使用](#4.3 Guava Retryer 集成与使用)

[4.3.1 导入核心依赖](#4.3.1 导入核心依赖)

[4.3.2 创建重试工具类](#4.3.2 创建重试工具类)

[4.3.3 业务方法中使用重试工具类](#4.3.3 业务方法中使用重试工具类)

[4.3.4 增加一个测试接口](#4.3.4 增加一个测试接口)

[4.3.5 效果测试](#4.3.5 效果测试)

[4.4 自定义注解 + AOP 实现](#4.4 自定义注解 + AOP 实现)

[4.4.1 添加自定义注解](#4.4.1 添加自定义注解)

[4.4.2 添加自定义切面类](#4.4.2 添加自定义切面类)

[4.4.3 使用自定义注解](#4.4.3 使用自定义注解)

[4.4.4 增加一个测试接口](#4.4.4 增加一个测试接口)

[4.4.5 效果测试](#4.4.5 效果测试)

[4.5 几种方案的对比](#4.5 几种方案的对比)

五、写在文末


一、前言

在微服务开发场景中,经常会碰到调用失败的情况,比如:

  • 消息发送失败;

  • 远程调用rpc接口失败;

  • 调用三方http接口失败;

  • 网络原因,幂等性场景下数据库超时导致数据增删改失败;

  • ...

诸如此类的场景有很多,对于发起调用的一方,为了保证方法(http接口)调用的可靠性,通常会通过重试的机制来保障,下面介绍几种在微服务开发模式下比较常见的实现重试的解决方案。

二、微服务重试介绍

2.1 重试介绍

在微服务架构设计中,服务之间通过网络进行调用,这不可避免地会引入不稳定性。比如网络抖动、目标服务瞬时高负载、短暂的资源不足等都可能导致一次请求失败。重试机制正是为了应对这种瞬时性故障 而设计的一种核心容错模式。

2.1.1 为什么引入重试

重试在微服务中具有非常重要的作用,具体来说,包括下面几点:

  1. 提高系统的可用性与韧性:

    1. 一个短暂的故障(如GC停顿、网络丢包)不应导致整个业务流程失败。自动重试可以掩盖这种瞬时故障,对用户而言,系统表现得更加稳定。
  2. 降低故障率:

    1. 对于非业务逻辑错误(如5xx服务器错误、连接超时),重试能显著降低最终失败的概率。
  3. 简化客户端逻辑:

    1. 客户端无需处理复杂的瞬时故障恢复逻辑,只需配置重试策略即可。

2.2 引入重试后需要注意的问题

如果重试策略使用不当,它不仅不能解决问题,反而会放大故障,甚至导致系统雪崩。具体来说,重试可能造成下面的问题:

  1. 放大流量,引发雪崩效应
  • 场景:服务B因为数据库压力过大而变慢,响应时间变长。服务A因超时而重试。如果A有多个实例,且每个实例都进行多次重试,那么涌向B的流量会呈指数级增长(例如:原始QPS为100,重试3次,理论最大QPS可达400)。这会使已经不堪重负的B服务彻底崩溃,进而导致A服务也失败,故障像雪崩一样蔓延开来。
  1. 浪费资源,增加系统负载
  • 不断重试失败的请求会消耗调用方的CPU、内存和网络带宽,同时也持续冲击着已经出现问题的基础服务。
  1. 非幂等操作导致数据不一致
  • 幂等性:一个操作执行一次与执行多次,产生的结果是相同的。

  • 风险:如果被重试的操作不是幂等的(例如:创建订单支付短信发送+1),那么重试会导致数据被错误地创建或修改多次(重复扣款、同一个订单创建两次等),造成严重的业务逻辑错误。

2.3 重试的最佳实践

为了在微服务开发中合理使用重试,下面列举了一想重试使用的最近实践:

1)仅对特定错误进行重试

  • 应该重试:网络超时、5xx服务器内部错误(特别是502/503/504)、连接拒绝等。这些通常是瞬态故障的标志。

  • 绝不重试:4xx客户端错误(如401未授权、403禁止访问、404未找到)。这类错误意味着请求本身有问题,重试多少次都不会成功。

2)使用指数退避策略

  • 机制:每次重试的间隔时间逐渐增加(例如:1s, 2s, 4s, 8s...)。

  • 好处:给故障服务更长的恢复时间,避免大量重试请求在同一时刻涌向目标服务,从而有效缓解其对系统的冲击。这是防止雪崩的关键。

3)限制最大重试次数

  • 设置一个合理的上限(如3-5次),避免无限重试消耗资源。如果重试多次后仍失败,应果断失败,并可能触发更高级别的故障处理(如熔断机制)。

4)结合熔断器模式

  • 熔断器(Circuit Breaker)是对重试的完美补充。

  • 工作原理:当失败率超过阈值时,熔断器"跳闸"进入Open状态,在一段时间内直接拒绝所有请求,而不是再去重试。这给了下游服务宝贵的恢复时间。

  • 协同效应:重试解决的是"点"上的瞬时故障,熔断器解决的是"面"上的持续故障。两者结合,共同构建了系统的韧性。

5)确保下游服务的幂等性

  • 在设计服务时,对于任何可能被重试的操作,都必须支持幂等性。常见方法:

    • 使用唯一的业务ID(如订单ID、支付流水号)。

    • 在服务端检查该ID是否已处理过,如果已处理则直接返回成功结果。

6)在网关/负载均衡层实施重试

  • 有时重试不必在业务代码中实现,可以在基础设施层(如API网关、服务网格如Istio、负载均衡器)配置。这样可以统一策略,减少业务代码的侵入性。

三、微服务中常用的重试解决方案

3.1 Spring-Retry 实现方案

Spring Retry 是一个声明式的重试框架,它的核心目标是让 Spring 应用中的方法调用具备自动重试的能力。通过简单的注解或基于代码的配置,你就可以为方法添加强大的重试策略,而无需编写冗长且易错的 try-catch-retry 循环代码。

3.1.1 Spring-Retry 常用注解

在spring-retry中,所有配置都是基于简单注解的。下面是Spring-Retry 中常用的几个注解

  • @EnableRetry -- 在 Spring Boot 项目的启动类中增加此注解以启用 SpringRetry

  • @Retryable -- 表示可以重试的任何方法

  • @Recover -- 指定后备方法

    • 全部尝试失败后的降级方法

3.1.2 Spring-Retry 最佳实践

在使用Spring-Retry 时,需要注意下面几点:

  1. 合理设置重试次数:根据业务容忍度和下游服务SLA确定

  2. 使用指数退避:避免加重下游服务负担

  3. 添加随机抖动:防止多个客户端同时重试

  4. 明确重试边界:区分可重试异常和业务异常

  5. 完善的监控:通过监听器记录重试指标

3.2 Guava Retryer 实现方案

3.2.1 Guava Retryer介绍

Guava Retryer是一个来自Google Guava库的扩展组件,它提供了一种灵活、声明式的方式来处理方法或操作调用失败后的重试逻辑。当你的代码在执行过程中遇到临时性故障(如网络抖动、服务短暂不可用)时,Guava Retryer可以帮助你自动进行重试,从而提高系统的健壮性和容错能力。

下面是Guava Retryer中的核心组件,在实际编码中会用到

|---------|--------------------------|-----------------------|
| 组件/配置类别 | 核心功能/选项 | 说明 & 常用策略 |
| 重试条件 | retryIfException() | 发生任何异常时重试 |
| | retryIfResult() | 返回结果满足特定条件(如为null)时重试 |
| 停止策略 | StopAfterAttemptStrategy | 达到最大重试次数后停止 |
| | StopAfterDelayStrategy | 总重试时间超过设定值后停止 |
| | NeverStopStrategy | 永不停止重试 |
| 等待策略 | FixedWaitStrategy | 固定时间间隔重试 |
| | IncrementingWaitStrategy | 等待时间随重试次数递增 |
| | RandomWaitStrategy | 在设定的随机时间区间内重试 |
| | ExponentialWaitStrategy | 指数退避策略,等待时间指数级增长 |
| 其他功能 | RetryListener | 重试监听器,用于记录日志等 |
| | AttemptTimeLimiter | 限制单次任务执行时间 |

3.2.2 Guava Retryer适用场景

Guava Retryer 特别适用于处理:

  • HTTP API 调用:处理 503/504 等临时错误。

  • 数据库操作:应对连接池短暂耗尽或瞬时故障。

  • 分布式锁获取:在 CAS 操作失败后自动重试。

  • 任何可能因临时性问题(如网络波动)而失败,且可自动恢复的操作。

3.3 自定义注解 + AOP 实现方案

也可以基于开源组件的实现思想自定义实现一套方法重试的方法,其基本的思路就是:自定义注解+AOP实现,具体来说,核心实现思路如下:

  • 自定义注解

    • 定义常用的参数,比如最大重试次数,重试时间间隔,指数退避,要重试的异常类型等
  • 自定义AOP切面类

    • 基于AOP的环绕通知编写逻辑,横切添加了自定义注解的方法

    • 在具体执行joinPoint.proceed()方法的时候进行异常拦截

3.4 Resilience4j 重试实现方案

3.4.1 Resilience4j 是什么

Resilience4j 是一个专为 Java 8 及函数式编程设计的轻量级容错库,其重试(Retry)机制能有效处理临时性故障。

Resilience4j 为现代分布式系统提供了全面的容错保护,通过合理的配置和组合使用,可以显著提升系统的弹性和可靠性。其函数式编程的设计理念使得代码更加简洁和可组合,是构建云原生应用的重要工具。

3.4.2 Resilience4j 核心模块

Resilience4j 的功能非常强大,重试只是它的一个模块的功能,下面汇总了Resilience4j 的核心模块能力,方便后续学习和使用

|------|-------------------|-------------|
| 模块名称 | 核心功能 | 适用场景 |
| 断路器 | 防止连续失败调用,提供故障恢复机制 | 服务调用、数据库访问 |
| 重试 | 自动重试失败的操作 | 网络波动、瞬时故障 |
| 限流器 | 控制并发执行数量 | 资源保护、防止过载 |
| 舱壁隔离 | 限制并发调用数量 | 资源隔离、防止级联故障 |
| 限时器 | 设置方法调用超时时间 | 防止长时间阻塞 |
| 缓存 | 提供方法结果缓存 | 减少重复计算、提升性能 |
| 请求重试 | 组合多个容错机制 | 复杂场景的综合保护 |

3.4.3 Resilience4j 使用场景

在下面的一些场景中可以考虑选择使用Resilience4j :

  • 远程调用和网络通信:由于网络不稳定或服务不可用,可能会出现连接问题。

  • 数据库交互:执行SQL查询或更新操作时,数据库服务器可能会出现临时性的问题,如死锁或连接丢失。

  • 外部依赖:如果你的应用程序依赖于外部服务、硬件设备或其他不可控因素,而这些依赖可能会偶尔出现故障或不可用状态,那么Spring Retry可以帮助你处理这些情况,确保应用程序在某些情况下能够自动进行重试。

  • 并发控制:在多线程环境中,可能会出现竞争条件或并发问题,导致某些操作失败。

  • 复杂的业务逻辑:某些业务逻辑可能需要多次尝试才能成功

四、几种核心重试组件的具体集成和使用

基本环境:

  • JDK17 + SpringBoot3.2.2

4.1 Spring-Retry 集成和使用过程

4.1.1 导入依赖

首先,在pom.xml中添加Spring Retry和AOP相关依赖:

java 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

<!-- 需要AOP支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4.1.2 启用重试功能

在主启动类上添加@EnableRetry注解,找到你的工程启动类,添加该注解启用

java 复制代码
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry // 启用重试功能
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

4.1.3 创建一个服务类并使用@Retryable

创建一个服务类,在需要重试的方法上添加@Retryable注解,在下面的方法中,针对Spring-Retry的几个常用注解,以及注解中的相关参数进行了详细的说明,可以结合注释一起理解,这里有个注意点:

  • 添加了@Retryable注解的方法,异常需要抛出来,而不能用try-catch进行捕捉
java 复制代码
@Service
public class TestRetryServiceImpl implements TestRetryService  {

    /**
     * value:抛出指定异常才会重试
     * include:和value一样,默认为空,当exclude也为空时,默认所有异常
     * exclude:指定不处理的异常
     * maxAttempts:最大重试次数,默认3次
     * backoff:重试等待策略,
     * 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
     * multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
     * @param code
     * @return
     * @throws Exception
     */
    @Override
    @Retryable(value = Exception.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
    public int testRetry(int code) throws Exception{
        System.out.println("test被调用,时间:"+ LocalTime.now());
          if (code==0){
              throw new Exception("情况不对头!");
          }
        System.out.println("test被调用,情况对头了!");
 
        return 200;
    }

    /**
     * Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。
     * 如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
     * 可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
     * 注意事项:
     * 方法的返回值必须与@Retryable方法一致
     * 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
     * 该回调方法与重试方法写在同一个实现类里面
     *
     * 由于是基于AOP实现,所以不支持类里自调用方法
     * 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
     * 方法内不能使用try catch,只能往外抛异常
     * @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。
     * @param e
     * @param code
     * @return
     */
    @Recover
    public int recover(Exception e, int code){
        System.out.println("回调方法执行!!!!");
        //记日志到数据库 或者调用其余的方法
        System.out.println("异常信息:"+e.getMessage());
        return 400;
    }
}

4.1.4 增加一个测试接口

为方便看效果,增加一个测试使用的接口

java 复制代码
@Resource
private TestRetryServiceImpl testRetryService;

@GetMapping("/test-retry")
public R testRetry(){
    try {
        testRetryService.testRetry(0);
    }catch (Exception e){
        log.error("异常信息:{}",e.getMessage());
    }
    return R.ok();
}

4.1.5 效果测试

启动工程后,调用一下上面的测试接口,从控制台效果可以看到,方法调用异常时执行了3次重试

4.2 Resilience4j 集成与使用

4.2.1 导入依赖

在pom文件中导入下面的依赖

  • 需要注意的是resilience4j的版本与springboot的版本有匹配要求
java 复制代码
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>2.1.0</version>
</dependency>

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

4.2.2 增加配置文件

在工程的配置文件中增加如下配置信息,更多配置细节可以参阅相关的文档资料,基本上都是见知义

java 复制代码
# Resilience4j 重试配置
resilience4j:
  retry:
    configs:
      default:
        max-attempts: 3
        wait-duration: 1s
        enable-exponential-backoff: false
        # 自定义异常类型,可以继续追加
        retry-exceptions:
          - java.lang.RuntimeException
          - java.io.IOException
      custom-config:
        max-attempts: 5
        wait-duration: 2s

    instances:
      backendA:
        base-config: default
      httpService:
        max-attempts: 4
        wait-duration: 500ms

4.2.3 增加一个模拟重试的业务方法

该方法中提供了正常的业务类中,那些需要进行重试的方法,使用Resilience4j提供的核心注解进行修饰

java 复制代码
package com.congge.service;

import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class ExternalService {

    private int attemptCount = 0;

    // 方法1:使用注解方式 + 降级方法
    @Retry(name = "backendA", fallbackMethod = "fallback")
    public String callExternalService(){
        attemptCount++;
        log.info("尝试调用外部服务 - 第 {} 次", attemptCount);
        if (attemptCount < 3) {
            throw new RuntimeException("模拟的外部服务暂时不可用");
        }
        log.info("调用外部服务成功!");
        return "Success at attempt: " + attemptCount;
    }

    // 降级方法
    public String fallback(Exception e) {
        log.warn("所有重试尝试都失败了,执行降级方法。最后异常: {}", e.getMessage());
        return "Fallback: Service unavailable after retries";
    }

    // 方法2:模拟网络调用
    @Retry(name = "httpService", fallbackMethod = "httpFallback")
    public String callHttpService(String url) {
        log.info("调用HTTP服务: {}", url);
        // 这里模拟HTTP调用失败
        throw new RuntimeException("HTTP 500 Internal Server Error");
    }

    public String httpFallback(String url, Exception e) {
        return "HTTP服务降级响应 for: " + url;
    }
}

4.2.4 增加一个测试接口

为方便看效果,增加下面一个测试接口

java 复制代码
@Resource
private ExternalService externalService;

//localhost:8081/res4j/test-retry
@GetMapping("/test-retry")
public String testRetry() {
    // 重置计数器以便多次测试
    // externalService.resetAttemptCount(); // 如果需要可以添加重置方法
    return externalService.callExternalService();
}

4.2.5 效果验证

工程启动后,调用一下接口,通过控制台输出可以看到,第一次调用失败后,会继续尝试,重试3次

Resilience4j 还提供了编程式的重试模式,篇幅原因这里不再赘述了,有兴趣可以继续尝试,实际开发中,使用注解的方式是主流,也是使用较多的一种方式。

4.3 Guava Retryer 集成与使用

4.3.1 导入核心依赖

在pom中导入下面的依赖

  • 注意与springboot版本的匹配
java 复制代码
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

4.3.2 创建重试工具类

自定义一个重试工具类

  • 该工具类中,主要接收重试次数和间隔的等待时间
java 复制代码
package com.congge.guava;

import com.github.rholder.retry.*;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;

@Component
public class GuavaRetryUtil {

    /**
     * 创建重试器 - 固定间隔
     */
    public static <T> Retryer<T> createFixedIntervalRetryer(int maxAttempts, long waitSeconds) {
        return RetryerBuilder.<T>newBuilder()
                .retryIfException()
                .withStopStrategy(StopStrategies.stopAfterAttempt(maxAttempts))
                .withWaitStrategy(WaitStrategies.fixedWait(waitSeconds, java.util.concurrent.TimeUnit.SECONDS))
                .withRetryListener(new RetryListener() {
                    @Override
                    public <V> void onRetry(Attempt<V> attempt) {
                        System.out.println("第" + attempt.getAttemptNumber() + "次重试, 时间: " + LocalTime.now());
                        if (attempt.hasException()) {
                            System.out.println("异常: " + attempt.getExceptionCause().getMessage());
                        }
                    }
                })
                .build();
    }

    /**
     * 创建重试器 - 指数退避
     */
    public static <T> Retryer<T> createExponentialRetryer(int maxAttempts, long initialWaitSeconds) {
        return RetryerBuilder.<T>newBuilder()
                .retryIfException()
                .withStopStrategy(StopStrategies.stopAfterAttempt(maxAttempts))
                .withWaitStrategy(WaitStrategies.exponentialWait(initialWaitSeconds, 2, TimeUnit.SECONDS))
                .build();
    }
}

4.3.3 业务方法中使用重试工具类

假设在自己的一个业务方法中进行模拟重试,使用上一步自定义的重试工具类方法

java 复制代码
package com.congge.guava;

import com.github.rholder.retry.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

@Service
public class GuavaRetryService {

    @Autowired
    private MyBizService myBizService;

    public String callWithRetry() {
        // 创建重试器:最多重试3次,每次间隔2秒
        Retryer<String> retryer = GuavaRetryUtil.createFixedIntervalRetryer(3, 2);

        Callable<String> task = () -> {
            System.out.println("执行调用,时间: " + java.time.LocalTime.now());
            return myBizService.doBusiness();
        };

        try {
            return retryer.call(task);
        } catch (ExecutionException | RetryException e) {
            System.out.println("所有重试失败: " + e.getMessage());
            return "fallback result";
        }
    }

    /**
     * 带条件重试的复杂示例
     */
    public String callWithConditionalRetry(String param) {
        Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
                .retryIfExceptionOfType(RuntimeException.class) // 只对网络异常重试
                .retryIfResult(result -> result.equals("retry")) // 根据结果判断是否重试
                .withStopStrategy(StopStrategies.stopAfterAttempt(4))
                .withWaitStrategy(WaitStrategies.fixedWait(1, java.util.concurrent.TimeUnit.SECONDS))
                .build();

        try {
            return retryer.call(() -> myBizService.doBusiness());
        } catch (Exception e) {
            return handleFailure(param, e);
        }
    }

    private String handleFailure(String param, Exception e) {
        // 失败处理逻辑
        return "fallback for: " + param;
    }
}

4.3.4 增加一个测试接口

增加一个测试接口,方便测试效果

java 复制代码
@Resource
private GuavaRetryService guavaRetryService;

//localhost:8081/guava/retry
@RequestMapping("/retry")
public String retry() {
    return guavaRetryService.callWithRetry();
}

4.3.5 效果测试

工程启动后,调用一下接口,通过控制台输出可以看到,第一次调用失败后,会继续尝试,重试3次

4.4 自定义注解 + AOP 实现

4.4.1 添加自定义注解

增加一个自定义注解,如下

java 复制代码
package com.congge.guava;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/guava")
public class GuavaRetryController {

    @Resource
    private GuavaRetryService guavaRetryService;

    //localhost:8081/guava/retry
    @RequestMapping("/retry")
    public String retry() {
        return guavaRetryService.callWithRetry();
    }

}

4.4.2 添加自定义切面类

增加一个自定义切面类,参考下面的代码

java 复制代码
package com.congge.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

@Aspect
@Component
public class CustomRetryAspect {

    @Around("@annotation(customRetryable)")
    public Object retryOperation(ProceedingJoinPoint joinPoint, CustomRetryable customRetryable) throws Throwable {
        int maxAttempts = customRetryable.maxAttempts();
        long interval = customRetryable.interval();
        boolean exponential = customRetryable.exponential();

        int attempt = 0;
        Throwable lastException;

        do {
            attempt++;
            try {
                System.out.println("第" + attempt + "次尝试, 时间: " + LocalTime.now());
                return joinPoint.proceed();
            } catch (Throwable e) {
                lastException = e;

                // 检查异常是否在排除列表中
                if (shouldNotRetry(e, customRetryable.noRetryExceptions())) {
                    throw e;
                }

                // 检查异常是否在重试列表中
                if (!shouldRetry(e, customRetryable.retryExceptions())) {
                    throw e;
                }

                System.out.println("执行失败,准备重试: " + e.getMessage());

                if (attempt < maxAttempts) {
                    long waitTime = exponential ? interval * (long) Math.pow(2, attempt - 1) : interval;
                    System.out.println("等待 " + waitTime + "ms 后重试");
                    Thread.sleep(waitTime);
                }
            }
        } while (attempt < maxAttempts);

        System.out.println("达到最大重试次数 " + maxAttempts + ",最终失败");
        throw lastException;
    }

    private boolean shouldRetry(Throwable e, Class<? extends Throwable>[] retryExceptions) {
        if (retryExceptions.length == 0) {
            return true;
        }
        for (Class<? extends Throwable> exceptionClass : retryExceptions) {
            if (exceptionClass.isInstance(e)) {
                return true;
            }
        }
        return false;
    }

    private boolean shouldNotRetry(Throwable e, Class<? extends Throwable>[] noRetryExceptions) {
        for (Class<? extends Throwable> exceptionClass : noRetryExceptions) {
            if (exceptionClass.isInstance(e)) {
                return true;
            }
        }
        return false;
    }
}

4.4.3 使用自定义注解

在自己的业务类方法中使用上面的自定义注解

java 复制代码
package com.congge.aop;

import com.congge.guava.MyBizService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class CustomRetryService {

    @Autowired
    private MyBizService myBizService;

    @CustomRetryable(
            maxAttempts = 4,
            interval = 1000,
            exponential = true,
            retryExceptions = {RuntimeException.class, IOException.class},
            noRetryExceptions = {IllegalArgumentException.class}
    )
    public String processWithCustomRetry(String data) {
        System.out.println("处理数据: " + data);
        // 模拟业务逻辑
        if (data.contains("error")) {
            throw new RuntimeException("运行时异常");
        }
        return "processed: " + data;
    }

    @CustomRetryable(maxAttempts = 3, interval = 2000)
    public String simpleRetryExample() {
        // 简单的重试示例
        return myBizService.doBusiness();
    }
}

4.4.4 增加一个测试接口

增加一个测试接口,方便测试效果

java 复制代码
@Autowired
private CustomRetryService CustomRetryService;

//localhost:8081/aop/retry
@RequestMapping("/retry")
public String retry() {
    return CustomRetryService.simpleRetryExample();
}

4.4.5 效果测试

工程启动后,调用一下接口,通过控制台输出可以看到,第一次调用失败后,会继续尝试,重试3次

4.5 几种方案的对比

下面这个表,对上面几种实现方案做了一个综合的对比,方便后续进行技术选择时参考使用

|---------------|-------------|-----------|---------|
| 方案 | 优点 | 缺点 | 适用场景 |
| Spring Retry | 集成简单,注解方式优雅 | 功能相对基础 | 简单的重试需求 |
| Guava Retryer | 功能强大,配置灵活 | 需要额外依赖 | 复杂的重试策略 |
| 自定义注解+AOP | 完全可控,高度定制 | 需要自行实现 | 特定业务需求 |
| Resilience4j | 功能丰富,生态完善 | 学习曲线较陡 | 微服务架构 |
| 手动重试 | 简单直接,无依赖 | 代码重复,维护困难 | 简单临时需求 |

选择建议

  1. 简单项目:推荐使用 Spring Retry 或手动重试

  2. 复杂重试策略:推荐 Guava Retryer

  3. 微服务架构:推荐 Resilience4j(它还提供熔断、限流等功能)

  4. 特定业务需求:推荐 自定义注解+AOP

五、写在文末

本文通过较大的篇幅详细介绍了在微服务开发中常用的重试解决方案,并通过实际案例演示了具体的使用,希望对看到的同学有用哦,本篇到此结束,感谢观看。