Spring Cloud LoadBalancer 和 Feign的重试机制

Spring Cloud LoadBalancer 是 Spring 官方推出的一款轻量级客户端负载均衡器,主要用来替代已经停更的 Netflix Ribbon。

从 Spring Cloud 2020.0 版本开始,Ribbon 已经被正式移除,Spring Cloud LoadBalancer 成为了默认的负载均衡组件。

在 Spring Cloud 中使用 LoadBalancer 非常简单,通常只需要引入依赖 并配合服务发现(如 Nacos、Eureka、Consul)一起使用。

以下是具体的使用指南:

1. 引入依赖

首先,在你的 pom.xml 中引入 Spring Cloud LoadBalancer 的依赖。

注意:如果你引入了 Spring Cloud Alibaba Nacos 或者 Eureka 的服务发现依赖,部分新版本已经默认包含了 LoadBalancer,但显式引入最为稳妥。

XML 复制代码
<!-- 引入 Spring Cloud LoadBalancer -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- 还需要搭配一个注册中心,例如 Nacos -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2. 三种常见的调用方式

Spring Cloud LoadBalancer 可以在底层无缝对接你常用的 HTTP 客户端进行远程服务调用。

方式一:配合 RestTemplate 使用(最常见)

只需要在注入 RestTemplate 的 Bean 上面加一个 @LoadBalanced 注解即可开启负载均衡。

java 复制代码
@Configuration
public class RestConfig {

    @Bean
    @LoadBalanced // 这个注解让 RestTemplate 具备了通过服务名查找 IP 列表并负载均衡的能力
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

发起调用 :在调用时,直接使用服务名(而不是硬编码的 IP 和端口)。

java 复制代码
@RestController
public class OrderController {
    
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/order")
    public String createOrder() {
        // "stock-service" 是目标服务在注册中心的名字
        String url = "http://stock-service/stock/deduct";
        return restTemplate.getForObject(url, String.class);
    }
}

方式二:配合 OpenFeign 使用(最推荐)

在现代微服务开发中,通常推荐使用声明式的 OpenFeign

在新版 Spring Cloud 中,OpenFeign 默认已经整合了 Spring Cloud LoadBalancer,你几乎不需要做额外配置。

java 复制代码
// 声明 Feign 客户端,指定服务名
@FeignClient(name = "stock-service")
public interface StockFeignClient {
    
    @GetMapping("/stock/deduct")
    String deduct();
}

调用时直接像调本地方法一样使用即可,底层自动实现负载均衡。

方式三:配合 WebClient 使用(响应式编程)

如果你的项目基于 WebFlux,可以使用 WebClient

java 复制代码
@Configuration
public class WebClientConfig {

    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
}

3. 自定义负载均衡策略

Spring Cloud LoadBalancer 默认采用的是轮询策略(RoundRobin)

如果你想把它修改为随机策略(Random)或自定义算法,可以按照以下步骤配置:

第一步:创建一个配置类

避坑指南 :这个配置类上千万不要@Configuration 注解,或者确保它不在 @SpringBootApplication 扫描的包路径下,否则该策略会变成全局生效,引起不必要的冲突。

java 复制代码
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

public class CustomLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                                   LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 返回随机策略的负载均衡器
        return new RandomLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

第二步:使用 @LoadBalancerClient 绑定该策略

在启动类或任意一个 Spring 扫描得到的 @Configuration 类上,通过注解指定对哪个服务生效。

java 复制代码
@SpringBootApplication
// name = 目标服务名,configuration = 自定义的策略类
@LoadBalancerClient(name = "stock-service", configuration = CustomLoadBalancerConfig.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

4. 性能优化:缓存机制 (Cache)

每次发起调用时,LoadBalancer 如果都去注册中心拉取服务列表会非常耗时。因此,LoadBalancer 在本地维护了一份缓存。

默认缓存过期时间为 35秒。如果你的服务实例经常上下线,或者对服务的感知延迟要求很高,可以通过 application.yml 进行调整:

5、Feign的重试机制

在 Spring Cloud 环境下,OpenFeign 默认是关闭重试机制的(底层使用的是 Retryer.NEVER_RETRY)。

如果调用超时,它会直接抛出异常。


要开启重试,目前主要有两种方式:

  • Spring Cloud LoadBalancer 重试(推荐)
  • OpenFeign 原生重试

方式一:使用 LoadBalancer 重试(微服务架构中最推荐)

这种方式不仅能重试,还能在不同实例之间切换

比如 A 实例挂了或超时了,它会自动去请求 B 实例。


1. 引入依赖

必须引入 spring-retry 依赖,LoadBalancer 的重试机制才会生效。

XML 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

2. 在 YAML 中配置重试策略

按上述配置,如果首次请求失败,它不会在当前实例重试,而是直接换一个新实例重试 1 次,总共最多请求 2 次。


方式二:使用 OpenFeign 原生重试

这种方式是 Feign 框架自己带的,它只是单纯地重新发起 HTTP 请求,不具备感知实例下线并切换下一个节点的能力,因此在配合注册中心使用时不如上面的方式灵活。


通过配置类注入 Retryer 即可开启:

java 复制代码
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

    @Bean
    public Retryer feignRetryer() {
        // 参数1: 初始重试间隔 (100ms)
        // 参数2: 最大重试间隔 (1000ms)
        // 参数3: 最大尝试次数 (3次,包含首次调用,即实际重试 2 次)
        return new Retryer.Default(100, 1000, 3);
    }
}

(然后在 @FeignClient(name = "stock-service", configuration = FeignConfig.class) 中绑定该配置)


🚨 重试机制的致命坑点(避坑指南)

开启重试非常简单,但如果没有处理好以下几个坑,可能会引发严重的线上故障。


坑一:Read Timeout 引发的非幂等性问题(最容易引发生产事故)

超时分为 Connect Timeout(连接超时)和 Read Timeout(读取超时)。

