跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Spring Cloud LoadBalancer 负载均衡实战与原理

深入解析 Spring Cloud 负载均衡机制,涵盖客户端与服务端负载均衡的区别。通过 RestTemplate 配置 @LoadBalanced 注解快速集成 LoadBalancer,演示如何自定义轮询与随机策略,并剖析 LoadBalancerInterceptor 拦截请求的核心源码流程,帮助开发者掌握微服务流量分配的实现细节。

竹影清风发布于 2026/3/27更新于 2026/10/890 浏览
Spring Cloud LoadBalancer 负载均衡实战与原理

Spring Cloud LoadBalancer 负载均衡实战与原理

问题背景

在微服务架构中,远程调用是常态。假设我们之前的代码是这样写的:

List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
// 服务可能有多个,获取第一个
EurekaServiceInstance instance = (EurekaServiceInstance) instances.get(0);

这段逻辑很简单:根据应用名称获取服务实例列表,然后直接取第一个。但如果一个服务对应多个实例呢?流量是否可以合理地分配到多个实例上?

现象观察

为了验证这个问题,我们在本地启动多个 product-service 实例。通过 IDEA 的 Copy Configuration 功能,修改 VM options 为 -Dserver.port=9091,再启动一个实例,共启动 3 个服务。

IDEA 启动配置界面

访问 Eureka 注册中心,可以看到 product-service 下确实有三个实例:

Eureka 服务列表显示三个实例

此时访问订单接口 http://127.0.0.1:8080/order/1,观察日志发现:请求多次访问,都是同一台机器。这显然不是我们想要的结果,启动多个实例本意是为了分担负荷。

手动轮询方案

我们可以对上述代码进行简单修改,引入轮询逻辑:

private static AtomicInteger atomicInteger = new AtomicInteger(1);
private static List<ServiceInstance> instances;

@PostConstruct
public void init() {
    // 根据应用名称获取服务列表
    instances = discoveryClient.getInstances("product-service");
}

public OrderInfo selectOrderById(Integer orderId) {
    OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
    
    // 轮询获取实例
    int index = atomicInteger.getAndIncrement() % instances.size();
    ServiceInstance instance = instances.get(index);
    log.info(instance.getInstanceId());
    
    // 拼接 URL
    String url = instance.getUri() + "/product/" + orderInfo.getProductId();
    ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
    orderInfo.setProductInfo(productInfo);
    return orderInfo;
}

修改后再次启动,请求会被均衡地分配在不同的实例上,这就是最基础的负载均衡实现。

什么是负载均衡

负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件。当服务流量增大时,通常会采用增加机器的方式进行扩容,负载均衡就是用来在多个机器或者其他资源中,按照一定的规则合理分配负载。

打个比方:一个团队最开始只有一个人,后来随着工作量的增加,公司又招聘了几个人。负载均衡就是如何把工作量均衡地分配到这几个人身上,以提高整个团队的效率。

负载均衡的实现方式

上面的例子中,我们只是简单的对实例进行了轮询,但真实的业务场景会更加复杂。比如根据机器的配置进行负载分配,配置高的分配的流量高,配置低的分配流量低等。类似企业员工:能力强的员工可以多承担一些工作。

服务多机部署时,开发人员都需要考虑负载均衡的实现,所以也出现了一些负载均衡器,来帮助我们实现负载均衡。主要分为两类:

服务端负载均衡

在服务端进行负载均衡的算法分配。比较有名的服务端负载均衡器是 Nginx。请求先到达 Nginx 负载均衡器,然后通过负载均衡算法,在多个服务器之间选择一个进行访问。

客户端负载均衡

在客户端进行负载均衡的算法分配。把负载均衡的功能以库的方式集成到客户端,而不再是由一台指定的负载均衡设备集中提供。

比如 Spring Cloud 的 Ribbon,请求发送到客户端,客户端从注册中心(比如 Eureka)获取服务列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问。Ribbon 是 Spring Cloud 早期的默认实现,由于不再维护,所以最新版本的 Spring Cloud 负载均衡集成的是 Spring Cloud LoadBalancer(由 Spring Cloud 官方维护)。

客户端负载均衡和服务端负载均衡最大的区别在于服务清单所存储的位置。

Spring Cloud LoadBalancer 实战

快速上手

1. 配置 RestTemplate

给 RestTemplate 这个 Bean 添加 @LoadBalanced 注解就可以启用负载均衡功能:

@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
2. 修改调用地址

将 IP 端口号改为服务名称:

public OrderInfo selectOrderById(Integer orderId) {
    OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
    
    // 使用服务名代替具体 IP 和端口
    String url = "http://product-service/product/" + orderInfo.getProductId();
    
    ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
    orderInfo.setProductInfo(productInfo);
    return orderInfo;
}
3. 测试效果

启动多个 product-service 实例后,观察日志,你会发现请求被自动分配到了不同的实例上,无需手动维护轮询计数器。

负载均衡策略

负载均衡策略是一种思想,无论是哪种负载均衡器,它们的负载均衡策略都是相似的。Spring Cloud LoadBalancer 仅支持两种负载均衡策略:轮询策略 和 随机策略。

  • 轮询 (Round Robin):服务器轮流处理用户的请求。这是一种实现最简单,也最常用的策略。生活中也有类似的场景,比如学校轮流值日,或者轮流打扫卫生。
  • 随机选择 (Random):随机选择一个后端服务器来处理新的请求。
自定义负载均衡策略

