eureka的使用负载均衡

在微服务架构中,服务间调用必然涉及负载均衡 ------ 核心目的是避免所有请求集中到单个服务实例,实现请求的均匀分发。而负载均衡的实现中,如何获取服务实例列表是最关键的设计点,这背后藏着 "性能" 与 "实时性" 的核心矛盾。

一、基础负载均衡逻辑:轮询(Round Robin)

我们手写实现负载均衡时,核心公式是:

java 复制代码
// 计数器自增(CAS 保证线程安全)
int index = count.getAndIncrement() % instances.size();
// 根据下标选取服务实例
String uri = instances.get(index).getUri().toString();

对应请求与实例的映射关系如下:

请求次数 1 2 3 4 5 6 7
实例下标 0 1 2 0 1 2 0

这种轮询模式能保证请求均匀分发,避免单个实例过载,是负载均衡的基础逻辑。

二、关键设计:实例列表的获取时机

负载均衡的核心前提是持有服务实例列表 (如商品服务的 product-service 实例地址),而列表的获取方式,直接决定了负载均衡的稳定性与可用性。

1. 错误写法:每次请求都从注册中心获取

如果在业务方法中每次调用都执行:

java 复制代码
instances = discoveryClient.getInstances("product-service");

会存在两个致命问题:

  • 性能损耗:频繁网络请求,增加注册中心压力,降低接口响应效率;
  • 负载均衡乱套 :Eureka/ZK 返回的实例顺序不保证固定,第一次可能是 [实例 1, 实例 2, 实例 0],第二次可能变成 [实例 2, 实例 0, 实例 1]。取模后的下标对应实例会频繁变化,请求无法均匀落到不同实例上。

2. 初步优化:@PostConstruct 启动时只取一次

这是很多新手的常用写法,在项目启动时获取实例列表:

java 复制代码
@PostConstruct
public void init() {
    // 项目启动时,从 Eureka 获取服务实例列表
    instances = discoveryClient.getInstances("product-service");
}

优势非常明显:

  • ✅ 负载均衡稳定:实例列表只获取一次,取模下标与实例的对应关系固定,轮询逻辑不乱;
  • ✅ 性能高效:避免重复网络请求,仅在启动时查询一次注册中心。

致命缺陷同样无法忽视:

  • ❌ 无法感知服务上下线:如果商品服务新增实例、原有实例重启或宕机,当前服务完全无法感知,仍会调用失效实例,导致 500 报错;
  • ❌ 缓存失效:实例列表变成 "死数据",无法同步注册中心的最新状态。
java 复制代码
   @Override
    public OrderInfo selectById(Long orderId){
       OrderInfo orderInfo = orderMapper.selectById(orderId);
    //   String url  = "https://127.0.0.1:8080/product" + orderInfo.getProductId();
       List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
      //找出他的ip和端口号
       String uri = instances.get(0).getUri().toString();
      //
       String url = uri + "/product/" + orderInfo.getProductId();
      ProductInfo proInfo = restTemplate.getForObject(url , ProductInfo.class);
       orderInfo.setProductInfo(proInfo);
       return orderInfo;
  }

三、核心矛盾:性能与实时性的平衡

这正是负载均衡设计的核心痛点:

设计方案 核心优势 核心缺陷
每次请求获取 实例列表实时最新 负载均衡乱套、性能差
启动时只取一次 负载均衡稳定、性能高 无法感知服务上下线

简单来说:不能每次都查(性能乱),也不能只查一次(实时性乱)

四、生产级解决方案:定时拉取 + 本地缓存

Spring Cloud 官方负载均衡(LoadBalancer / Ribbon)的底层逻辑,完美解决了这个矛盾,核心思路是:

  1. 本地缓存实例列表:启动时获取实例列表并缓存,后续请求直接使用缓存数据,保证性能与负载均衡稳定;
  2. 定时拉取更新缓存:每隔 3~5 秒(可配置)从注册中心拉取最新实例列表,更新本地缓存,保证服务上下线的实时性。

这种设计兼顾了性能实时性

  • 定时拉取频率可调整,平衡注册中心压力与服务感知时效;
  • 本地缓存保证负载均衡逻辑稳定,请求均匀分发;
  • 服务新增 / 宕机时,缓存会及时更新,避免调用失效实例。

