基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Feign 调用失败时的重试机制是如何触发的?

知识点图片

在 Spring Cloud 生态中,Feign(具体来说是 OpenFeign)的重试机制触发逻辑稍微有些复杂,因为它涉及到 OpenFeign 原生重试机制Spring Cloud 负载均衡器(Ribbon 或 Spring Cloud LoadBalancer)重试机制 的结合。

以下是 Feign 调用失败时,重试机制被触发的详细原理和条件:


一、 触发重试的核心条件(何时会重试?)

在底层,OpenFeign 的原生重试机制只认一种异常RetryableException
只有当 Feign 客户端抛出 RetryableException 时,底层的 Retryer(重试器)才会触发重试逻辑。

具体来说,以下几种情况会触发重试:

  1. 网络层面的 IOException(最常见)

    • 连接超时(Connection Timeout): 无法与目标服务建立 TCP 连接。
    • 读取超时(Read Timeout): 连接建立成功,但目标服务处理太慢,未能在规定时间内返回响应。
    • 连接拒绝(Connection Refused): 目标服务宕机或端口未监听。
    • 底层逻辑: Feign 在发送 HTTP 请求时,如果捕获到原生的 java.io.IOException,会自动将其包装为 RetryableException 并抛出,从而触发重试。
  2. 特定的 HTTP 状态码(默认仅支持带 Retry-After 的响应)

    • 默认情况下,Feign 遇到 HTTP 4xx 或 5xx 错误时,不会触发重试,而是直接抛出 FeignException
    • 唯一例外: 如果目标服务返回了 HTTP 状态码(如 503 Service Unavailable),并且 HTTP 响应头中包含了 Retry-After(告诉客户端多久后重试),Feign 默认的 ErrorDecoder 会将其解析并抛出 RetryableException,触发重试。
  3. 自定义 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 中配置:
    yaml
    feign:
      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 依赖。
  • 触发机制:
    1. 默认对网络异常重试。
    2. 默认对 GET 请求重试。
    3. 可以通过配置让特定的 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

java
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);
    }
}

四、 总结与最佳实践(重试的风险)

  1. 机制总结:
    • Feign 原生重试: 基于 RetryableException(主要是网络 IO 异常引发)。
    • Spring Cloud 环境: 默认关闭原生重试,依赖底层负载均衡器(Ribbon / LoadBalancer)结合 spring-retry 实现更智能的跨实例重试。
  2. 幂等性警告: 重试机制极易导致数据重复插入/重复扣款等生产事故!
    • 默认情况下,建议只对 GET(查询)请求开启重试。
    • 如果要对 POST/PUT/DELETE 开启重试(如配置 OkToRetryOnAllOperations: true),目标服务端必须实现严格的接口幂等性(例如基于全局唯一流水号进行去重)。
  3. 超时时间计算: 如果开启了重试,客户端的总体超时时间会成倍增加(比如重试 3 次,客户端可能需要等 3 倍的 ReadTimeout 时间才会报错),这可能会导致客户端线程池耗尽(雪崩)。因此使用重试时,务必配合熔断器(Resilience4j/Sentinel)使用。
00:00
00:00