Spring Cloud LoadBalancer 默认负载均衡策略是轮询策略(RoundRobinLoadBalancer)。如果服务的消费者想采用随机的负载均衡策略,也非常简单。

第一步:定义随机算法对象

通过 @Bean 将其加载到 Spring 容器中。此处使用 Spring Cloud LoadBalancer 提供的 RandomLoadBalancer:

public class LoadBalancerConfig {
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        System.out.println("==============" + name);
        return new RandomLoadBalancer(
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), 
            name
        );
    }
}

注意:该类需要满足以下两点:

  1. 不用加 @Configuration 注解。
  2. 必须在组件扫描范围内。

第二步:绑定策略

使用 @LoadBalancerClient 或 @LoadBalancerClients 注解在 RestTemplate 配置类上方。可以对不同的服务提供方配置不同的客户端负载均衡算法策略。由于项目中只有一个服务提供者,所以直接使用 @LoadBalancerClient:

@LoadBalancerClient(name = "product-service", configuration = LoadBalancerConfig.class)
@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

注解说明:

  • name:该负载均衡策略对哪个服务生效(服务提供方)。
  • configuration:该负载均衡策略用哪个负载均衡策略实现。

LoadBalancer 原理分析

LoadBalancer 的实现,核心在于 LoadBalancerInterceptor。这个类会对 RestTemplate 的请求进行拦截,然后从 Eureka 根据服务 ID 获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务 ID。

我们来看看源码实现的关键部分:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    public ClientHttpResponse intercept(final HttpRequest request,
                                        final byte[] body,
                                        final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse) this.loadBalancer.execute(serviceName,
                this.requestFactory.createRequest(request, body, execution));
    }
}

可以看到这里的 intercept 方法,拦截了用户的 HTTP 请求,然后做了几件事:

  1. request.getURI():从请求中获取 URI。
  2. originalUri.getHost():从 URI 中获取路径的主机名,也就是服务 ID(例如 product-service)。
  3. loadBalancer.execute:根据服务 ID,进行负载均衡,并处理请求。

后续的核心逻辑在 BlockingLoadBalancerClient 中:

public class BlockingLoadBalancerClient implements LoadBalancerClient {
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        String hint = this.getHint(serviceId);
        LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, this.buildRequestContext(request, hint));
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);
        supportedLifecycleProcessors.forEach((lifecycle) -> {
            lifecycle.onStart(lbRequest);
        });
        
        // 根据 serviceId,和负载均衡策略,选择处理的服务
        ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);
        if (serviceInstance == null) {
            supportedLifecycleProcessors.forEach((lifecycle) -> {
                lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));
            });
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            return this.execute(serviceId, serviceInstance, lbRequest);
        }
    }

    /**
     * 根据 serviceId,和负载均衡策略,选择处理的服务
     */
    public <T> ServiceInstance choose(String serviceId, Request<T> request) {
        // 获取负载均衡器
        ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);
        if (loadBalancer == null) {
            return null;
        } else {
            // 根据负载均衡算法,在列表中选择⼀个服务实例
            Response<ServiceInstance> loadBalancerResponse = (Response) Mono.from(loadBalancer.choose(request)).block();
            return loadBalancerResponse == null ? null : (ServiceInstance) loadBalancerResponse.getServer();
        }
    }
}

整个过程就是:拦截请求 -> 解析服务名 -> 获取实例列表 -> 执行策略选择 -> 替换地址 -> 发起实际调用。

目录

  1. Spring Cloud LoadBalancer 负载均衡实战与原理
  2. 问题背景
  3. 现象观察
  4. 手动轮询方案
  5. 什么是负载均衡
  6. 负载均衡的实现方式
  7. 服务端负载均衡
  8. 客户端负载均衡
  9. Spring Cloud LoadBalancer 实战
  10. 快速上手
  11. 1. 配置 RestTemplate
  12. 2. 修改调用地址
  13. 3. 测试效果
  14. 负载均衡策略
  15. 自定义负载均衡策略
  16. LoadBalancer 原理分析

更多推荐文章

查看全部
  • Linux 线程池封装与实现详解
  • 使用 BeautifulSoup 提取网页中的表格数据
  • Copilot 四大模式详解:Ask、Edit、Agent 与 Plan 的核心差异
  • 2026 年 Gemini 学生计划实战指南:将免费 Pro 权限转化为 AI 求职优势
  • UAV 动态环境下快速自适应感知与规划
  • NLP 领域最优秀的预训练模型详解
  • OpenClaw 龙虾机器人 Windows 系统部署指南
  • C++ 特殊类设计:不可拷贝、堆栈限制与单例模式实现
  • 2025 年第三季度程序员推荐阅读书单
  • SWE-CI:基于持续集成评估智能体代码维护能力
  • OpenAI 集成 Langchain 操作实战详解
  • 2026 年 3 月 18 日人工智能行业要闻:算力、智能体与产业动态
  • macOS 本地部署 OpenClaw 智能体框架指南
  • 基于 Qwen3-VL 与 LLaMA-Factory 的 Grounding 任务 LoRA 微调实战
  • Python 开源 AI 模型引入与测试全流程实战
  • 用 WebGIS 和百度天气做一个复古天气预报页
  • 学生如何申请并使用 GitHub Copilot
  • Qwen3-Omni 架构详解:Thinker-Talker 与 MoE 设计
  • UUID 和 GUID 的区别及常见语言实现
  • C++ 多态详解:概念、实现与底层原理

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online