失败了就该无脑重试吗?——Spring Retry 这套机制到底是在“救你”还是在“坑你”?
🏆本文收录于《滚雪球学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(别把下游当沙包)
重试最怕两件事:
- 你重试太快,下游还没喘口气,你又来一拳
- 你重试太齐,所有客户端同一时间一起重试,直接“惊群”
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. 最后聊聊:重试不是“提高成功率”的唯一手段,它是“组合拳”里的一个动作
我比较喜欢把“调用可靠性”看成一套组合拳:
- 重试:解决瞬态错误(Spring Retry)
- 退避 + 抖动:防止惊群
- 超时:没有超时的重试,会把线程和连接卡死
- 熔断/限流:下游真不行了就别硬打
- 幂等:否则重试可能制造重复副作用
如果你只做第 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 -
- 点赞
- 收藏
- 关注作者
评论(0)