Feign 配置了重试后,不需要再单独配置 LoadBalancer 的自动重试,甚至不建议两者同时启用 —— 否则会导致「重试叠加」(比如 Feign 重试 2 次 + LoadBalancer 重试 1 次,最终可能发起 3 次甚至更多次请求),不仅浪费资源,还可能引发重复业务操作(如重复扣库存)。
核心结论:Feign 重试和 LoadBalancer 重试是「同层级的重试机制」,二选一即可,优先使用 Feign 重试(更灵活、更贴合服务调用场景)。
一、先理清两者的本质区别(为什么不能叠加)
Feign 和 LoadBalancer 的重试机制,解决的是「不同层面的问题」,但最终都会触发「重新选择实例并发起请求」,叠加后会产生冲突:
| 维度 | Feign 重试(feign-retry) | LoadBalancer 重试(spring-cloud-loadbalancer-retry) |
|---|---|---|
| 设计目标 | 针对「Feign 接口调用」的重试(如接口超时、5xx 错误) | 针对「实例选择失败」的重试(如选中的实例宕机、网络不可达) |
| 重试触发条件 | 可配置(默认:仅重试 IO 异常、连接超时、5xx 错误,不重试 4xx 业务错误) | 固定触发条件(实例连接失败、读取超时、实例不健康) |
| 实例选择逻辑 | 每次重试会通过 LoadBalancer 重新选择实例(天然支持负载均衡) | 重试时会切换到下一个健康实例(依赖 LoadBalancer 实例列表) |
| 配置灵活性 | 支持按接口、按服务自定义重试次数、间隔、异常类型 | 全局配置,仅支持简单的重试次数配置(无细粒度控制) |
| 业务影响 | 可配合 @Retryable 注解跳过幂等性接口重试(如支付接口) | 无业务层面控制,仅机械重试 |
简单说:Feign 重试是「业务级重试」(知道自己在调用哪个接口、遇到了什么错误),LoadBalancer 重试是「网络级重试」(只知道实例连不上,不知道业务逻辑)。两者同时启用时,会出现「Feign 发起 1 次重试,LoadBalancer 对每次重试再发起 1 次重试」的叠加效果,比如:
- 正常请求:1 次(失败)→ Feign 重试 1 次 → LoadBalancer 对该重试再重试 1 次 → 总共 3 次请求,远超预期。
二、推荐方案:仅使用 Feign 重试(放弃 LoadBalancer 重试)
Feign 重试本身已经集成了「负载均衡 + 实例切换」的能力(每次重试都会通过 LoadBalancer 重新选择实例),完全能覆盖 LoadBalancer 重试的场景,且更灵活。
1. 配置步骤(仅需 2 步)
(1)添加 Feign 重试依赖(已添加可忽略)
<!-- Feign 重试核心依赖(Spring Cloud 2020+ 版本需手动引入) -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
(2)配置 Feign 重试规则(application.yml)
feign:
retry:
enabled: true # 启用 Feign 重试(默认 false)
max-attempts: 3 # 最大尝试次数(包括首次调用,默认 5)→ 实际重试次数 = max-attempts - 1 = 2 次
period: 1000 # 重试间隔(每次重试之间等待 1 秒,默认 1000ms)
max-period: 3000 # 最大重试间隔(避免间隔过长,默认 1000ms)
retryable-exceptions: # 仅对指定异常重试(可选,默认已包含常见异常)
- java.net.ConnectException # 连接失败
- java.net.SocketTimeoutException # 读取超时
- org.springframework.web.client.HttpServerErrorException # 5xx 服务器错误
# 关键:禁用 LoadBalancer 重试(避免叠加)
spring:
cloud:
loadbalancer:
retry:
enabled: false # 默认 false,明确禁用更稳妥
2. Feign 重试的核心逻辑(为什么足够用)
当 Feign 调用失败时,会按以下逻辑重试:
- 首次调用:通过 LoadBalancer 选择实例 A → 失败(如实例 A 宕机);
- 第 1 次重试:Feign 触发重试,通过 LoadBalancer 重新选择实例 B(健康实例)→ 若成功则返回结果;
- 第 2 次重试:若实例 B 仍失败,LoadBalancer 选择实例 C → 若成功则返回结果;
- 若所有重试都失败,抛出最终异常(如
FeignException)。
可见:Feign 重试已经包含了「实例切换」的逻辑(依赖 LoadBalancer 重新选择实例),完全不需要 LoadBalancer 再额外重试。
三、特殊场景:什么时候需要 LoadBalancer 重试?
只有一种极端场景可能需要启用 LoadBalancer 重试:不使用 Feign,仅使用 RestTemplate + LoadBalancer 调用服务(无 Feign 重试机制时)。
示例配置(仅 RestTemplate 场景):
# 启用 LoadBalancer 重试(仅 RestTemplate 场景)
spring:
cloud:
loadbalancer:
retry:
enabled: true
max-retries-on-next-service: 1 # 切换到下一个实例的重试次数(默认 1)
max-retries-on-same-service-instance: 0 # 同一实例不重试
# RestTemplate 配置(需 @LoadBalanced 注解)
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
但这种场景在微服务中极少使用(Feign 是服务间调用的主流方式),所以大多数情况下,LoadBalancer 重试都是多余的。
四、避坑指南:重试的关键注意事项
无论使用哪种重试,都必须避免「重复业务操作」(这是重试最容易踩的坑):
-
仅对「幂等接口」重试:
- 幂等接口:多次调用结果一致(如查询接口、删除接口、幂等的更新接口);
- 非幂等接口:禁止重试(如创建订单、扣减库存、转账接口,多次调用会导致重复创建 / 扣减)。
-
Feign 如何跳过非幂等接口重试?用
@Retryable注解指定需要重试的接口,非幂等接口不添加该注解:import org.springframework.retry.annotation.Retryable; import feign.RetryableException; @FeignClient(name = "stock-server") public interface StockFeignClient { // 非幂等接口(扣减库存):禁止重试 @GetMapping("/stock/deduct/{productId}") String deductStock(@PathVariable("productId") Long productId); // 幂等接口(查询库存):允许重试 @Retryable( value = {RetryableException.class, SocketTimeoutException.class}, // 触发重试的异常 maxAttempts = 3, // 最大尝试次数 backoff = @Backoff(delay = 1000) // 重试间隔 1 秒 ) @GetMapping("/stock/query/{productId}") Integer queryStock(@PathVariable("productId") Long productId); } -
设置合理的重试次数和间隔:
- 重试次数:建议
max-attempts=3(1 次首次 + 2 次重试),过多重试会增加系统压力; - 重试间隔:建议
delay=1000ms(1 秒),避免短时间内频繁重试导致服务雪崩。
- 重试次数:建议
五、总结
- Feign 配置了重试后,不需要再配置 LoadBalancer 重试,两者叠加会导致重试次数超标、业务重复执行;
- 优先使用 Feign 重试:更灵活、支持细粒度控制(按接口、按异常类型),且天然集成 LoadBalancer 的实例切换能力;
- 仅 RestTemplate + LoadBalancer 场景,才需要启用 LoadBalancer 重试;
- 重试的核心前提:仅对幂等接口重试,非幂等接口禁止重试,避免业务风险。
一句话记住:Feign 重试是「业务级重试」,LoadBalancer 重试是「网络级重试」,业务级优先,且无需叠加。
828

被折叠的 条评论
为什么被折叠?



