Spring Retry 实战:优雅搞定重试需求

工作里总遇到需要重试的场景,比如调用第三方接口超时、数据库临时连接失败之类的情况。以前我都是用 try...catch 加 while 循环写,代码又丑又难维护,后来发现了 Spring Retry,用起来是真方便,分享下我的使用经验。

先搞定依赖

用 Spring Retry 第一步肯定要引依赖,我这边是 Spring Boot 项目,除了核心的 spring-retry,还得加 aop 依赖,不然注解不好使。

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 Retry 有命令式和声明式两种用法,我平时更爱用声明式,注解写起来省事,但有时候复杂场景还得靠命令式。

命令式:灵活控制细节

命令式主要靠 RetryTemplate,能手动配置重试次数、间隔、触发条件这些。我认为这种方式适合需要动态调整重试规则的场景,比如根据不同业务参数改变重试策略。

简单示例安排上,最多重试 3 次,每次间隔 1 秒,只有抛 RemoteAccessException 才重试:

java 复制代码
RetryTemplate template = RetryTemplate.builder()
        .maxAttempts(3)
        .fixedBackoff(1000)
        .retryOn(RemoteAccessException.class)
        .build();

// 执行业务逻辑
template.execute(ctx -> {
    // 这里放可能失败的操作,比如调用接口
    System.out.println("执行业务逻辑,当前重试次数:" + ctx.getRetryCount());
    return "处理结果";
});

还有些实用的配置,比如指数退避,第一次间隔 100ms,之后每次翻倍,最大间隔 10 秒,适合怕频繁请求压垮服务的场景:

java 复制代码
RetryTemplate.builder()
      .maxAttempts(10)
      .exponentialBackoff(100, 2, 10000)
      .retryOn(IOException.class)
      .build();

如果想无限重试,只要间隔合理也可以,比如 1-3 秒随机间隔:

java 复制代码
RetryTemplate.builder()
      .infiniteRetry()
      .retryOn(IOException.class)
      .uniformRandomBackoff(1000, 3000)
      .build();

重试耗尽还没成功怎么办?可以加个兜底逻辑,用 RecoveryCallback:

java 复制代码
template.execute(new RetryCallback<Object, Throwable>() {
    @Override
    public Object doWithRetry(RetryContext context) throws Throwable {
        System.out.println("尝试执行,次数:" + context.getRetryCount());
        // 模拟失败
        throw new RemoteAccessException("调用失败");
    }
}, new RecoveryCallback<Object>() {
    @Override
    public Object recover(RetryContext context) throws Exception {
        System.out.println("重试都失败了,执行兜底方案");
        return "兜底返回结果";
    }
});

声明式:注解搞定一切

声明式用注解,代码更简洁,我平时写常规业务都用这个。首先得在启动类加 @EnableRetry 开启功能:

java 复制代码
@EnableRetry
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

然后在需要重试的方法上加 @Retryable,比如这个方法,抛 RuntimeException 时重试,最多 5 次,每次间隔 1 秒,之后每次乘以 1.5:

java 复制代码
@Service
public class MyService {
    @Retryable(value = RuntimeException.class, maxAttempts = 5, backoff = @Backoff(value = 1000L, multiplier = 1.5))
    public void sayHello() {
        System.out.println("执行 sayHello,时间:" + LocalDateTime.now());
        // 模拟失败场景
        throw new RuntimeException("测试重试");
    }
}

也能指定多个异常类型,还能加监听器:

java 复制代码
@Retryable(value = {IOException.class, RemoteAccessException.class},
        listeners = {"myRetryListener"},
        maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 500))
public void sayHi() {
    // 业务逻辑
}

重试失败后的兜底方法用 @Recover 注解,得和重试方法在同一个类里,返回类型要一致,还能拿到原始参数:

java 复制代码
@Recover
public void recoverRuntime(RuntimeException e) {
    System.out.println("RuntimeException 重试失败,兜底处理:" + e.getMessage());
}

// 带参数的兜底方法
@Retryable(value = RemoteAccessException.class)
public void service(String str1, String str2) {
    throw new RemoteAccessException("调用失败");
}

@Recover
public void recoverRemote(RemoteAccessException e, String str1, String str2) {
    System.out.println("RemoteAccessException 重试失败,参数:" + str1 + "," + str2);
}

重试策略和监听器

重试策略

在我看来,重试策略就是控制"什么时候重试""重试多少次"的规则。Spring Retry 有现成的实现,比如 SimpleRetryPolicy 控制最大重试次数:

java 复制代码
// 包括首次尝试,最多 5 次,所有 Exception 都重试
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);

还有 TimeoutRetryPolicy,控制超时时间,30 秒内可以一直重试:

java 复制代码
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);
template.setRetryPolicy(policy);

监听器

想监控重试过程的话,就用 RetryListener,能拿到重试开始、出错、结束的事件。我们的经验是,用监听器记录重试日志很方便,排查问题时能清楚看到每一次重试的情况:

java 复制代码
RetryListener myListener = new RetryListener() {
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        System.out.println("重试开始");
        return true;
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("重试出错,次数:" + context.getRetryCount() + ",异常:" + throwable.getMessage());
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("重试结束,是否成功:" + (throwable == null));
    }
};

// 注册监听器
template.registerListener(myListener);

无状态和有状态重试

这个点容易被忽略,我简单说下。无状态重试就是普通场景,重试上下文存在线程栈里,不用额外存储,大部分情况都能用。

有状态重试就特殊了,比如数据库事务场景,失败后需要回滚事务再重新开始,这时候上下文得存在堆里,避免丢失。Spring Retry 用 RetryContextCache 来存储,默认是内存 Map,不够用的话可以自己定制。

一些使用小技巧

  1. 不要滥用重试,像参数错误这种明确不会成功的异常,千万别加重试,不然只会浪费资源。
  2. 间隔时间要合理,远程调用可以设长点,本地操作可以短点,也能用上随机间隔,避免同时重试造成峰值。
  3. 兜底方法很重要,一定要处理好重试失败的情况,比如返回默认值、触发告警之类的。
  4. 用表达式配置更灵活,能从配置文件或者其他 Bean 里读取参数,不用写死代码:
java 复制代码
@Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
        backoff = @Backoff(delayExpression = "@runtimeConfigs.initial"))
public void service() {
    // 业务逻辑
}

总的来说,Spring Retry 把重试逻辑抽象得很好,不用我们自己写重复代码,不管是简单场景还是复杂场景都能覆盖。我用下来感觉是真的香,推荐大家试试!

相关推荐
ZoeGranger2 小时前
【Spring】使用注解开发
后端
哔哩哔哩技术2 小时前
2025年哔哩哔哩技术精选技术干货
前端·后端·架构
IT_陈寒2 小时前
Redis性能翻倍的5个关键策略:从慢查询到百万QPS的实战优化
前端·人工智能·后端
蓝眸少年CY2 小时前
测试Java性能
java·开发语言·python
何包蛋H2 小时前
数据结构深度解析:Java Map 家族完全指南
java·开发语言·数据结构
linsa_pursuer2 小时前
最长连续序列
java·数据结构·算法·leetcode
强子感冒了2 小时前
Java集合框架深度学习:从Iterable到ArrayList的完整继承体系
java·笔记·学习
drebander2 小时前
Cursor IDE 中 Java 项目无法跳转到方法定义问题解决方案
java·ide·cursor
踏浪无痕2 小时前
从救火到防火:我在金融企业构建可观测性体系的实战之路
后端·面试·架构