【SpringCloud合集-02】Spring Cloud LoadBalancer 负载均衡器学习笔记

目录

一、核心定位与组件架构

[1.1 定位与演进](#1.1 定位与演进)

[1.2 核心组件分层架构](#1.2 核心组件分层架构)

[1.3 与 Ribbon 的核心对比](#1.3 与 Ribbon 的核心对比)

[二、Spring Cloud 整合实战](#二、Spring Cloud 整合实战)

[2.1 与 RestTemplate 整合](#2.1 与 RestTemplate 整合)

第一步:引入依赖

[第二步:配置 RestTemplate](#第二步:配置 RestTemplate)

第三步:业务调用

[2.2 与 OpenFeign 整合](#2.2 与 OpenFeign 整合)

[2.3 基础配置项说明](#2.3 基础配置项说明)

三、负载均衡核心执行流程源码解析

[3.1 完整执行链路图](#3.1 完整执行链路图)

[3.2 拦截器入口源码](#3.2 拦截器入口源码)

[3.3 负载均衡客户端执行源码](#3.3 负载均衡客户端执行源码)

[3.4 子上下文隔离设计源码](#3.4 子上下文隔离设计源码)

四、内置负载均衡策略源码解析

[4.1 轮询策略 RoundRobinLoadBalancer(默认)](#4.1 轮询策略 RoundRobinLoadBalancer(默认))

[4.2 随机策略 RandomLoadBalancer](#4.2 随机策略 RandomLoadBalancer)

[4.3 自定义负载均衡策略实战](#4.3 自定义负载均衡策略实战)

第一步:自定义负载均衡器

第二步:注册自定义策略

第三步:指定服务使用自定义策略

五、服务实例列表获取与缓存机制

[5.1 实例列表提供者分层架构](#5.1 实例列表提供者分层架构)

[5.2 缓存机制源码解析](#5.2 缓存机制源码解析)

[5.3 饥饿加载机制](#5.3 饥饿加载机制)

六、实战踩坑与最佳实践

[6.1 常见踩坑点](#6.1 常见踩坑点)

[6.2 生产最佳实践](#6.2 生产最佳实践)

七、面试速记总结


基于 Spring Cloud 2021.0.x 源码深度拆解,替代 Ribbon 的官方标准实现

一、核心定位与组件架构

1.1 定位与演进

Spring Cloud LoadBalancer(简称 SCL)是 Spring 官方推出的第二代负载均衡组件,用于替代已停止迭代的 Netflix Ribbon。从 Spring Cloud 2020.0.0(Ilford 版本)开始,Ribbon 被正式移除,LoadBalancer 成为微服务调用的默认负载均衡实现。

它的核心定位是客户端负载均衡:负载均衡逻辑在消费者本地执行,消费者从注册中心拉取服务列表,在本地通过算法选择目标实例发起调用,无需经过独立的负载均衡中间件,性能更高、架构更简单。

核心特性:

  • 基于 Reactor 响应式编程,同时支持阻塞(RestTemplate)和非阻塞(WebClient)两种调用方式
  • 与 Spring 生态深度整合,原生支持 RestTemplate、OpenFeign、WebClient
  • 采用服务级子上下文隔离设计,不同服务的负载均衡配置互不影响
  • 支持自定义负载均衡策略、实例过滤、缓存、重试等扩展能力

1.2 核心组件分层架构

各层职责:

  • 调用入口层:RestTemplate、OpenFeign 等调用工具,对上层业务完全透明
  • 拦截器层:拦截 HTTP 请求,解析服务名,触发负载均衡逻辑
  • 客户端层:统一负载均衡入口,封装响应式到阻塞的适配
  • 均衡算法层:核心负载均衡策略实现,如轮询、随机、权重等
  • 实例供给层:负责从注册中心获取、缓存、更新服务实例列表

1.3 与 Ribbon 的核心对比

对比维度 Spring Cloud LoadBalancer Netflix Ribbon
官方归属 Spring 官方原生维护 Netflix 开源,已停止迭代
编程模型 响应式 Reactor 基础,兼容阻塞 纯阻塞式编程
内置策略 轮询、随机,扩展灵活 轮询、随机、加权响应时间等 7 种
上下文隔离 基于 NamedContextFactory 子容器隔离 同样子容器隔离设计
生态适配 适配 Spring 全家桶新特性,持续迭代 仅适配旧版 Spring Cloud
重试机制 依赖 Spring Retry 实现 内置重试能力

面试:Spring Cloud 为什么要替换 Ribbon?核心原因是 Ribbon 停止维护,且是阻塞式模型,无法适配 Spring WebFlux 响应式生态,官方需要一个原生、可迭代的负载均衡组件。

二、Spring Cloud 整合实战

2.1 与 RestTemplate 整合

这是最基础的使用方式,通过 @LoadBalanced 注解给 RestTemplate 赋予负载均衡能力。

第一步:引入依赖

Spring Cloud Alibaba 2.2.9 及以上版本默认已包含 LoadBalancer,无需额外引入核心包;若为纯 Spring Cloud 项目,显式引入:

XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
第二步:配置 RestTemplate
java 复制代码
@Configuration
public class RestTemplateConfig {

    /**
     * @LoadBalanced 核心注解:给RestTemplate添加负载均衡拦截器
     * 后续调用时直接写服务名,自动替换为实际IP端口
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
第三步:业务调用

直接用服务名替代具体的 IP 端口,负载均衡自动生效:

java 复制代码
@Service
public class OrderService {

    @Autowired
    private RestTemplate restTemplate;

    public User getUserById(Long userId) {
        // 直接写服务名 user-service,不需要写具体IP
        String url = "http://user-service/user/" + userId;
        return restTemplate.getForObject(url, User.class);
    }
}

2.2 与 OpenFeign 整合

OpenFeign 原生适配 LoadBalancer,引入 Feign 依赖后自动生效,无需额外配置,直接写服务名即可:

java 复制代码
@FeignClient("user-service")
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long userId);
}

Feign 内部会自动通过 LoadBalancer 选择实例发起调用,原理和 RestTemplate 一致,只是拦截器不同。

2.3 基础配置项说明

java 复制代码
spring:
  cloud:
    loadbalancer:
      cache:
        enabled: true          # 开启服务实例本地缓存,默认开启
        ttl: 35s               # 缓存过期时间,默认35秒
        capacity: 256          # 缓存最大容量
      eager-load:
        enabled: true          # 开启饥饿加载,启动时初始化负载均衡上下文
        clients: user-service,order-service  # 指定需要提前加载的服务名
      retry:
        enabled: true          # 开启重试,需配合spring-retry依赖
        max-retries: 0         # 同一实例重试次数
        max-retries-next-server: 1  # 切换实例重试次数

三、负载均衡核心执行流程源码解析

3.1 完整执行链路图

3.2 拦截器入口源码

@LoadBalanced 注解的本质是给 RestTemplate 注入 LoadBalancerInterceptor 拦截器,这是负载均衡的触发入口。

java 复制代码
// org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                                        final ClientHttpRequestExecution execution) throws IOException {
        // 1. 获取原始请求的URI
        final URI originalUri = request.getURI();
        // 2. 从URI中解析出服务名
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        // 3. 调用负载均衡客户端,执行实例选择和请求执行
        return this.loadBalancer.execute(serviceName,
                this.requestFactory.createRequest(request, body, execution));
    }
}

源码解析

  • 拦截器实现了 Spring 的 ClientHttpRequestInterceptor 接口,RestTemplate 发送请求前会经过所有拦截器。
  • 核心逻辑只有三步:解析服务名 → 交给负载均衡客户端 → 执行并返回结果。
  • @LoadBalanced 注解通过 LoadBalancerAutoConfiguration 自动配置,给所有加了该注解的 RestTemplate Bean 自动添加这个拦截器。

3.3 负载均衡客户端执行源码

BlockingLoadBalancerClient 是阻塞模式下的默认实现,负责桥接响应式的负载均衡器和阻塞式的调用。

java 复制代码
// org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    // 1. 根据服务名获取对应的负载均衡器
    ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
    if (loadBalancer == null) {
        throw new IllegalStateException("No loadbalancer found for " + serviceId);
    }

    // 2. 响应式转阻塞:调用choose方法选择实例,阻塞等待结果
    ServiceInstance serviceInstance = Mono.from(loadBalancer.choose())
            .map(Response::getServer)
            .block();

    if (serviceInstance == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }

    // 3. 用选中的实例执行实际请求
    return execute(serviceId, serviceInstance, request);
}

private <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    try {
        // 核心:重构URI,把服务名替换成实例的IP+端口
        URI uri = reconstructURI(serviceInstance, request.getUri());
        // 执行实际的HTTP调用
        return request.apply(serviceInstance);
    } catch (Exception e) {
        throw new IOException("LoadBalancer request failed", e);
    }
}

关键设计

  • loadBalancerClientFactoryNamedContextFactory 的实现类,每个服务对应一个独立的 Spring 子上下文,不同服务的负载均衡配置、策略、缓存完全隔离,互不影响。
  • 底层核心均衡器是响应式的,阻塞客户端通过 .block() 方法转为同步调用,兼容传统业务代码。

3.4 子上下文隔离设计源码

LoadBalancerClientFactory 继承自 NamedContextFactory,这是 Spring Cloud 通用的「命名上下文工厂」设计模式。

java 复制代码
public class LoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification> {
    // 学习下思想得了
    // 每个服务名对应一个独立的ApplicationContext子容器
    // 子容器里有自己的负载均衡器、实例列表提供者、配置Bean
    // 不同服务的配置完全隔离,修改A服务的策略不影响B服务
}

四、内置负载均衡策略源码解析

4.1 轮询策略 RoundRobinLoadBalancer(默认)

版本接口差异声明 不同 Spring Cloud LoadBalancer 版本中,轮询策略实现的接口存在层级差异,但核心逻辑完全等价:

  • 2.2.x 及更早版本:RoundRobinLoadBalancer 直接实现顶层泛型接口 ReactorLoadBalancer<ServiceInstance>
  • 3.0.x 及更新版本:官方抽取了语义化子接口 ReactorServiceInstanceLoadBalancer,该接口继承自 ReactorLoadBalancer<ServiceInstance>,仅做类型固化,无新增抽象方法,轮询器改为实现这个子接口。

两者是纯粹的父子继承关系,内部计数器、取模算法、实例选择逻辑完全一致,不存在原理冲突。主要是学习框架的底层实现原理与思想,后续能够用到其他地方或者业务上。

轮询是 LoadBalancer 的默认策略,按顺序依次调用每个服务实例,实现简单、负载均匀,适合所有实例性能相近的场景。

java 复制代码
// org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer
public class RoundRobinLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> {

    // 原子整型计数器,保证多线程下的线程安全
    private final AtomicInteger position = new AtomicInteger(0);
    private final ServiceInstanceListSupplier serviceInstanceListSupplier;
    private final String serviceId;

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 1. 从实例提供者获取当前可用的服务实例列表
        return serviceInstanceListSupplier.get()
                .next()
                .map(serviceInstances -> processInstanceResponse(serviceInstances));
    }

    private Response<ServiceInstance> processInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        // 2. 原子类自增,获取当前计数位置
        int pos = Math.abs(this.position.incrementAndGet());
        // 3. 对实例数取模,得到本次选中的实例索引
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }
}

源码深度解析

  • AtomicInteger 原子类实现计数器,CAS 无锁操作保证高并发下的线程安全,不会出现两个请求拿到同一个索引的问题。
  • 计数器是持续递增的,不是循环归零,通过取模运算实现循环效果,避免归零瞬间的并发问题。
  • 实例列表动态变化时(上下线),取模的分母会变化,可能出现短暂的调用不均匀,属于正常现象。

4.2 随机策略 RandomLoadBalancer

从可用实例列表中随机选择一个,流量越大约均匀,适合实例性能差异不大的场景。

java 复制代码
public class RandomLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> {

    private final ServiceInstanceListSupplier serviceInstanceListSupplier;
    private final Random random = new Random();

    private Response<ServiceInstance> processInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        // 生成随机索引,范围是0到实例数-1
        int index = random.nextInt(instances.size());
        return new DefaultResponse(instances.get(index));
    }
}

4.3 自定义负载均衡策略实战

生产环境经常需要基于权重、灰度标签、一致性哈希等自定义策略,以权重策略为例,完整实现如下:

第一步:自定义负载均衡器
java 复制代码
public class WeightLoadBalancer implements ReactiveLoadBalancer<ServiceInstance> {

    private final ServiceInstanceListSupplier supplier;

    public WeightLoadBalancer(ServiceInstanceListSupplier supplier) {
        this.supplier = supplier;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplier.get().next().map(this::selectByWeight);
    }

    private Response<ServiceInstance> selectByWeight(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }

        // 1. 计算总权重
        int totalWeight = 0;
        for (ServiceInstance instance : instances) {
            // 从实例元数据中读取权重,默认权重为1
            String weightStr = instance.getMetadata().get("weight");
            int weight = StringUtils.hasText(weightStr) ? Integer.parseInt(weightStr) : 1;
            totalWeight += weight;
        }

        // 2. 生成0到总权重之间的随机数
        int random = ThreadLocalRandom.current().nextInt(totalWeight);
        int currentWeight = 0;

        // 3. 权重轮询:累加权重,命中区间则返回对应实例
        for (ServiceInstance instance : instances) {
            String weightStr = instance.getMetadata().get("weight");
            int weight = StringUtils.hasText(weightStr) ? Integer.parseInt(weightStr) : 1;
            currentWeight += weight;
            if (random < currentWeight) {
                return new DefaultResponse(instance);
            }
        }
        // 兜底返回第一个实例
        return new DefaultResponse(instances.get(0));
    }
}
第二步:注册自定义策略
java 复制代码
// 注意:不要加@Configuration注解,避免被全局扫描
// 这是单个服务的子上下文配置
public class WeightLoadBalancerConfig {

    @Bean
    public ReactiveLoadBalancer<ServiceInstance> loadBalancer(Environment environment,
                                                              ServiceInstanceListSupplier supplier) {
        String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new WeightLoadBalancer(supplier);
    }
}
第三步:指定服务使用自定义策略
java 复制代码
@Configuration
@LoadBalancerClient(value = "user-service", configuration = WeightLoadBalancerConfig.class)
public class LoadBalancerConfig {
    // 给user-service服务单独指定权重负载均衡策略
    // 其他服务继续使用默认轮询策略
}

五、服务实例列表获取与缓存机制

5.1 实例列表提供者分层架构

ServiceInstanceListSupplier 是实例供给的顶层接口,采用装饰器模式层层增强,形成完整的获取、过滤、缓存链路:

  1. DiscoveryClientServiceInstanceListSupplier:最底层,通过 DiscoveryClient 从注册中心(Nacos/Eureka)拉取原始服务列表
  2. HealthCheckServiceInstanceListSupplier:健康检查过滤,剔除不健康的实例
  3. CachingServiceInstanceListSupplier:缓存装饰器,给实例列表增加本地缓存能力
  4. ZonePreferenceServiceInstanceListSupplier:同机房优先过滤,优先选择同可用区的实例

设计思想:用装饰器模式替代继承,灵活组合各种能力,需要什么功能就加对应的装饰器,扩展性极强。

5.2 缓存机制源码解析

默认开启本地缓存,避免每次调用都去注册中心拉取实例,大幅降低注册中心压力,提升调用性能。

java 复制代码
// CachingServiceInstanceListSupplier 核心逻辑
public Flux<List<ServiceInstance>> get() {
    // 1. 先查缓存
    Cache cache = cacheManager.getCache(serviceId);
    if (cache != null) {
        List<ServiceInstance> cached = cache.get(serviceId, List.class);
        if (cached != null) {
            return Flux.just(cached);
        }
    }
    // 2. 缓存未命中,调用下层提供者拉取
    return delegate.get()
            .doOnNext(instances -> {
                // 3. 拉取到后写入缓存,设置过期时间
                if (cache != null) {
                    cache.put(serviceId, instances);
                }
            });
}

特性说明

  • 默认缓存过期时间 35 秒,过期后下次调用才重新拉取,不是主动定时更新。
  • 缓存的好处是性能高、注册中心故障不影响调用;坏处是实例上下线有延迟,最长延迟等于缓存过期时间。
  • 生产环境权衡:对扩缩容实时性要求高的场景调小 TTL,对稳定性要求高的场景调大 TTL。

5.3 饥饿加载机制

默认情况下,负载均衡的子上下文是懒加载的:第一次调用某个服务时,才会创建该服务的 Spring 子上下文,初始化负载均衡器、实例缓存等组件,导致第一次调用耗时明显变长。

开启饥饿加载后,应用启动时就会初始化所有指定服务的负载均衡上下文,消除首次调用延迟:

java 复制代码
spring:
  cloud:
    loadbalancer:
      eager-load:
        enabled: true
        clients: user-service,order-service,goods-service

生产:核心链路的服务全部开启饥饿加载,避免冷启动时的超时、慢调用问题。

六、实战踩坑与最佳实践

6.1 常见踩坑点

  1. 依赖冲突导致负载均衡失效 项目中同时引入了 Ribbon 和 LoadBalancer 依赖,或者旧版 starter 自带 Ribbon,会导致负载均衡行为异常、策略不生效。解决方式是排除 Ribbon 依赖,统一使用 LoadBalancer。

  2. 实例宕机后仍被调用 本地缓存默认 35 秒过期,实例宕机后最长 35 秒内还会被负载均衡选中,导致调用失败。 解决方案:调小缓存 TTL;配合重试机制,失败后自动切换实例;配合健康检查过滤器主动探活。

  3. 首次调用超时 懒加载导致第一次调用需要初始化子上下文,耗时几百毫秒到几秒,高并发下容易触发超时。 解决方案:核心服务开启饥饿加载,启动时提前初始化。

  4. 重试次数配置错误 max-retries 是同一实例的重试次数,max-retries-next-server 是切换实例的次数,总调用次数 = (1 + max-retries) * (1 + max-retries-next-server)。配置过大会导致雪崩效应,幂等性差的接口要谨慎开启重试。

  5. 自定义配置被全局扫描 自定义负载均衡配置类如果加了 @Configuration 且被主类包扫描到,会变成全局配置,所有服务都生效,违背隔离设计。 解决方案:自定义配置类不要加 @Configuration,或者放到扫描范围之外。

关于第4点重试次数:

在 Ribbon 的 RetryRule 或 Spring Retry 的 RetryTemplate 实现中,重试是双层循环结构:

  • 外层循环 :控制"切换实例"(next-server

  • 内层循环 :控制"当前实例重试"(retries

代码逻辑如下:

java 复制代码
for (int i = 0; i <= maxRetriesNextServer; i++) { // 外层:换机器
    // 选一台机器
    for (int j = 0; j <= maxRetries; j++) { // 内层:死磕这台机器
        try {
            // 发起调用
            return result;
        } catch (Exception e) {
            if (j == maxRetries) break; // 内层结束,外层换机
        }
    }
}

因为是两层 for 循环嵌套,总执行次数是 (1 + 内层重试) × (1 + 外层换机)

6.2 生产最佳实践

  • 核心服务开启饥饿加载,消除首次调用延迟
  • 合理设置缓存 TTL,平衡实时性与性能,常规业务 10~30 秒即可
  • 幂等接口开启重试,非幂等接口关闭重试,避免数据重复
  • 多机房部署时开启同机房优先,降低跨机房调用延迟
  • 自定义策略优先通过装饰器扩展,不直接修改核心逻辑
  • 配合注册中心的健康检查,双重过滤不健康实例,提升调用成功率

七、面试速记总结

  1. 定位:Spring Cloud LoadBalancer 是官方替代 Ribbon 的客户端负载均衡组件,支持阻塞和响应式,基于子上下文实现服务隔离。
  2. 执行流程:拦截器拦截请求 → 解析服务名 → 从子上下文获取负载均衡器 → 拉取实例列表 → 算法选实例 → 重构 URL → 发起调用。
  3. 默认策略:默认轮询 RoundRobin,用 AtomicInteger 原子自增取模实现,线程安全;内置随机策略,支持自定义扩展。
  4. 缓存机制:默认本地缓存 35 秒,装饰器模式实现,性能高但有延迟;可通过 TTL 调整实时性。
  5. 子上下文设计:每个服务对应独立 Spring 子容器,配置隔离、故障隔离;饥饿加载解决首次调用慢的问题。
  6. 核心设计模式:拦截器模式、装饰器模式(实例供给链)、工厂模式(命名上下文工厂)、策略模式(负载均衡算法)。
相关推荐
YUS云生1 小时前
Python学习笔记·第28天:Git入门——版本控制与基础操作
笔记·python·学习
xiaoyuchidayuma1 小时前
【# 电压极限圆、电流极限圆、MTPA曲线、最大功率曲线的关系】
笔记·学习
r_oo_ki_e_10 小时前
Java Map 集合学习笔记
java·笔记·学习
笨鸟先飞的橘猫12 小时前
skynet——sharetable学习
学习·skynet
liuyicenysabel12 小时前
大模型学习笔记 · 第八篇 · 进阶:偏好对齐与多卡训练
人工智能·笔记·学习
Funing715 小时前
FreeRTOS学习day1:Keil 工程配置与 FreeRTOS 链表机制理解
数据结构·学习·链表
心中有国也有家15 小时前
使用 DevEco Studio 配置 Flutter 鸿蒙签名
学习·flutter·华为·harmonyos
灯澜忆梦15 小时前
MySQL 完整学习路线
学习·mysql
森林古猿116 小时前
再论斜率优化
c++·学习·算法