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>
相关推荐
荣--1 小时前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森2 小时前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜18 小时前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB2 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
XIAOHEZIcode3 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220704 天前
如何搭建本地yum源(上)
运维
大树887 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
Inhand陈工7 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信