重试,你真的用对了吗?聊聊 Java 里的几种重试姿势

重试,你真的用对了吗?聊聊 Java 里的几种重试姿势

日常开发中,网络抖动、依赖服务暂时不可用、并发冲突这类“瞬时故障”太常见了。遇到这些情况,最简单的处理就是——再试一次。但“再试一次”写起来容易,写好了却没那么简单:重试多少次?等多久?什么异常才值得重试?重试失败了怎么兜底?

我这几年经手过几个项目,从最早在循环里 sleep,到后来用 Guava Retrying、Spring Retry,再到最近试了一款叫 FastRetry 的轻量库,踩过不少坑,也攒了一些心得。今天就把这些代码和经验拿出来聊聊。

最朴素的方式:自己写循环 + 休眠

很多初学者(包括我当年)会这么干:

public class RetryUtils<R> {
    private int retryTimes = 3;
    private long retryTimesLong = 500L;
    
    public Optional<R> getResult() {
        for (int i = 1; i <= retryTimes; i++) {
            try {
                return Optional.of(supplier.get());
            } catch (Exception e) {
                if (i == retryTimes) throw new RuntimeException(e);
                Thread.sleep(retryTimesLong);
            }
        }
        return Optional.empty();
    }
}

这段代码简单直接,一个 Builder 风格链式调用,用起来还算舒服。但它有一个致命缺陷:对任何异常都无脑重试。如果是 NullPointerExceptionIllegalArgumentException 这种逻辑错误,重试一万次也没用。另外,固定间隔 500ms 重试,在高并发下可能放大故障。

这种写法适合什么场景?内部测试、对可靠性要求不高的快速脚本。线上核心链路千万别这么用。

Guava Retrying:灵活、成熟,但有点重

Google 的 Guava 库附带了一个 guava-retrying 模块(实际是第三方,但常和 Guava 一起出现)。它的 API 设计我很喜欢:用 RetryerBuilder 组装策略,代码可读性很高。

Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
    .retryIfExceptionOfType(AccessException.class)   // 只重试特定异常
    .withWaitStrategy(WaitStrategies.incrementingWait(2, TimeUnit.SECONDS, 2, TimeUnit.SECONDS))
    .withStopStrategy(StopStrategies.stopAfterAttempt(6))
    .build();

这里我配置了:遇到 AccessException 才重试,每次重试间隔递增(第一次等2秒,第二次等4秒…),最多重试6次。

Guava Retrying 支持多种退避策略(固定、随机、指数、斐波那契),也支持自定义重试条件(比如判断返回结果是否为空)。它是目前功能最完备的轻量级重试库,适合大部分中大型项目。

但缺点也明显:依赖 guava-retrying 包(约 200KB),而且设计上偏同步阻塞。如果想异步重试,你得自己包一层 CompletableFuture。

Spring Retry:Spring 生态的亲儿子

如果你在用 Spring Boot,Spring Retry 可能是最顺手的方案。它通过注解和模板两种方式工作,AOP 风格对业务代码侵入小。

@Retryable(value = AccessException.class, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2))
public Integer getCount() throws AccessException {
    // 业务逻辑
}

或者用 RetryTemplate 编程式:

RetryTemplate template = RetryTemplate.builder()
    .retryOn(AccessException.class)
    .fixedBackoff(1000)
    .maxAttempts(3)
    .build();

template.execute(context -> riskyMethod(), context -> fallbackValue);

Spring Retry 最大的优点是跟 Spring 生态无缝集成,支持 @Recover 注解做降级。但它的设计有些臃肿,而且默认的 RetryTemplate 是线程不安全的(虽然官方建议每次使用新建实例或单例模式包装)。另外,它的重试策略和退避策略实现比较死板,不如 Guava 灵活。

FastRetry:小众但有点意思

代码里还看到一个 FastRetryTest,用的是 com.burukeyou.retry 这个库(老实说之前我没见过)。简单看了一下,它主打异步非阻塞,RetryQueue 提交任务返回 CompletableFuture,内部用线程池调度重试。

RetryTask<String> task = new RetryTask<String>() {
    int result = 0;
    @Override
    public long waitRetryTime() { return 2000; }
    @Override
    public boolean retry() { return ++result < 5; }
    @Override
    public String getResult() { return result + ""; }
};
CompletableFuture<String> future = queue.submit(task);

这个库的设计很适合异步高并发场景,比如调用多个外部服务,各自独立重试。但文档太少,社区几乎没人用,生产环境我不敢用。

小结:怎么选?

  • 简单工具类、脚本 → 自己写循环 sleep 就够了。
  • 中大型项目,对重试策略要求高 → Guava Retrying 首选。
  • Spring Boot 项目,已经用了 Spring Retry → 继续用它,注意线程安全。
  • 高并发异步场景 → 自己基于 CompletableFuture 和线程池封装,或者考虑 Resilience4j(比 FastRetry 靠谱)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值