自定义的负载均衡(loadbalancer)

定义beanconfig

java 复制代码
package com.ytvc.order.Config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

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

定义自定义的负载均衡

java 复制代码
package com.ytvc.order.Config;

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 CustomLoadBalanancerConfiguration {
 @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
     );
 }


}

创建模块的时候,通常在抽出一个模块放公共的东西

你的项目父工程(只放pom.xml,不放代码)

├── common <-- 这里才是放公共类的地方(普通Maven项目)

├── order-service └── product-service

父工程 pom.xml 这样写

复制代码
<modules>
    <module>common</module>
    <module>order-service</module>
    <module>product-service</module>
</modules>

微服务(order /product)只引 common

复制代码
<dependency>
    <groupId>com.ytvc</groupId>
    <artifactId>common</artifactId>
</dependency>

要不然就是使用:

java 复制代码
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <classifier>exec</classifier>
    </configuration>
</plugin>

在 Maven 里,classifier 是用来区分同一个 groupId:artifactId:version 下,不同类型的附属构件的标签,比如:

  • 带源码的包:xxx-1.0-sources.jar
  • 带 Javadoc 的包:xxx-1.0-javadoc.jar
  • 你这里的 exec,就是给 Spring Boot 插件生成的包打个 "标记"

加了 <classifier>exec</classifier> 之后,会发生什么?

1. 生成的包名会变

不加 classifier 时,spring-boot-maven-plugin 默认会生成两个包:

  • xxx.jar可执行的、带依赖的胖包 (能直接 java -jar 运行)
  • xxx.original.jar:原始的普通 Jar 包(不含依赖)

加了 <classifier>exec</classifier> 之后,生成的包名会变成:

  • xxx-exec.jar:可执行胖包(classifierexec
  • xxx.jar:原始普通 Jar 包

也就是说,把 "可执行胖包" 加上了 -exec 后缀,和普通 Jar 包区分开了。

2. 为什么要这么做?

常见的使用场景有两个:

场景 1:父子工程中,只把普通 Jar 包给其他模块依赖

  • 父工程下有多个子模块,比如:eureka-serverorder-service
  • 如果 eureka-server 模块不加 classifier,它生成的 eureka-server.jar 是可执行胖包
  • 其他模块如果依赖 eureka-server,会把这个胖包一起带进来,导致依赖臃肿、冲突

加上 <classifier>exec</classifier> 之后:

  • 模块的主构件是 eureka-server.jar(普通 Jar,不含启动类和依赖)
  • 可执行胖包是 eureka-server-exec.jar,只用来运行
  • 其他模块依赖 eureka-server 时,只会引入普通 Jar,不会把胖包带进来,完美解决依赖冲突

场景 2:同一个模块,生成多个不同用途的 Jar 包

比如一个模块既要作为依赖被其他项目引用,又要自己作为独立服务运行:

  • 主构件 xxx.jar:作为依赖,供其他项目引用
  • xxx-exec.jar:可执行胖包,用来直接部署运行
XML 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!-- 给可执行胖包加上exec后缀 -->
                <classifier>exec</classifier>
                <!-- 如果你需要指定主类,可以加上 -->
                <mainClass>com.example.EurekaServerApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

不适用的话就是这个

XML 复制代码
 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>exec</classifier>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>
相关推荐
idolao2 小时前
PE启动盘制作与启动教程 Windows版:NTFS格式化+一键制作+双模式引导指南
linux·运维·服务器
程序员晨曦2 小时前
理解函数调用Function Call
java·运维·服务器
花无缺就是我2 小时前
内网穿透哪个好,之神卓互联Linux版Arm安装教程2026最新
linux·运维·arm开发
of Watermelon League2 小时前
SQL server配置ODBC数据源(本地和服务器)
运维·服务器·github
小陈99cyh2 小时前
安装NVIDIA Container Toolkit,让gpu容器环境跑通
运维·pytorch·docker·nvidia
Run_Teenage2 小时前
Linux:理解中断
linux·运维·服务器
北山有鸟2 小时前
解析 Linux 内核驱动中的“换行美学”
linux·运维·服务器
Run_Teenage2 小时前
Linux:信号保存与捕捉
运维·服务器
龙侠九重天2 小时前
可视化自动化工具实现
运维·自动化·openclaw