在没有网关和注册中心的情况下,如果你手动写死两个副本(服务实例)的地址,Feign 默认是不能自动完成负载均衡的,除非你 配合 Ribbon 或其他负载均衡组件 来实现。
✅ 实现方式(可行方案):
你可以通过以下方式让 Feign 实现负载均衡:
方式一:使用 Ribbon(Spring Cloud Netflix Ribbon)
虽然 Spring Cloud 已逐步废弃 Ribbon,但在一些项目中仍可用。
步骤:
- 定义服务名并配置多个地址:
my-service:
ribbon:
listOfServers: http://192.168.1.10:8080,http://192.168.1.11:8080
- Feign 接口中使用服务名:
@FeignClient(name = "my-service")
public interface MyServiceClient {
@GetMapping("/api/test")
String test();
}
这样,Feign + Ribbon 就可以在这两个地址之间自动负载均衡。
方式二:自定义 Feign 的 Targeter
或 Client
实现自己的负载均衡逻辑(高级用法)
你可以不用 Ribbon,而是实现自己的 FeignClientFactoryBean
或注入一个自定义 Client
实例来控制请求分发逻辑,这样可以自己轮询或者按权重访问写死的服务实例。
❌ 不可行情况(默认行为):
如果你直接写死 Feign 的 URL:
@FeignClient(name = "my-service", url = "http://192.168.1.10:8080")
那么 Feign 就只能请求这个 URL,不会做负载均衡。
✅ 推荐实践(若不使用注册中心):
如果你不想接入注册中心(如 Eureka/Nacos):
- 用 Ribbon(或 Spring Cloud LoadBalancer)+ Feign 是最简单的方式。
如果使用的是 Spring Cloud 2020+,建议用 Spring Cloud LoadBalancer 替代 Ribbon。
Spring Cloud LoadBalancer
Feign 搭配 Spring Cloud LoadBalancer(SC LoadBalancer)是 Spring Cloud 2020 及以后的推荐方式,用于在 没有 Ribbon、没有注册中心 的情况下,仍然实现 多实例负载均衡调用。
✅ 一、场景目标
你有两个服务实例,地址写死,例如:
http://192.168.1.10:8080
http://192.168.1.11:8080
你希望 Feign 每次调用时在这两个地址之间负载均衡。
✅ 二、实现步骤
1. 添加依赖
确保使用的是 Spring Cloud 2020+,引入以下依赖:
<!-- Feign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- LoadBalancer 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2. 配置固定服务列表(写死多个地址)
在 application.yml
中配置:
spring:
cloud:
loadbalancer:
clients:
my-service:
instances:
- uri: http://192.168.1.10:8080
- uri: http://192.168.1.11:8080
注意:这是 Spring Cloud LoadBalancer 的静态服务配置方式,不需要注册中心。
3. 定义 FeignClient 接口
@FeignClient(name = "my-service") // name 对应配置的 clients.my-service
public interface MyServiceClient {
@GetMapping("/api/test")
String test();
}
4. 启用 Feign 和 LoadBalancer
确保主类上有:
@EnableFeignClients
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
✅ 三、负载均衡策略(可选)
你可以自定义负载均衡策略:
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
ReactorLoadBalancer<ServiceInstance> myServiceLoadBalancer(
Environment environment,
LoadBalancerClientFactory factory) {
return new RoundRobinLoadBalancer(
factory.getLazyProvider("my-service", ServiceInstanceListSupplier.class),
"my-service"
);
}
}
默认就是 RoundRobin(轮询),也可以实现自定义策略。
✅ 四、调用示意
当你注入并调用 MyServiceClient.test()
方法时,Spring Cloud LoadBalancer 会自动从你配置的两个实例中轮询选择一个实例进行调用。
实现熔断
Spring Cloud LoadBalancer 本身不具备熔断功能。它的职责是实现客户端负载均衡(选择可用实例),不会探测实例是否健康,也不会在实例失败时自动熔断或剔除。
✅ 你关心的问题:
一个副本在更新时不可用,LoadBalancer 会不会跳过它?
- 不会自动跳过。
- LoadBalancer 仍然会轮询或使用指定策略挑选这个“坏”实例。
- 如果你不加控制,它会把请求发给这个更新中的实例,导致访问失败。
✅ 如何解决这个问题?
✅ 方案一:结合 Spring Cloud Circuit Breaker(推荐)
Spring Cloud 提供 spring-cloud-starter-circuitbreaker-resilience4j
(或 Sentinel/Hystrix)来实现熔断机制。
步骤如下:
1. 添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
2. 修改你的 Feign 接口方法,结合熔断:
@FeignClient(name = "my-service")
public interface MyServiceClient {
@GetMapping("/api/test")
String test();
}
然后在调用方使用如下方式包装熔断:
@Service
public class MyServiceWrapper {
private final MyServiceClient client;
private final CircuitBreakerFactory<?, ?> cbFactory;
public MyServiceWrapper(MyServiceClient client, CircuitBreakerFactory<?, ?> cbFactory) {
this.client = client;
this.cbFactory = cbFactory;
}
public String safeCall() {
return cbFactory.create("myServiceCircuitBreaker")
.run(() -> client.test(), throwable -> fallback());
}
private String fallback() {
return "fallback result";
}
}
这样,如果某个实例失败,熔断器会在短期内阻止再请求那个实例。
✅ 方案二:借助健康检查 + 自定义过滤器
你也可以定期在服务端维护一个“健康实例列表”,通过 Spring Cloud LoadBalancer 的 ServiceInstanceListSupplier
自定义逻辑,把不健康的副本剔除(但这需要你自己维护健康状态)。
✅ 总结
功能 | 是否由 LoadBalancer 提供 |
---|---|
负载均衡 | ✅ 是 |
熔断(失败跳过) | ❌ 否(需配合 Circuit Breaker) |
实例健康剔除 | ❌ 否(需你手动实现) |
数据库获取副本
是的,Spring Cloud LoadBalancer 支持自定义服务实例列表来源,你完全可以把副本地址从数据库中读取,并动态更新这些地址。这样当某个副本不可用时,你只需从数据库中移除它,LoadBalancer 就不会再调用它。
✅ 实现目标
- 使用 Spring Cloud LoadBalancer
- 实例地址不来自注册中心,也不写死在配置文件中
- 从数据库查询服务实例列表
- 可以通过删除数据库记录控制负载均衡行为
✅ 实现方式
你需要自定义一个 ServiceInstanceListSupplier
。
1. 定义实体类(用于从数据库中取服务地址)
public class MyServiceInstance implements ServiceInstance {
private final String serviceId;
private final URI uri;
public MyServiceInstance(String serviceId, String host, int port) {
this.serviceId = serviceId;
this.uri = URI.create("http://" + host + ":" + port);
}
@Override public String getInstanceId() { return null; }
@Override public String getServiceId() { return serviceId; }
@Override public String getHost() { return uri.getHost(); }
@Override public int getPort() { return uri.getPort(); }
@Override public boolean isSecure() { return false; }
@Override public URI getUri() { return uri; }
@Override public Map<String, String> getMetadata() { return Collections.emptyMap(); }
}
2. 自定义 ServiceInstanceListSupplier
@Component
public class DbServiceInstanceListSupplier implements ServiceInstanceListSupplier {
private final String serviceId = "my-service";
private final MyInstanceRepository repository; // 你自己的DAO类
public DbServiceInstanceListSupplier(MyInstanceRepository repository) {
this.repository = repository;
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public Flux<List<ServiceInstance>> get() {
List<MyInstanceEntity> dbInstances = repository.findAll(); // 从数据库获取
List<ServiceInstance> instances = dbInstances.stream()
.map(i -> new MyServiceInstance(serviceId, i.getHost(), i.getPort()))
.collect(Collectors.toList());
return Flux.just(instances);
}
}
3. 启用 Feign 调用
@FeignClient(name = "my-service")
public interface MyServiceClient {
@GetMapping("/api/test")
String test();
}
✅ 实现效果
- 每次 LoadBalancer 获取实例列表时,都会从数据库中查询;
- 当你想“熔断”某个副本,只需从数据库中移除该条记录;
- 系统会自动跳过失效副本。
⚠️ 注意
get()
方法可以加缓存(比如用@Scheduled
定时刷新实例列表),避免每次都查库;- 确保数据库查询不会成为瓶颈。
自定义 Feign 的负载均衡逻辑
当然可以,下面我给你一个完整的示例:自定义 Feign 的 Client
实现负载均衡逻辑,不依赖 Ribbon、Spring Cloud LoadBalancer,也不需要注册中心。
✅ 场景
你手动指定两个服务实例地址:
http://192.168.1.10:8080
http://192.168.1.11:8080
你希望 Feign 每次调用时,在它们之间轮询负载均衡。
✅ 步骤详解
1. 定义一个轮询负载均衡器
@Component
public class MySimpleLoadBalancer {
private final List<String> servers = List.of(
"http://192.168.1.10:8080",
"http://192.168.1.11:8080"
);
private final AtomicInteger index = new AtomicInteger(0);
public String chooseServer() {
int i = Math.abs(index.getAndIncrement() % servers.size());
return servers.get(i);
}
}
2. 自定义 Feign Client 实现类
@Component
public class MyFeignClient implements Client {
private final Client delegate = new Client.Default(null, null); // 使用默认 Feign 客户端
private final MySimpleLoadBalancer loadBalancer;
public MyFeignClient(MySimpleLoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
String baseUrl = loadBalancer.chooseServer();
// 重写原始请求的 URL
String originalUrl = request.url(); // 例如 http://dummy/api/xxx
URI originalUri = URI.create(originalUrl);
URI newUri = URI.create(baseUrl + originalUri.getPath() + (originalUri.getQuery() != null ? "?" + originalUri.getQuery() : ""));
Request newRequest = Request.create(
request.httpMethod(),
newUri.toString(),
request.headers(),
request.body(),
request.charset(),
request.requestTemplate()
);
return delegate.execute(newRequest, options);
}
}
3. 注入到 Feign 配置中
@Configuration
public class FeignCustomConfig {
@Bean
public Client feignClient(MySimpleLoadBalancer loadBalancer) {
return new MyFeignClient(loadBalancer);
}
}
4. 定义 Feign 接口(URL 随便写,反正会被你自定义 Client 改写)
@FeignClient(name = "dummy", url = "http://dummy", configuration = FeignCustomConfig.class)
public interface MyServiceClient {
@GetMapping("/api/test")
String test();
}
✅ 效果
- 每次调用
MyServiceClient.test()
时,都会通过你自己的轮询逻辑在两个地址之间切换; - 你可以随时改写轮询逻辑为:加权、最少连接数、健康检查等;
- 不依赖 Ribbon、LoadBalancer 或注册中心。
Ribbon、LoadBalancer的实现原理
非常准确的问题!
是的,从 原理上讲,无论是旧版的 Ribbon,还是新版的 Spring Cloud LoadBalancer,它们实现 Feign 的客户端负载均衡,核心就是:替换 Feign 默认的 Client
实现,注入它们自己的逻辑。
✅ Feign 调用流程回顾
Feign 的调用大致流程是:
- 创建 Feign 接口代理;
- 封装请求为
Request
; - 调用
Client.execute(Request, Options)
; - 执行真正的 HTTP 请求。
✅ Ribbon 的实现原理(旧版)
在使用 Ribbon 时,Spring Cloud 会注入一个 LoadBalancerFeignClient
:
public class LoadBalancerFeignClient implements Client {
private final Client delegate;
private final SpringClientFactory clientFactory;
@Override
public Response execute(Request request, Request.Options options) {
// 从 Request 的 URL 里提取服务名
// 用 Ribbon 的负载均衡策略选择实例
// 替换 URL,再用 delegate(如 OkHttp)执行请求
}
}
特点:
- 从请求 URL 中解析服务名(如
my-service
); - 用 Ribbon 的
ILoadBalancer
查找实例; - 替换 URL;
- 交由底层 HTTP 客户端(如
OkHttpClient
)执行。
✅ Spring Cloud LoadBalancer 的实现原理(新版)
Ribbon 被废弃后,Spring Cloud 使用了 LoadBalancerClientFactory
来创建实例列表,替代 Ribbon。
注入的也是一个 LoadBalancerFeignClient
,只是实现不同:
public class LoadBalancerFeignClient implements Client {
private final Client delegate;
private final LoadBalancerClientFactory clientFactory;
@Override
public Response execute(Request request, Request.Options options) {
// 从 serviceId 获取 ServiceInstanceListSupplier
// 负载均衡选择一个实例
// 替换 request.url 为真实 IP+端口
// 调用底层 HTTP Client 发请求
}
}
✅ 总结
框架 | 作用原理 |
---|---|
Feign 默认 | 直接用 URL 发请求(无负载均衡) |
Feign + Ribbon | 使用 Ribbon 的负载均衡器 + 重写 Client 逻辑 |
Feign + LoadBalancer | 使用 Spring Cloud LoadBalancer + 重写 Client 逻辑 |
你自定义的实现 | 一样地,重写 Client ,插入自己的负载均衡策略 |