Feign 调用失败时的重试机制是如何触发的?
在 Spring Cloud 生态中,Feign(具体来说是 OpenFeign)的重试机制触发逻辑稍微有些复杂,因为它涉及到 OpenFeign 原生重试机制 和 Spring Cloud 负载均衡器(Ribbon 或 Spring Cloud LoadBalancer)重试机制 的结合。
以下是 Feign 调用失败时,重试机制被触发的详细原理和条件:
一、 触发重试的核心条件(何时会重试?)
在底层,OpenFeign 的原生重试机制只认一种异常:RetryableException。
只有当 Feign 客户端抛出 RetryableException 时,底层的 Retryer(重试器)才会触发重试逻辑。
具体来说,以下几种情况会触发重试:
网络层面的 IOException(最常见)
- 连接超时(Connection Timeout): 无法与目标服务建立 TCP 连接。
- 读取超时(Read Timeout): 连接建立成功,但目标服务处理太慢,未能在规定时间内返回响应。
- 连接拒绝(Connection Refused): 目标服务宕机或端口未监听。
- 底层逻辑: Feign 在发送 HTTP 请求时,如果捕获到原生的
java.io.IOException,会自动将其包装为RetryableException并抛出,从而触发重试。
特定的 HTTP 状态码(默认仅支持带
Retry-After的响应)- 默认情况下,Feign 遇到 HTTP 4xx 或 5xx 错误时,不会触发重试,而是直接抛出
FeignException。 - 唯一例外: 如果目标服务返回了 HTTP 状态码(如 503 Service Unavailable),并且 HTTP 响应头中包含了
Retry-After(告诉客户端多久后重试),Feign 默认的ErrorDecoder会将其解析并抛出RetryableException,触发重试。
- 默认情况下,Feign 遇到 HTTP 4xx 或 5xx 错误时,不会触发重试,而是直接抛出
自定义 ErrorDecoder 触发的重试
- 开发者可以通过自定义
ErrorDecoder,将特定的 HTTP 5xx 状态码(如 500 内部错误、502 网关错误、504 网关超时)手动转换为RetryableException,从而主动触发重试。
- 开发者可以通过自定义
二、 Spring Cloud 中的重试机制(容易踩坑的重点)
如果你在 Spring Cloud 环境下使用 @FeignClient,你需要知道:Spring Cloud 默认关闭了 OpenFeign 的原生重试机制。
Spring Cloud 认为,带有负载均衡的重试(在多个实例间切换重试)比在同一个实例上死磕更合理。因此:
1. OpenFeign 原生重试(默认被关闭)
Spring Cloud 将 Feign 的默认重试器设置为了 Retryer.NEVER_RETRY。
- 如果要开启原生重试: 需要在配置类中注入
Retryer.Default,或者在application.yml中配置:yamlfeign: client: config: default: # 或者具体的服务名 retryer: feign.Retryer.Default
2. 结合负载均衡器的重试(推荐方式)
实际开发中,我们通常使用底层负载均衡器(旧版用 Ribbon,新版用 Spring Cloud LoadBalancer)来触发重试。
A. 旧版:配合 Ribbon 重试(Spring Cloud Hoxton 及以前)
- 触发条件: 发生网络异常(ConnectException、SocketTimeoutException等),或者配置了对特定请求方法和状态码的重试。
- 前提: 项目中必须引入
spring-retry依赖。 - 配置示例:yaml
<服务名>: ribbon: MaxAutoRetries: 1 # 同一实例最大重试次数 MaxAutoRetriesNextServer: 1 # 切换实例的最大重试次数 OkToRetryOnAllOperations: false # 是否对所有请求(包括POST)都重试
B. 新版:配合 Spring Cloud LoadBalancer (SCLB)(Spring Cloud 2020.0 及以后)
- Ribbon 被废弃后,Spring 官方推出了 SCLB。
- 前提: 同样需要引入
spring-retry依赖。 - 触发机制:
- 默认对网络异常重试。
- 默认对
GET请求重试。 - 可以通过配置让特定的 HTTP 状态码触发重试。
- 配置示例:yaml
spring: cloud: loadbalancer: retry: enabled: true retryable-status-codes: 503, 502, 504 # 遇到这些HTTP状态码时重试 max-retries-on-same-service-instance: 1 max-retries-on-next-service-instance: 1
三、 自定义触发重试(实战示例)
如果你想让 Feign 遇到 HTTP 500 时也重试,通常的做法是自定义 ErrorDecoder:
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new ErrorDecoder() {
private final ErrorDecoder defaultDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
// 如果是 5xx 错误,抛出 RetryableException 触发重试
if (response.status() >= 500 && response.status() <= 599) {
return new RetryableException(
response.status(),
"Server error " + response.status(),
response.request().httpMethod(),
null, // 重试时间(Date)
response.request()
);
}
return defaultDecoder.decode(methodKey, response);
}
};
}
// 别忘了开启 Feign 原生重试器(如果没用 Ribbon/SCLB)
@Bean
public feign.Retryer retryer() {
// 初始间隔 100ms,最大间隔 1s,最大尝试次数 3
return new feign.Retryer.Default(100, 1000, 3);
}
}
四、 总结与最佳实践(重试的风险)
- 机制总结:
- Feign 原生重试: 基于
RetryableException(主要是网络 IO 异常引发)。 - Spring Cloud 环境: 默认关闭原生重试,依赖底层负载均衡器(Ribbon / LoadBalancer)结合
spring-retry实现更智能的跨实例重试。
- Feign 原生重试: 基于
- 幂等性警告: 重试机制极易导致数据重复插入/重复扣款等生产事故!
- 默认情况下,建议只对
GET(查询)请求开启重试。 - 如果要对
POST/PUT/DELETE开启重试(如配置OkToRetryOnAllOperations: true),目标服务端必须实现严格的接口幂等性(例如基于全局唯一流水号进行去重)。
- 默认情况下,建议只对
- 超时时间计算: 如果开启了重试,客户端的总体超时时间会成倍增加(比如重试 3 次,客户端可能需要等 3 倍的 ReadTimeout 时间才会报错),这可能会导致客户端线程池耗尽(雪崩)。因此使用重试时,务必配合熔断器(Resilience4j/Sentinel)使用。