失败了就该无脑重试吗?——Spring Retry 这套机制到底是在“救你”还是在“坑你”?

举报
bug菌 发表于 2026/01/13 16:38:26 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

0. 先把“重试的边界”钉死:哪些失败值得重试,哪些失败重试只会更惨

Spring Retry 的官方 README 里提到一个关键判断:适合重试的错误往往是“transient(瞬态)”的,也就是“过会儿可能自己就好了”。
所以你大纲里写的“网络抖动、临时故障”,确实是典型场景。

✅ 适合重试的(常见)

  • 网络抖动、DNS 短暂异常、TCP 连接超时(瞬态)
  • 下游返回 503/504(服务临时不可用/网关超时)
  • 数据库连接池瞬时耗尽(短时间恢复的那种)
  • 限流导致的临时失败(但要配合退避和抖动)

❌ 不适合重试的(重试=作死)

  • 参数错误(400)、鉴权失败(401/403)——你再试一百次也不会对
  • 业务不满足(余额不足、库存不足)——重试只会把日志刷爆
  • 非幂等操作且没有幂等保护(比如“创建订单”没幂等 key)——重试可能直接下出两单🙃

经验话:重试之前先问一句“这事儿重做一次会不会产生副作用?” 如果答案是“可能会”,那你要么做幂等,要么别重试。

1. @Retryable 注解:适用场景与最小可用写法(网络抖动、临时故障)

Spring Retry 的声明式重试核心就是 @Retryable,但它不是“贴上就灵”,它背后依赖 AOP 代理。
官方 @EnableRetry 文档说得很直接:加了 @EnableRetry 之后,@Retryable 的 bean 方法会被代理并按注解元数据执行重试

1.1 先启用:@EnableRetry

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry
public class RetryDemoApp {
  public static void main(String[] args) {
    SpringApplication.run(RetryDemoApp.class, args);
  }
}

1.2 一个典型“网络抖动”场景:调用第三方接口

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.net.SocketTimeoutException;

@Service
public class PartnerApiClient {

  @Retryable(
      retryFor = {SocketTimeoutException.class},
      maxAttempts = 3,
      backoff = @Backoff(delay = 200) // 先给个最小退避,别急着连环call
  )
  public String fetchProfile(String userId) throws SocketTimeoutException {
    // 假装这里是 HTTP 调用:可能抖一下超时
    if (Math.random() < 0.7) {
      throw new SocketTimeoutException("partner api timeout");
    }
    return "ok:" + userId;
  }
}

这里的意图很“干净”:

  • 只对“可能一会儿就好”的异常重试(超时)
  • maxAttempts 包含首次调用(总共最多 3 次)
  • 退避不是为了“等着变好”那么简单,更重要是避免惊群(你不退避,所有实例一起重试,下游更崩)

小提醒(很重要):@Retryable 依赖代理,同一个类内部自调用可能绕开代理导致不重试。这是 AOP 常见坑,别等线上才想起来。

2. 自定义重试策略:指数退避 Backoff(别把下游当沙包)

重试最怕两件事:

  1. 你重试太快,下游还没喘口气,你又来一拳
  2. 你重试太齐,所有客户端同一时间一起重试,直接“惊群”

2.1 注解方式:@Backoff + multiplier 做指数退避

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class InventoryClient {

  @Retryable(
      retryFor = {TransientInventoryException.class},
      maxAttempts = 5,
      backoff = @Backoff(
          delay = 200,        // 初始等待 200ms
          multiplier = 2.0,   // 指数增长:200, 400, 800, 1600...
          maxDelay = 3000     // 别无限长,最多等 3s
      )
  )
  public String reserve(String sku, int count) {
    if (Math.random() < 0.8) {
      throw new TransientInventoryException("inventory temporary failure");
    }
    return "reserved";
  }
}

class TransientInventoryException extends RuntimeException {
  public TransientInventoryException(String msg) { super(msg); }
}

2.2 为什么指数退避是“默认更安全”的选择?

Spring Retry 提供了 ExponentialBackOffPolicy:它会在同一轮重试中按指数函数递增等待时间,而且明确说明它是线程安全的实现。
翻译成工程语言就是:越失败越别急,给下游恢复窗口,也给自己降风险。

2.3 进一步:加“随机抖动”避免惊群(思路)

虽然你的大纲没强制要写 jitter,但生产里它非常关键:

  • 多实例同样的退避参数,会导致它们在相同时间点一起重试
  • 解决办法:在退避时间上加一点随机抖动(例如 0.8~1.2 倍)

如果你们的调用量大、实例多,我会强烈建议把“抖动”作为默认策略之一(尤其是对同一个下游)。

