Spring Retry注解不生效主因是代理失效,如自调用、非容器托管Bean、异常类型未匹配或缺失@EnableRetry;手动重试需参数化策略,用指数退避而非固定sleep,并设3~5次上限。

Spring Retry注解不生效的常见原因
加了 @EnableRetry 和 @Retryable 却没重试,大概率是代理问题。Spring AOP 默认只对被 Spring 容器管理的 Bean、且通过接口代理调用时才生效。
- 类内部方法自调用(比如
methodA()直接调methodB()),即使methodB标了@Retryable,也不会触发重试——因为绕过了代理 - 目标类没被 Spring 托管(比如 new 出来的实例),注解完全无效
- 异常类型没匹配上:默认只对
RuntimeException重试,若抛的是IOException这类受检异常,得显式配置include = { IOException.class } - 没加
@EnableRetry到配置类或启动类上(Spring Boot 2.4+ 后需手动加,不再自动启用)
手动实现重试时怎么控制最大尝试次数和间隔
手写重试容易陷入“while + sleep”硬编码陷阱,既难测又难调。关键不是循环本身,而是把重试策略参数化、可配置。
- 别用
Thread.sleep(1000)写死间隔:网络抖动时固定等待可能放大失败率;建议用指数退避,比如第 n 次重试前等(long) Math.pow(2, n-1) * 1000毫秒 - 最大重试次数必须设上限,否则可能卡死线程或耗尽连接池;推荐设为 3~5 次,再往上失败概率已趋近于 1
- 捕获异常后要区分“可重试”和“不可重试”:比如
IllegalArgumentException是参数错,重试无意义;而SocketTimeoutException才值得重试 - 示例片段:
for (int i = 0; i < maxRetries; i++) {<br> try {<br> return doSomething();<br> } catch (SocketTimeoutException | IOException e) {<br> if (i == maxRetries - 1) throw e;<br> Thread.sleep((long) Math.pow(2, i) * 1000);<br> }<br>}
Spring Retry 的 recover 方法什么时候被调用
@Recover 不是兜底日志打印,它是重试彻底失败后的最终补偿动作,必须严格匹配签名才能触发。
- 方法名任意,但必须标
@Recover,且参数列表第一个是异常类型(如Exception),后续可选加原始方法的参数(顺序和类型都要一致) - 如果原始方法返回
String,@Recover方法也必须返回String,否则 Spring 找不到匹配的恢复方法,直接抛出NoSuchMethodException - 它不会捕获原始方法里已处理过的异常——只响应那些被
@Retryable明确声明为“可重试”、且最终仍失败的异常 - 典型误用:在
@Recover里又去调一次远程接口,结果又超时……这等于把失败转移了,不是恢复
重试逻辑要不要放在 Service 层还是 FeignClient 层
优先交给 FeignClient 或 HTTP 客户端层处理,而不是堆在业务 Service 里。
立即学习“Java免费学习笔记(深入)”;
- Feign 自带
@Retryable(配合 Spring Retry)或原生Retryer接口,能更早拦截网络层异常(如连接拒绝、超时),避免业务逻辑反复构造请求体 - Service 层重试容易污染职责:比如一次下单操作包含库存扣减 + 订单创建 + 消息发送,其中只有消息发送失败才该重试,混在一起会导致库存被重复扣
- 若用 WebClient 或 OkHttp,直接配置
RetryPolicy或拦截器更轻量,比在 Service 加 AOP 更可控 - 例外情况:跨系统协作类操作(如调 A 系统成功后调 B 系统失败),此时需要 Saga 或本地事务+补偿,不能靠简单重试
重试不是万能补丁,真正难的是判断“这次失败到底值不值得再试一次”。网络超时、限流拒绝可以试,参数校验失败、数据库唯一键冲突、下游明确返回 400 错误码,再试只是让问题更隐蔽。










