目录
一、核心概念:重试的适用场景
二、方案 1:OpenFeign 原生重试(基于 Retryer)
1. 原生 Retryer 接口定义
2. 内置重试实现:Default
3. 配置原生重试(两种方式)
方式 1:配置文件全局生效
方式 2:自定义 Retryer Bean(推荐,更灵活)
4. 原生重试的核心规则
5. 自定义 Retryer(完全控制重试逻辑)
三、方案 2:集成 Spring Retry(更灵活)
1. 核心依赖
2. 启用 Spring Retry
3. 配置重试策略(三种方式)
方式 1:注解方式(推荐,细粒度控制)
方式 2:全局配置重试模板
方式 3:编程式调用(手动控制重试)
4. Spring Retry 核心注解说明
四、方案 3:结合熔断组件(Sentinel/Hystrix)
1. 集成 Sentinel 示例
2. 核心规则
五、关键扩展:自定义重试触发条件
六、生产环境最佳实践
1. 重试配置建议
2. 核心注意事项
3. 禁用重试的场景
七、问题排查
1. 重试不生效
2. 重试次数超出预期
3. 重试导致重复数据
总结
OpenFeign 本身内置了重试机制(基于feign.Retryer接口),同时也可结合 Spring Retry 实现更灵活的重试策略(如按异常类型、返回值重试)。本文从原生重试、Spring Retry 集成、高级定制三个维度,全面讲解 Feign 请求重试的实现方式、配置细节和最佳实践。
一、核心概念:重试的适用场景
重试仅适用于幂等性请求(如 GET 查询、PUT 更新),严禁对非幂等请求(如 POST 创建)重试,否则可能导致重复创建数据、重复扣款等严重问题。
- ✅ 推荐重试:网络抖动、超时、5xx 服务端错误(如 500/502/503/504);
- ❌ 禁止重试:4xx 客户端错误(如 400 参数错误、401 未授权、403 禁止访问)、404 资源不存在。
二、方案 1:OpenFeign 原生重试(基于 Retryer)
Feign 内置了Retryer接口,通过实现该接口可自定义重试逻辑,是轻量、无依赖的重试方案。
1. 原生 Retryer 接口定义
java
运行
public interface Retryer extends Cloneable { // 执行重试:若允许重试则等待后返回,否则抛出RetryableException void continueOrPropagate(RetryableException e); // 克隆重试器(Feign 每次请求会创建新的重试器实例) Retryer clone(); // 内置默认实现:默认不重试(NEVER_RETRY) Retryer NEVER_RETRY = new Retryer() { @Override public void continueOrPropagate(RetryableException e) { throw e; // 直接抛出异常,不重试 } @Override public Retryer clone() { return this; } }; }2. 内置重试实现:Default
Feign 提供了Retryer.Default实现,支持配置最大重试次数、初始间隔、最大间隔:
java
运行
// 构造函数参数说明 public Default(long period, long maxPeriod, int maxAttempts) { this.period = period; // 初始重试间隔(毫秒) this.maxPeriod = maxPeriod; // 最大重试间隔(毫秒) this.maxAttempts = maxAttempts; // 最大重试次数(含首次请求) this.attempt = 1; }3. 配置原生重试(两种方式)
方式 1:配置文件全局生效
yaml
# application.yml feign: client: config: default: # 所有Feign客户端生效(替换为服务名可指定客户端) retryer: # 最大重试次数(含首次请求,如3=首次+2次重试) maxAttempts: 3 # 初始重试间隔(毫秒) period: 1000 # 最大重试间隔(毫秒) maxPeriod: 3000方式 2:自定义 Retryer Bean(推荐,更灵活)
java
运行
import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignRetryConfig { /** * 自定义原生重试器 * 说明:maxAttempts=3(首次请求+2次重试),初始间隔1秒,最大间隔3秒 */ @Bean public Retryer feignRetryer() { // Default重试器会采用指数退避策略:间隔时间 = min(period * 2^(attempt-1), maxPeriod) // 例如:第1次重试间隔1秒,第2次重试间隔2秒(1*2^1),达到maxPeriod后不再增加 return new Retryer.Default(1000, 3000, 3); } }4. 原生重试的核心规则
- Feign 原生重试仅对
RetryableException异常触发(如连接超时、读取超时、5xx 错误); - 4xx 错误默认不会触发重试(Feign 会抛出非重试型异常);
- 重试间隔采用指数退避策略,避免短时间内重复请求压垮服务;
- 若需自定义重试触发条件(如特定 4xx 错误也重试),需扩展
ErrorDecoder。
5. 自定义 Retryer(完全控制重试逻辑)
java
运行
import feign.RetryableException; import feign.Retryer; import org.springframework.stereotype.Component; @Component public class CustomRetryer implements Retryer { // 最大重试次数(含首次) private static final int MAX_ATTEMPTS = 3; // 重试间隔(固定1秒,不使用指数退避) private static final long PERIOD = 1000; // 当前重试次数 private int attempt = 1; @Override public void continueOrPropagate(RetryableException e) { // 超过最大次数则抛出异常,终止重试 if (attempt >= MAX_ATTEMPTS) { throw e; } // 记录重试日志 System.out.printf("Feign请求重试,第%d次,异常:%s%n", attempt, e.getMessage()); // 等待重试间隔 try { Thread.sleep(PERIOD); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } // 重试次数+1 attempt++; } @Override public Retryer clone() { // 每次请求创建新的重试器实例(重置attempt) return new CustomRetryer(); } }三、方案 2:集成 Spring Retry(更灵活)
原生重试仅支持基于异常的重试,而 Spring Retry 可实现:按异常类型、返回值、自定义条件重试,支持重试监听、退避策略等高级特性,是生产环境的首选方案。
1. 核心依赖
xml
<!-- Spring Retry 核心依赖 --> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.4</version> </dependency> <!-- 需引入AOP依赖(Spring Retry 基于AOP实现) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency>2. 启用 Spring Retry
在启动类添加@EnableRetry注解:
java
运行
import org.springframework.retry.annotation.EnableRetry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableRetry // 启用Spring Retry public class FeignDemoApplication { public static void main(String[] args) { SpringApplication.run(FeignDemoApplication.class, args); } }3. 配置重试策略(三种方式)
方式 1:注解方式(推荐,细粒度控制)
在 Feign 接口方法上添加@Retryable注解,指定重试规则:
java
运行
import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.retry.annotation.Recover; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user-service") public interface UserFeignClient { /** * 重试规则: * - 仅对指定异常(超时、5xx)重试 * - 最大重试次数2次(首次+2次=共3次) * - 重试间隔:初始1秒,指数退避(第2次重试2秒) */ @Retryable( // 触发重试的异常类型 include = { feign.RetryableException.class, // Feign原生重试异常 java.net.SocketTimeoutException.class, // 超时异常 org.springframework.web.client.HttpServerErrorException.class // 5xx服务端错误 }, // 排除不重试的异常 exclude = { org.springframework.web.client.HttpClientErrorException.class // 4xx客户端错误 }, // 最大重试次数(不含首次,即maxAttempts=2 → 首次+2次重试) maxAttempts = 2, // 退避策略:initialInterval初始间隔,multiplier指数倍数 backoff = @Backoff(initialInterval = 1000, multiplier = 2) ) @GetMapping("/user/{id}") UserDTO getUserById(@PathVariable("id") Long id); /** * 重试耗尽后的降级方法(必须与重试方法同参数、同返回值) */ @Recover default UserDTO recover(Exception e, Long id) { // 降级逻辑:返回默认值/空值/抛自定义异常 System.out.printf("获取用户失败,id:%d,异常:%s%n", id, e.getMessage()); return new UserDTO(); } }方式 2:全局配置重试模板
java
运行
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @Configuration public class RetryConfig { /** * 全局重试模板 */ @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); // 1. 重试策略:最大重试2次,仅对指定异常重试 SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(2); // 重试次数(不含首次) // 指定可重试的异常 retryPolicy.setRetryableExceptions( feign.RetryableException.class, java.net.SocketTimeoutException.class ); // 指定不可重试的异常 retryPolicy.setFatalExceptions( org.springframework.web.client.HttpClientErrorException.class ); retryTemplate.setRetryPolicy(retryPolicy); // 2. 退避策略:指数退避,初始1秒,最大3秒 ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1000); // 初始间隔 backOffPolicy.setMaxInterval(3000); // 最大间隔 backOffPolicy.setMultiplier(2); // 指数倍数 retryTemplate.setBackOffPolicy(backOffPolicy); return retryTemplate; } }方式 3:编程式调用(手动控制重试)
java
运行
import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserService { @Resource private UserFeignClient userFeignClient; @Resource private RetryTemplate retryTemplate; public UserDTO getUser(Long id) { // 编程式重试 return retryTemplate.execute( // 重试逻辑 context -> userFeignClient.getUserById(id), // 降级逻辑 context -> { System.out.printf("重试耗尽,id:%d%n", id); return new UserDTO(); } ); } }4. Spring Retry 核心注解说明
| 注解 | 核心参数 | 作用 |
|---|---|---|
@Retryable | include | 指定触发重试的异常类型(数组) |
exclude | 指定不触发重试的异常类型(数组) | |
maxAttempts | 最大重试次数(不含首次请求,默认 3) | |
backoff | 退避策略(@Backoff 注解) | |
@Backoff | initialInterval | 初始重试间隔(毫秒,默认 1000) |
multiplier | 指数退避倍数(默认 1,即固定间隔) | |
maxInterval | 最大重试间隔(毫秒,默认 0,无限制) | |
@Recover | 无 | 重试耗尽后的降级方法,参数需包含重试方法的参数 + 异常类型 |
四、方案 3:结合熔断组件(Sentinel/Hystrix)
生产环境中,重试通常与熔断配合使用(避免重试导致服务雪崩),流程为:重试 → 重试耗尽 → 熔断降级。
1. 集成 Sentinel 示例
java
运行
import com.alibaba.csp.sentinel.annotation.SentinelResource; import org.springframework.retry.annotation.Retryable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user-service") public interface UserFeignClient { @Retryable( include = {feign.RetryableException.class}, maxAttempts = 2, backoff = @Backoff(initialInterval = 1000) ) @SentinelResource( value = "getUserById", fallback = "fallbackGetUser" // 熔断降级方法 ) @GetMapping("/user/{id}") UserDTO getUserById(@PathVariable("id") Long id); // 熔断降级方法(与原方法参数一致) default UserDTO fallbackGetUser(Long id) { return new UserDTO(); } }2. 核心规则
- 重试次数不宜过多(建议 1-2 次),否则会增加服务响应时间,甚至导致雪崩;
- 熔断阈值需包含重试次数(如熔断规则:10 秒内 5 次失败则熔断),避免重试次数被计入失败次数,导致熔断提前触发。
五、关键扩展:自定义重试触发条件
Feign 原生仅对RetryableException重试,若需对特定 HTTP 状态码(如 408 请求超时、503 服务不可用)触发重试,需自定义ErrorDecoder:
java
运行
import feign.Response; import feign.RetryableException; import feign.codec.ErrorDecoder; import org.springframework.stereotype.Component; @Component public class CustomErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultErrorDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { Exception exception = defaultErrorDecoder.decode(methodKey, response); // 对特定状态码触发重试 int status = response.status(); if (status == 408 || status >= 500) { // 抛出RetryableException,触发重试 return new RetryableException( response.status(), exception.getMessage(), response.request().httpMethod(), (Throwable) exception, null, response.request() ); } // 其他状态码返回原异常,不重试 return exception; } }六、生产环境最佳实践
1. 重试配置建议
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 最大重试次数 | 1-2 次(含首次) | 过多重试会增加响应时间,建议仅重试 1-2 次 |
| 重试间隔 | 1-3 秒(指数退避) | 避免短时间重复请求,给服务恢复时间 |
| 重试触发条件 | 仅 5xx / 超时 / 网络异常 | 4xx 客户端错误不重试,避免无效重试 |
| 日志记录 | 开启重试日志 | 记录每次重试的原因、次数、间隔,便于问题排查 |
2. 核心注意事项
- ❌ 禁止对 POST 请求重试:POST 通常是非幂等的,重试可能导致重复创建数据;
- ✅ 幂等性保障:若必须对写请求重试,需通过幂等性设计(如唯一请求 ID、乐观锁)避免重复操作;
- ⚠️ 超时时间:Feign 超时时间需包含重试总耗时(如单次超时 3 秒,2 次重试 → 总超时 ≥ 9 秒);
- 📝 监控告警:对重试次数过多的接口告警,排查服务稳定性问题;
- 🎯 粒度控制:不同 Feign 客户端配置不同的重试策略(核心服务重试次数少,非核心服务可适当增加)。
3. 禁用重试的场景
- 非幂等请求(POST 创建、DELETE 删除);
- 实时性要求高的接口(如秒杀、实时查询);
- 下游服务明确禁止重试(如返回
Retry-After头); - 接口响应慢但成功率高(重试会增加整体响应时间)。
七、问题排查
1. 重试不生效
- 检查是否抛出
RetryableException(可通过自定义ErrorDecoder确保); - Spring Retry 需引入
spring-retry和spring-aspects依赖,且启动类添加@EnableRetry; - 检查
@Retryable注解的include是否包含目标异常类型。
2. 重试次数超出预期
- 区分「原生 Retryer」和「Spring Retry」:两者不要同时使用,避免重复重试;
- 检查
maxAttempts定义:原生 Retryer 是「含首次」,Spring Retry 是「不含首次」。
3. 重试导致重复数据
- 立即停止重试,检查请求是否为非幂等(POST);
- 为写请求添加幂等性控制(如请求 ID 去重)。
总结
OpenFeign 实现重试有两种核心方案:
- 原生 Retryer:轻量、无依赖,适合简单场景,仅支持基于
RetryableException的重试; - Spring Retry:功能丰富,支持按异常类型、返回值重试,支持退避策略、降级方法,是生产环境首选。
核心原则:仅对幂等请求重试,控制重试次数和间隔,结合熔断避免雪崩,做好日志和监控。