3. @Recover:重试耗尽后的兜底逻辑(别把失败直接扔给用户)

@Recover 是 Spring Retry 给你的“安全气囊”:
当重试次数用光还没成功,就走恢复方法。Spring Retry README 里把它作为声明式重试的重要组成部分之一。

3.1 一个“优雅兜底”的例子:下游挂了就走缓存/降级

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class PriceService {

  @Retryable(
      retryFor = {PartnerDownException.class},
      maxAttempts = 4,
      backoff = @Backoff(delay = 300, multiplier = 2.0, maxDelay = 2000)
  )
  public int queryPrice(String sku) {
    if (Math.random() < 0.9) {
      throw new PartnerDownException("partner service unavailable");
    }
    return 199;
  }

  @Recover
  public int recover(PartnerDownException ex, String sku) {
    // 兜底策略:返回缓存价/默认价/或者触发异步修复
    // 关键:这里别再去调用同一个不稳定下游,否则你是“复读机式失败”
    return 999; // demo:默认价
  }
}

class PartnerDownException extends RuntimeException {
  public PartnerDownException(String msg) { super(msg); }
}

3.2 @Recover 写得“像人一样”要注意什么?

  • 恢复方法的入参通常包含:异常 + 原方法参数(让你能定位/补偿)
  • 恢复逻辑尽量做到可观测:至少日志、最好打指标
  • 恢复逻辑不要做“高风险动作”:比如再次同步调用不稳定下游
    不然你只是把失败换了个地方继续失败🙂

4. 命令式重试:RetryTemplate(当你需要“更强控制力”时)

注解很香,但它也有局限:

  • 有些重试规则依赖运行时条件(比如根据响应码、业务字段决定是否重试)
  • 有些地方你不想引入 AOP 代理(例如非 Spring Bean、或需要更明确的控制流程)

这时候就该 RetryTemplate 出场了。Spring Retry 官方 README 专门用一节介绍 RetryTemplate 的使用价值:让处理更健壮,对瞬态错误自动重试。

4.1 最常见写法:execute 包住你的调用

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.util.Map;

@Configuration
public class RetryTemplateConfig {

  @Bean
  public RetryTemplate retryTemplate() {
    RetryTemplate template = new RetryTemplate();

    // 1) 重试次数策略:最多 4 次(含首次)
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(
        4,
        Map.of(TransientInventoryException.class, true),
        true
    );
    template.setRetryPolicy(retryPolicy);

    // 2) 指数退避策略
    ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
    backOff.setInitialInterval(200);
    backOff.setMultiplier(2.0);
    backOff.setMaxInterval(3000);
    template.setBackOffPolicy(backOff); // ExponentialBackOffPolicy 来自官方 BackOffPolicy 体系 :contentReference[oaicite:6]{index=6}

    return template;
  }
}

4.2 使用:失败就重试,耗尽就走 recovery callback

import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;

@Service
public class InventoryFacade {

  private final RetryTemplate retryTemplate;

  public InventoryFacade(RetryTemplate retryTemplate) {
    this.retryTemplate = retryTemplate;
  }

  public String reserveWithTemplate(String sku, int count) {
    return retryTemplate.execute(
        (context) -> doReserve(sku, count, context),
        (context) -> fallbackReserve(sku, count, context)
    );
  }

  private String doReserve(String sku, int count, org.springframework.retry.RetryContext context) {
    // 你甚至可以拿到当前第几次重试
    int attempt = context.getRetryCount() + 1;

    if (Math.random() < 0.85) {
      throw new TransientInventoryException("attempt=" + attempt + " failed");
    }
    return "reserved";
  }

  private String fallbackReserve(String sku, int count, org.springframework.retry.RetryContext context) {
    // recovery:重试耗尽
    return "reserve_pending"; // 比如写入补偿队列/返回可接受的降级结果
  }
}

RetryTemplate 的优势是:

  • 你能在一次调用里把“执行”和“兜底”写得特别明确
  • 你可以在回调里读取 RetryContext,做更细腻的控制
  • 不依赖注解代理,更“可控”、也更利于写单测

5. 最后聊聊:重试不是“提高成功率”的唯一手段,它是“组合拳”里的一个动作

我比较喜欢把“调用可靠性”看成一套组合拳:

  1. 重试:解决瞬态错误(Spring Retry)
  2. 退避 + 抖动:防止惊群
  3. 超时:没有超时的重试,会把线程和连接卡死
  4. 熔断/限流:下游真不行了就别硬打
  5. 幂等:否则重试可能制造重复副作用

如果你只做第 1 条,其他都不管,那你会很容易体验到一种“邪门的稳定性”——日志特别稳定地爆炸😇。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。