  • Connect Timeout :请求根本没发出去,或者没连上目标服务器。此时重试绝对安全。

  • Read Timeout :请求已经到达目标服务器并且正在执行,只是因为数据库慢或者逻辑复杂,没在规定时间内返回结果给客户端。

致命场景 :订单服务调用库存服务执行 POST /stock/deduct(扣减库存)。库存服务其实扣减成功了,但由于网络抖动耗时 6 秒,超过了 Feign 配置的 5 秒 readTimeout。订单服务认为失败了,触发重试 ,结果库存被扣了两次

解决方案

  1. 接口必须保证幂等性 。所有写入类操作(POST/PUT),必须通过唯一流水号(单据号/Token)+ 数据库唯一索引/分布式锁来防止重复提交。

  2. 强烈建议 spring.cloud.loadbalancer.retry.retry-on-all-operations 保持为 false。默认只对 GET(天然幂等)请求进行重试。


坑二:重试风暴导致雪崩

如果下游的 stock-service 是因为流量过大导致 CPU 打满而大面积超时。

此时上游的 order-service 开启了重试,原本 1000 的 QPS 会瞬间被放大成 3000 QPS(假设重试 2 次)。这不仅救不了下游,反而会成为压死骆驼的最后一根稻草,导致整个微服务集群雪崩。

解决方案

重试机制绝不能孤立使用,必须配合熔断器(如 Resilience4j 或 Sentinel)

当错误率达到一定阈值(比如 50%),直接触发熔断,拒绝后续请求并快速失败(Fallback),而不是继续傻傻地重试。


坑三:LoadBalancer 重试与 Feign 原生重试的冲突

有些开发者在网上的老教程里复制了 Feign 的 Retryer.Default 代码,同时又在 YAML 里配置了 Spring Cloud LoadBalancer 的重试。

后果 :两者会产生嵌套放大效应。假如 Feign 配置重试 3 次,LoadBalancer 配置重试 3 次,一个超时的请求最终可能会被发出去 3*3=9 次。

解决方案:永远只用一种。

推荐彻底放弃 Feign 自带的 Retryer(保持其默认的 NEVER_RETRY),统一通过 spring-retry + LoadBalancer 在 YAML 中进行全局管控。


坑四:超时时间设置不合理

重试是需要消耗总响应时间的。

假设你配置了 readTimeout=5000(5秒),并允许重试 2 次。那么客户端线程最多会被阻塞 15 秒(5s * 3次)。

如果你的服务对接了前端或网关,网关的超时时间如果是 10 秒,那么在微服务完成重试前,网关就已经给用户返回 504 Gateway Timeout 了,你的重试毫无意义。

解决方案

确保超时时间的递进关系:网关超时时间 > 消费者总超时时间(包含重试) > 生产者超时时间 。如果业务极慢,应该改用 MQ 异步处理,而不是靠拉长超时时间和重试来硬抗。


6、OpenFeign的Fallback 机制

场景:

如果 Feign 远程调用失败或超时,我不想抛出异常,而是想返回一个默认的降级数据(兜底数据),OpenFeign 的 Fallback 机制具体要怎么实现?


在 OpenFeign 中实现 Fallback(服务降级/兜底)非常成熟。

Feign 本身将降级功能委托给了底层的熔断器(如 Resilience4j 或 Alibaba Sentinel)。

实现降级主要有两种方式:

  • fallback(简单兜底)
  • fallbackFactory(带异常信息的兜底)

在实际开发中,强烈推荐使用 fallbackFactory ,因为它能捕获并记录导致降级的具体异常。

以下是完整的落地步骤:


1. 开启熔断与降级支持 (最容易漏掉的一步)

在现代版本的 Spring Cloud 中,Feign 的降级功能默认是关闭的。

必须引入熔断器依赖,并在配置文件中显式开启。

引入依赖

(如果你使用的是 Spring Cloud Alibaba 体系,引入 Sentinel 即可;如果是标准 Spring Cloud,引入 Resilience4j):

XML 复制代码
<!-- 方案 A:标准 Spring Cloud 熔断器 (Resilience4j) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<!-- 方案 B:Spring Cloud Alibaba Sentinel (推荐) -->
<!-- <dependency> -->
<!--     <groupId>com.alibaba.cloud</groupId> -->
<!--     <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> -->
<!-- </dependency> -->

修改 application.yml 开启配置

2. 方式一:使用 fallbackFactory(推荐)

FallbackFactory 的最大优势是:

能够拿到触发降级的具体异常(Throwable,非常有利于排查问题(比如到底是超时了,还是被限流了,还是目标服务报 500 了)。


编写 FallbackFactory 类:

实现 org.springframework.cloud.openfeign.FallbackFactory 接口,并必须加上 @Component 交给 Spring 容器管理

java 复制代码
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

@Component // 必须注入 Spring 容器
public class StockClientFallbackFactory implements FallbackFactory<StockFeignClient> {

    @Override
    public StockFeignClient create(Throwable cause) {
        // cause 就是导致调用的异常对象
        return new StockFeignClient() {
            @Override
            public String deductStock() {
                // 1. 记录具体的失败原因
                System.err.println("调用库存服务扣减接口失败,触发降级!原因:" + cause.getMessage());
                
                // 2. 返回兜底数据
                return "触发兜底机制:当前访问人数过多,请稍后再试!";
            }
        };
    }
}

@FeignClient 中绑定:

java 复制代码
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(
    name = "stock-service", 
    fallbackFactory = StockClientFallbackFactory.class // 指向 Factory 类
)
public interface StockFeignClient {

    @GetMapping("/stock/deduct")
    String deductStock(); 
}

3. 方式二:使用 fallback(简单粗暴)

如果你根本不在乎是什么原因导致的失败,只需要一个静态的兜底返回值,可以直接使用 fallback


编写 Fallback 类:

直接实现你的 Feign 客户端接口,并加上 @Component

java 复制代码
import org.springframework.stereotype.Component;

@Component // 必须注入 Spring 容器
public class StockClientFallback implements StockFeignClient {

    @Override
    public String deductStock() {
        return "触发简单兜底:服务暂时不可用";
    }
}

@FeignClient 中绑定:

java 复制代码
@FeignClient(
    name = "stock-service", 
    fallback = StockClientFallback.class // 指向 Fallback 类
)
public interface StockFeignClient {

    @GetMapping("/stock/deduct")
    String deductStock(); 
}

🚨 降级机制的常见避坑指南

  1. fallbackfallbackFactory 不要同时使用

    如果在一个 @FeignClient 中同时配置了这两个属性,fallbackFactory 的优先级更高,fallback 会被直接忽略。

  2. ErrorDecoder 与 Fallback 的冲突

    如果你按照之前说的自定义了 ErrorDecoder,并且在里面抛出了自定义的 RemoteCallException。别担心,这依然会触发 Fallback 机制。在 fallbackFactorycause 参数里,你拿到的正是你自定义抛出的那个 RemoteCallException,你可以通过 cause.getCode() 来做更细粒度的降级逻辑分发。

  3. Fallback 对象未注入 Spring 容器

    这是最常见的新手错误。无论是 Fallback 类还是 FallbackFactory 类,如果没有标 @Component@Service,Feign 在启动时就会报错找不到对应的 Bean。

  4. 内部服务间的异常传递

    如果你不希望触发 Fallback 默默消化掉错误,而是想把下游服务明确的业务报错(例如 "库存不足")透传给上游,那就不要配置 fallback,直接利用 ErrorDecoder 抛出异常并由上游的全局异常处理器(@RestControllerAdvice)拦截并返回给前端即可。Fallback 只适合做"非核心链路"的柔性可用保障(比如获取用户头像失败,降级返回默认头像)。

相关推荐
就叫_这个吧1 天前
IDEA本地springcloud项目 接入SkyWalking实现链路追踪
spring cloud·intellij-idea·skywalking
小江的记录本1 天前
【ORM框架】MyBatis核心原理、ORM思想、MyBatis vs JPA
java·数据库·后端·spring·spring cloud·oracle·mybatis
凤山老林1 天前
高可靠 API 网关架构:Spring Cloud Gateway 集成 Sentinel 实现智能限流、动态路由与安全管控
安全·spring cloud·架构·sentinel
凤山老林2 天前
微服务契约测试体系:Spring Cloud Contract/Pact 实战与 CI/CD 自动化 Mock 搭建
spring cloud·ci/cd·微服务
choumou_M2 天前
MySQL_1:数据库悲观锁
后端·spring·spring cloud
吴声子夜歌2 天前
Java面试——Spring Cloud原理及应用(二)
java·spring cloud·面试
夏天拐跑了西瓜3 天前
Eureka注册中心——单机与高可用集群搭建
java·spring cloud·微服务·eureka
就叫_这个吧3 天前
java springcloud熔断降级组件sentinel基础应用及nacos持久化处理
java·spring cloud·nacos·sentinel
仍然.4 天前
SpringCloud---Gateway
spring·spring cloud·gateway