第三章:LoadBalancer负载均衡

理论讲解

什么是负载均衡?
负载均衡就像是餐厅里的多个服务员,当有很多顾客同时来就餐时,经理会把顾客均匀地分配给不同的服务员,避免某个服务员忙不过来,而其他服务员却闲着。在微服务中,负载均衡就是把用户请求合理地分配到多个相同的服务实例上。

应用场景举例:
想象一下双11购物节:

  • 成千上万的用户同时访问电商网站
  • 如果所有请求都打到同一台商品服务服务器,服务器会崩溃
  • 通过负载均衡,把请求分散到10台相同的商品服务服务器上
  • 这样每台服务器只处理1/10的请求,系统就能稳定运行

项目结构

chapter-03-loadbalancer/
├── commons-service/        # 公共基础服务
├── eureka-server/          # 注册中心服务端
├── user-service/           # 用户服务
├── product-service/        # 商品服务(多实例)
├── order-service/          # 订单服务(使用LoadBalancer)
└── inventory-service/      # 库存服务(新增)

完整代码实现

1. 父工程pom.xml更新

在父工程中添加LoadBalancer依赖管理:


2. Product Service商品服务(多实例配置)

商品服务配置文件1 - application-8082.properties
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================

# 商品服务实例1端口,集群部署时切忌重复
server.port=8082

# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring.application.name=product-service

#=============================================
# 1. 服务器级配置
#=============================================

# 如需统一加前缀可修改,默认根路径
server.servlet.context-path=/

#=============================================
# 2. Spring 基础配置
#=============================================

# 开启 Spring Security 时的登录账号
spring.security.user.name=admin

#=============================================
# 3. Eureka 客户端实例配置
#=============================================

# 在 Eureka 控制台显示的主机名,可读性优先
eureka.instance.hostname=product-service

# 注册时优先使用 IP,防止 hostname 解析失败
eureka.instance.prefer-ip-address=true

# 唯一标识,格式 IP:端口
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

# 心跳间隔 5s,缩短感知时间
eureka.instance.lease-renewal-interval-in-seconds=5

# 10s 内收不到心跳即剔除,开发调试可设短
eureka.instance.lease-expiration-duration-in-seconds=10

# 自定义元数据,可做灰度、路由、权重
eureka.instance.metadata-map.zone=zone-a

# 权重,负载均衡策略可读取
eureka.instance.metadata-map.weight=1

# 版本号,方便 A/B 发布
eureka.instance.metadata-map.version=v1.1

#=============================================
# 4. Eureka 客户端行为配置
#=============================================

# 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
eureka.client.register-with-eureka=true

# 是否拉取注册表(默认 true,同上)
eureka.client.fetch-registry=true

#=============================================
# 5. Actuator 监控配置
#=============================================

# 按需暴露,生产勿暴露 env/beans
management.endpoints.web.exposure.include=health,info,metrics

# 默认 never,always 方便排查
management.endpoint.health.show-details=always

#=============================================
# 6. 日志级别
#=============================================

# 业务包日志级别,上线后改为 INFO 或 WARN
logging.level.com.lihaozhe.productservice=DEBUG

商品服务配置文件2 - application-8084.properties
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================

# 商品服务实例2端口,集群部署时切忌重复
server.port=8084

# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring.application.name=product-service

#=============================================
# 1. 服务器级配置
#=============================================

# 如需统一加前缀可修改,默认根路径
server.servlet.context-path=/

#=============================================
# 2. Spring 基础配置
#=============================================

# 开启 Spring Security 时的登录账号
spring.security.user.name=admin

#=============================================
# 3. Eureka 客户端实例配置
#=============================================

# 在 Eureka 控制台显示的主机名,可读性优先
eureka.instance.hostname=product-service

# 注册时优先使用 IP,防止 hostname 解析失败
eureka.instance.prefer-ip-address=true

# 唯一标识,格式 IP:端口
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

# 心跳间隔 5s,缩短感知时间
eureka.instance.lease-renewal-interval-in-seconds=5

# 10s 内收不到心跳即剔除,开发调试可设短
eureka.instance.lease-expiration-duration-in-seconds=10

# 自定义元数据,可做灰度、路由、权重
eureka.instance.metadata-map.zone=zone-b

# 权重,负载均衡策略可读取
eureka.instance.metadata-map.weight=2

# 版本号,方便 A/B 发布
eureka.instance.metadata-map.version=v1.1

#=============================================
# 4. Eureka 客户端行为配置
#=============================================

# 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
eureka.client.register-with-eureka=true

# 是否拉取注册表(默认 true,同上)
eureka.client.fetch-registry=true

#=============================================
# 5. Actuator 监控配置
#=============================================

# 按需暴露,生产勿暴露 env/beans
management.endpoints.web.exposure.include=health,info,metrics

# 默认 never,always 方便排查
management.endpoint.health.show-details=always

#=============================================
# 6. 日志级别
#=============================================

# 业务包日志级别,上线后改为 INFO 或 WARN
logging.level.com.lihaozhe.productservice=DEBUG

商品服务控制器增强
package com.lihaozhe.productservice.controller;

import com.lihaozhe.productservice.dto.ProductDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 商品服务控制器
 * 提供商品管理的REST API
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
  // 模拟商品数据
  private final List<ProductDTO> products = new ArrayList<>();
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  // 注入当前服务端口,用于标识实例
  @Value("${server.port}")
  private String serverPort;

  // 实例标识
  private final String instanceId;

  /**
   * 构造函数,初始化模拟数据
   */
  public ProductController() {
    // 初始化测试商品
    products.add(new ProductDTO(1L, "iPhone 15", "手机", 5999.00, 100, "最新款iPhone"));
    products.add(new ProductDTO(2L, "MacBook Pro", "电脑", 12999.00, 50, "专业级笔记本电脑"));
    products.add(new ProductDTO(3L, "AirPods Pro", "耳机", 1899.00, 200, "无线降噪耳机"));
    products.add(new ProductDTO(4L, "iPad Air", "平板", 4399.00, 80, "轻薄便携平板"));

    // 生成实例ID(基于时间戳)
    this.instanceId = "product-service-" + System.currentTimeMillis() % 1000;
  }

  /**
   * 根据ID获取商品  - 增强版本,返回实例信息
   * GET /api/products/{id}
   */
  @GetMapping("/{id}")
  public Map<String, Object> getProduct(@PathVariable("id") Long id) {
    logger.info("实例 {} (端口:{}) 处理商品查询请求: {}", instanceId, serverPort, id);

    simulateProcessingTime();
    Optional<ProductDTO> productOpt = products.stream()
        .filter(p -> p.getId().equals(id))
        .findFirst();

    if (productOpt.isPresent()) {
      ProductDTO product = productOpt.get();
      return Map.of(
          "product", product,
          "instanceInfo", Map.of(
              "instanceId", instanceId,
              "serverPort", serverPort,
              "timestamp", System.currentTimeMillis()
          )
      );
    }

    return Map.of(
        "product", null,
        "instanceInfo", Map.of(
            "instanceId", instanceId,
            "serverPort", serverPort,
            "timestamp", System.currentTimeMillis()
        )
    );
  }

  /**
   * 获取所有商品 - 增强版本
   * GET /api/products
   */
  @GetMapping
  public Map<String, Object> getAllProducts() {
    logger.info("实例 {} (端口:{}) 处理获取所有商品请求", instanceId, serverPort);
    simulateProcessingTime();
    return Map.of(
        "products", products,
        "instanceInfo", Map.of(
            "instanceId", instanceId,
            "serverPort", serverPort,
            "totalProducts", products.size(),
            "timestamp", System.currentTimeMillis()
        )
    );
  }

  /**
   * 根据分类获取商品
   * GET /api/products/category/{category}
   */
  @GetMapping("/category/{category}")
  public Map<String, Object> getProductsByCategory(@PathVariable("category") String category) {
    logger.info("实例 {} (端口:{}) 处理分类查询: {}", instanceId, serverPort, category);

    simulateProcessingTime();

    List<ProductDTO> categoryProducts = products.stream()
        .filter(p -> p.getCategory().equalsIgnoreCase(category))
        .toList();

    return Map.of(
        "products", categoryProducts,
        "instanceInfo", Map.of(
            "instanceId", instanceId,
            "serverPort", serverPort,
            "category", category,
            "count", categoryProducts.size(),
            "timestamp", System.currentTimeMillis()
        )
    );
  }

  /**
   * 更新商品库存
   * PUT /api/products/{id}/stock
   */
  @PutMapping("/{id}/stock")
  public Map<String, Object> updateStock(@PathVariable("id") Long id, @RequestBody Map<String, Integer> request) {
    Integer quantity = request.get("quantity");
    logger.info("实例 {} (端口:{}) 更新商品库存: {}, 数量: {}", instanceId, serverPort, id, quantity);

    // 查找商品并更新库存
    Optional<ProductDTO> productOpt = products.stream()
        .filter(p -> p.getId().equals(id))
        .findFirst();

    if (productOpt.isPresent()) {
      ProductDTO product = productOpt.get();
      int newStock = product.getStock() + quantity;
      product.setStock(Math.max(newStock, 0)); // 库存不能为负数

      return Map.of(
          "success", true,
          "message", "库存更新成功",
          "newStock", product.getStock(),
          "instanceInfo", Map.of(
              "instanceId", instanceId,
              "serverPort", serverPort,
              "timestamp", System.currentTimeMillis()
          )
      );
    }

    return Map.of(
        "success", false,
        "message", "商品不存在",
        "instanceInfo", Map.of(
            "instanceId", instanceId,
            "serverPort", serverPort,
            "timestamp", System.currentTimeMillis()
        )
    );
  }

  /**
   * 获取实例信息
   * GET /api/products/instance-info
   */
  @GetMapping("/instance-info")
  public Map<String, Object> getInstanceInfo() {
    return Map.of(
        "instanceId", instanceId,
        "serverPort", serverPort,
        "serviceName", "product-service",
        "startupTime", System.currentTimeMillis() - (System.currentTimeMillis() % 1000000),
        "currentTime", System.currentTimeMillis()
    );
  }

  /**
   * 健康检查端点
   * GET /api/products/health
   */
  @GetMapping("/health")
  public Map<String, Object> health() {
    return Map.of(
        "status", "UP",
        "service", "product-service",
        "timestamp", System.currentTimeMillis(),
        "totalProducts", products.size()
    );
  }

  /**
   * 模拟处理时间
   * 不同实例有不同的处理时间,便于观察负载均衡效果
   */
  private void simulateProcessingTime() {
    try {
      // 基于端口号模拟不同的处理时间
      int port = Integer.parseInt(serverPort);
      int delay = port == 8082 ? 50 : 30; // 8082端口延迟50ms,8084端口延迟30ms
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}

3. Order Service订单服务(使用LoadBalancer)

pom.xml更新
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.lihaozhe</groupId>
    <artifactId>springcloud-course</artifactId>
    <version>1.0.0</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>order-service</artifactId>

  <properties>
    <maven.compiler.source>25</maven.compiler.source>
    <maven.compiler.target>25</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 替换为你的启动类全路径 -->
    <start-class>com.lihaozhe.orderservice.OrderServiceApplication</start-class>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 引出 commons-service 公共基础服务 -->
    <dependency>
      <groupId>com.lihaozhe</groupId>
      <artifactId>commons-service</artifactId>
      <version>1.0.0</version>
    </dependency>

    <!-- Spring Cloud loadbalancer依赖管理 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

    <!-- Apache HttpClient 5 -->
    <dependency>
      <groupId>org.apache.httpcomponents.client5</groupId>
      <artifactId>httpclient5</artifactId>
    </dependency>

    <!-- 可选:如果需要连接池等高级功能 -->
    <dependency>
      <groupId>org.apache.httpcomponents.core5</groupId>
      <artifactId>httpcore5</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 替换为你的启动类全路径 -->
          <mainClass>com.lihaozhe.orderservice.OrderServiceApplication</mainClass>
        </configuration>
        <!-- 可选:如果需要打包为可执行jar,添加此配置 -->
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

应用启动类更新
package com.lihaozhe.orderservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.web.client.RestClient;

/**
 * 订单服务应用启动类 - 负载均衡版本
 * 使用@LoadBalanced注解启用负载均衡功能
 * 订单服务是服务消费者,会调用用户服务和商品服务
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(OrderServiceApplication.class, args);
  }

  /**
   * 创建普通的RestClient(用于直接URL调用)
   * 首选的默认RestClient(添加@Primary)
   *
   * @return 普通的RestClient实例
   */
  @Primary
  @Bean("restClient")
  public RestClient restClient() {
    return RestClient.builder().build();
  }

  /**
   * 创建普通的RestClient(用于直接URL调用)
   * 其他RestClient(非首选)指定名称
   *
   * @return 普通的RestClient实例
   */
  @Bean("normalRestClient")
  public RestClient normalRestClient() {
    return RestClient.builder().build();
  }

  /**
   * 创建负载均衡的RestClient
   * 使用@LoadBalanced注解后,RestClient会自动使用服务名进行服务发现和负载均衡
   * 这个方法本身不实现负载均衡逻辑,只是“启用”负载均衡功能。
   * 实际的负载均衡策略由另一个组件(即 ReactorLoadBalancer<ServiceInstance>)提供。
   * 它是客户端入口,用于发起带服务发现 + 负载均衡的请求。
   * 其他RestClient(非首选)指定名称
   *
   * @return 用于发起带服务发现 + 负载均衡请求的客户端入口
   */
  @Bean("loadBalancedRestClient")
  @LoadBalanced
  public RestClient loadBalancedRestClient() {
    return RestClient.builder().build();
  }
}

工具类
package com.lihaozhe.orderservice.util;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Loadbalancer工具
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Component
public class LoadbalancerUtil {
  private final DiscoveryClient discoveryClient;
  private final LoadBalancerClient loadBalancerClient;
  @Qualifier("loadBalancedRestClient")
  private final RestClient loadBalancedRestClient;

  /**
   * 构造函数,注入依赖
   */
  public LoadbalancerUtil(DiscoveryClient discoveryClient,
                          LoadBalancerClient loadBalancerClient, RestClient loadBalancedRestClient) {
    this.discoveryClient = discoveryClient;
    this.loadBalancerClient = loadBalancerClient;
    this.loadBalancedRestClient = loadBalancedRestClient;
  }

  /**
   * 获取当前服务实例信息
   *
   * @return 当前服务实例列表
   */
  public List<ServiceInstance> getDiscoveryClient(String serviceId) {
    return discoveryClient.getInstances(serviceId);
  }


  /**
   * 获取当前选择的服务实例
   *
   * @return 当前选择的服务实例
   */
  public ServiceInstance getCurrentServiceInstance(String serviceId) {
    return loadBalancerClient.choose(serviceId);
  }

  /**
   * 随机获取某服务实例主机
   *
   * @param serviceId 服务实例
   * @return 服务实例主机 URI 地址
   */
  public String randomServiceUrl(String serviceId) {
    List<ServiceInstance> instances = getDiscoveryClient(serviceId);
    ServiceInstance instance = instances.get(ThreadLocalRandom.current().nextInt(instances.size()));
    return instance.getUri().toString();
  }

  /**
   * 获取某服务实例主机
   *
   * @param serviceId 服务实例
   * @return 服务实例主机 URI 地址
   */
  public String getCurrentServiceUrl(String serviceId) {
    return getCurrentServiceInstance(serviceId).getUri().toString();
  }

  public LoadBalancerClient getLoadBalancerClient() {
    return loadBalancerClient;
  }

  public RestClient getLoadBalancedRestClient() {
    return loadBalancedRestClient;
  }
}

负载均衡服务类
package com.lihaozhe.orderservice.service;

import com.lihaozhe.orderservice.util.LoadbalancerUtil;
import com.lihaozhe.productservice.dto.ProductDTO;
import com.lihaozhe.userservice.dto.UserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 负载均衡服务类
 * 演示如何使用LoadBalancer进行服务调用和负载均衡
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Service
public class LoadBalancerService {

  private final LoadbalancerUtil loadbalancerUtil;
  private final Logger logger = LoggerFactory.getLogger(LoadBalancerService.class);

  /**
   * 构造函数,注入依赖
   */
  public LoadBalancerService(LoadbalancerUtil loadbalancerUtil) {
    this.loadbalancerUtil = loadbalancerUtil;
  }

  /**
   * 方式1:使用@LoadBalanced的RestClient进行服务调用(推荐)
   * 这种方式会自动进行负载均衡
   */
  public ProductDTO getProductWithLoadBalancer(Long productId) {
    // 注意:这里使用服务名而不是具体的URL
    // String url = "http://product-service/api/products/" + productId;
    String productServiceId = "product-service";
    String url = String.format("%s/api/products/%d", loadbalancerUtil.getCurrentServiceUrl(productServiceId), productId);

    try {
      // 定义泛型类型:Map<String, User>
      ParameterizedTypeReference<Map<String, Object>> typeRef = new ParameterizedTypeReference<Map<String, Object>>() {
      };
      Map<String, Object> response = loadbalancerUtil.getLoadBalancedRestClient().get()
          .uri(url)
          .retrieve()
          .body(typeRef);

      if (response != null && response.get("product") != null) {
        // 将Map转换为ProductDTO
        Map<String, Object> productMap = (Map<String, Object>) response.get("product");
        Map<String, Object> instanceInfo = (Map<String, Object>) response.get("instanceInfo");

        ProductDTO product = new ProductDTO();
        product.setId(Long.valueOf(productMap.get("id").toString()));
        product.setName((String) productMap.get("name"));
        product.setCategory((String) productMap.get("category"));
        product.setPrice(Double.valueOf(productMap.get("price").toString()));
        product.setStock(Integer.valueOf(productMap.get("stock").toString()));
        product.setDescription((String) productMap.get("description"));

        logger.info("通过负载均衡调用商品服务,实例信息: {}", instanceInfo);
        return product;
      }
      return null;
    } catch (Exception e) {
      throw new RuntimeException("通过负载均衡调用商品服务失败: " + e.getMessage());
    }
  }


  /**
   * 获取所有商品服务实例信息
   */
  public List<ServiceInstance> getProductServiceInstances() {
    return loadbalancerUtil.getDiscoveryClient("product-service");
  }

  /**
   * 获取当前选择的商品服务实例
   */
  public ServiceInstance getCurrentProductServiceInstance() {
    return loadbalancerUtil.getLoadBalancerClient().choose("product-service");
  }

  /**
   * 使用负载均衡调用用户服务
   */
  public UserDTO getUserWithLoadBalancer(Long userId) {
    // String url = "http://user-service/api/users/" + userId;
    String userServiceId = "user-service";
    String url = String.format("%s/api/users/%s", loadbalancerUtil.getCurrentServiceUrl(userServiceId), userId);

    try {
      return loadbalancerUtil.getLoadBalancedRestClient().get()
          .uri(url)
          .retrieve()
          .body(UserDTO.class);
    } catch (Exception e) {
      throw new RuntimeException("通过负载均衡调用用户服务失败: " + e.getMessage());
    }
  }

  /**
   * 测试负载均衡 - 连续调用多次观察分布
   */
  public Map<String, Object> testLoadBalancing(int callCount) {
    Map<String, Integer> distribution = new java.util.HashMap<>();
    List<Map<String,Object>> details = new ArrayList<>();
    String productServiceId = "product-service";

    for (int i = 0; i < callCount; i++) {
      // String url = "http://product-service/api/products/instance-info";
      String url = String.format("%s/api/products/instance-info", loadbalancerUtil.getCurrentServiceUrl(productServiceId));

      try {
        // 定义泛型类型:Map<String, Object>
        ParameterizedTypeReference<Map<String, Object>> typeRef = new ParameterizedTypeReference<Map<String, Object>>() {
        };
        Map<String, Object> response = loadbalancerUtil
            .getLoadBalancedRestClient()
            .get()
            .uri(url)
            .retrieve()
            .body(typeRef);

        if (response != null) {
          String instanceId = (String) response.get("instanceId");
          distribution.put(instanceId, distribution.getOrDefault(instanceId, 0) + 1);
          details.add(response);
        }

        // 短暂延迟,避免请求过快
        Thread.sleep(10);
      } catch (Exception e) {
        logger.error("负载均衡测试调用失败: {}", e.getMessage());
      }
    }

    return Map.of(
        "totalCalls", callCount,
        "distribution", distribution,
        "details", details,
        "instances", loadbalancerUtil.getDiscoveryClient(productServiceId).size()
    );
  }
}

订单控制器增强
package com.lihaozhe.orderservice.controller;

import com.lihaozhe.orderservice.dto.OrderDTO;
import com.lihaozhe.orderservice.dto.OrderItemDTO;
import com.lihaozhe.orderservice.service.LoadBalancerService;
import com.lihaozhe.orderservice.util.LoadbalancerUtil;
import com.lihaozhe.productservice.dto.ProductDTO;
import com.lihaozhe.userservice.dto.UserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 订单服务控制器 - 负载均衡版本
 * 演示负载均衡的各种使用方式
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {
  private final LoadBalancerService loadBalancerService;
  private final List<OrderDTO> orders = new ArrayList<>();
  private Long orderIdCounter = 1L;
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  private final LoadbalancerUtil loadbalancerUtil;

  /**
   * 构造函数,注入服务发现工具类
   */
  public OrderController(LoadBalancerService loadBalancerService, LoadbalancerUtil loadbalancerUtil) {
    this.loadBalancerService = loadBalancerService;
    this.loadbalancerUtil = loadbalancerUtil;
  }

  /**
   * 创建订单 - 使用负载均衡版本
   * POST /api/orders
   * 演示调用用户服务和商品服务
   */
  @PostMapping
  public Map<String, Object> createOrder(@RequestBody Map<String, Object> orderRequest) {
    Long userId = Long.valueOf(orderRequest.get("userId").toString());
    Long productId = Long.valueOf(orderRequest.get("productId").toString());
    Integer quantity = Integer.valueOf(orderRequest.get("quantity").toString());

    logger.info("创建订单 - 用户ID: {}, 商品ID: {}, 数量: {}", userId, productId, quantity);

    try {
      // 1. 使用负载均衡调用用户服务
      UserDTO user = loadBalancerService.getUserWithLoadBalancer(userId);
      if (user == null) {
        return Map.of("success", false, "message", "用户不存在");
      }

      // 2. 使用负载均衡调用商品服务
      ProductDTO product = loadBalancerService.getProductWithLoadBalancer(productId);
      if (product == null) {
        return Map.of("success", false, "message", "商品不存在");
      }

      // 3. 检查库存
      if (product.getStock() < quantity) {
        return Map.of("success", false, "message", "库存不足");
      }

      // 4. 创建订单
      OrderItemDTO orderItem = new OrderItemDTO(productId, product.getName(), quantity, product.getPrice());
      List<OrderItemDTO> items = List.of(orderItem);
      Double totalAmount = product.getPrice() * quantity;

      OrderDTO order = new OrderDTO(orderIdCounter++, userId, items, totalAmount, "CREATED");
      orders.add(order);

      return Map.of(
          "success", true,
          "message", "订单创建成功",
          "orderId", order.getId(),
          "totalAmount", totalAmount,
          "user", user.getUsername(),
          "product", product.getName()
      );

    } catch (Exception e) {
      return Map.of("success", false, "message", "创建订单失败: " + e.getMessage());
    }
  }


  /**
   * 根据订单ID获取订单详情
   * GET /api/orders/{orderId}
   */
  @GetMapping("/{orderId}")
  public OrderDTO getOrder(@PathVariable("orderId") Long orderId) {
    return orders.stream()
        .filter(o -> o.getId().equals(orderId))
        .findFirst()
        .orElse(null);
  }

  /**
   * 获取用户的所有订单
   * GET /api/orders/user/{userId}
   */
  @GetMapping("/user/{userId}")
  public List<OrderDTO> getUserOrders(@PathVariable("userId") Long userId) {
    return orders.stream()
        .filter(o -> o.getUserId().equals(userId))
        .toList();
  }


  /**
   * 负载均衡测试端点
   * GET /api/orders/loadbalance-test?calls=20
   */
  @GetMapping("/loadbalance-test")
  public Map<String, Object> loadBalanceTest(@RequestParam(name = "calls", defaultValue = "20") int calls) {
    return loadBalancerService.testLoadBalancing(calls);
  }

  /**
   * GET /api/orders/testServiceInstance
   *
   * @return 商品服务地址
   */
  @GetMapping("/testServiceInstance")
  public List<String> testServiceInstance() {
    return IntStream.range(0, 10)
        .mapToObj(i -> loadbalancerUtil.getCurrentServiceUrl("product-service"))
        .collect(Collectors.toCollection(ArrayList::new));
  }

  /**
   * GET /api/orders/randomServiceInstance
   *
   * @return 随机商品服务地址
   */
  @GetMapping("/randomServiceInstance")
  public List<String> randomServiceInstance() {
    return IntStream.range(0, 10)
        .mapToObj(i -> loadbalancerUtil.randomServiceUrl("product-service"))
        .collect(Collectors.toCollection(ArrayList::new));
  }

  /**
   * 获取商品服务实例信息
   * GET /api/orders/service-instances
   */
  @GetMapping("/service-instances")
  public Map<String, Object> getServiceInstances() {
    List<ServiceInstance> instances = loadBalancerService.getProductServiceInstances();
    ServiceInstance currentInstance = loadBalancerService.getCurrentProductServiceInstance();

    List<Map<String, Object>> instanceInfo = instances.stream()
        .map(instance -> Map.of(
            "instanceId", instance.getInstanceId(),
            "host", instance.getHost(),
            "port", instance.getPort(),
            "uri", instance.getUri().toString(),
            "metadata", instance.getMetadata(),
            "isCurrent", instance.equals(currentInstance)
        )).toList();

    return Map.of(
        "serviceName", "product-service",
        "totalInstances", instances.size(),
        "currentInstance", currentInstance != null ? currentInstance.getInstanceId() : "none",
        "instances", instanceInfo
    );
  }

  /**
   * 健康检查端点
   * GET /api/orders/health
   */
  @GetMapping("/health")
  public Map<String, Object> health() {
    return Map.of(
        "status", "UP",
        "service", "order-service",
        "timestamp", System.currentTimeMillis(),
        "totalOrders", orders.size(),
        "loadBalancerEnabled", true
    );
  }
}

4. 自定义负载均衡配置

自定义负载均衡配置类

注意:

会覆盖配置文件中的策略,如有更复杂需求(如基于标签、延迟感知等),再考虑实现 ReactorServiceInstanceLoadBalancer 接口。

但对于 90% 的场景,配置文件 + 标准客户端已足够。

本案例没有使用。

如果是用 httpclient5 实现 RestClient 需要引入以下依赖

<!-- Apache HttpClient 5 -->
<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
</dependency>

<!-- 可选:如果需要连接池等高级功能 -->
<dependency>
  <groupId>org.apache.httpcomponents.core5</groupId>
  <artifactId>httpcore5</artifactId>
</dependency>
package com.lihaozhe.orderservice.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.client.RestClient;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;

import java.net.http.HttpClient;
import java.time.Duration;

/**
 * 自定义负载均衡配置
 * 可以配置不同的负载均衡策略
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Configuration
public class LoadBalancerConfiguration {

  /**
   * 创建负载均衡的RestClient
   * 使用@LoadBalanced注解后,RestClient会自动使用服务名进行服务发现和负载均衡
   * 这个方法本身不实现负载均衡逻辑,只是“启用”负载均衡功能。
   * 实际的负载均衡策略由另一个组件(即 ReactorLoadBalancer<ServiceInstance>)提供。
   * 它是客户端入口,用于发起带服务发现 + 负载均衡的请求。
   * 其他RestClient(非首选)指定名称
   *
   * @return 用于发起带服务发现 + 负载均衡请求的客户端入口
   */
  @Bean("loadBalancedRestClient")
  @LoadBalanced
  public RestClient loadBalancedRestClient() {
    return RestClient.builder().build();
  }

  /**
   * 定义一个自定义的负载均衡器 Bean。
   * Spring Cloud LoadBalancer 在需要为某个服务做实例选择时,
   * 会尝试查找类型为 ReactorLoadBalancer<ServiceInstance> 的 Bean。
   * 可以根据需要返回不同的负载均衡策略
   * 此方法返回一个具体的负载均衡策略实现(如轮询或随机)。
   *
   * @param environment               Spring 环境对象,用于获取当前上下文中的属性(如服务名)
   * @param loadBalancerClientFactory 负载均衡客户端工厂,用于获取服务实例列表提供者(ServiceInstanceListSupplier)
   * @return 一个实现了负载均衡逻辑的 ReactorLoadBalancer 实例(这里是轮询策略)
   */
  @Bean
  public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(
      Environment environment,
      LoadBalancerClientFactory loadBalancerClientFactory) {

    // 从当前 Spring 环境中获取正在被调用的服务名称。
    // LoadBalancerClientFactory.PROPERTY_NAME 的值是 "loadbalancer.client.name"
    // 这个属性由 Spring Cloud LoadBalancer 在内部自动设置,
    // 表示当前正在处理哪个微服务的负载均衡请求(例如 "user-service")。
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

    // 【可选】返回一个随机负载均衡器(每次从可用实例中随机选一个)
    // 注释掉了,未启用
    // return new RandomLoadBalancer(
    //     // getLazyProvider(name, ServiceInstanceListSupplier.class) 返回一个延迟初始化的 Provider,
    //     // 它能在需要时获取该服务(name)对应的服务实例列表(比如从 Eureka/Nacos 获取)
    //     loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
    //     name  // 当前服务名,用于标识和日志等
    // );

    // 返回一个轮询(Round Robin)负载均衡器 —— 这是 Spring Cloud LoadBalancer 的默认策略
    return new RoundRobinLoadBalancer(
        // 同上:通过工厂获取该服务名对应的服务实例列表的延迟提供者(Lazy Provider)
        // 这样可以避免在启动时就拉取所有服务列表,按需加载,提升性能
        loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
        name  // 将服务名传入,用于内部状态管理(如轮询计数器按服务隔离)
    );
  }

  /**
   * - 连接超时时间
   * - 读取超时时间
   */
  @Bean("restClientWithJDK")
  @LoadBalanced
  public RestClient restClientWithJDK() {
    IO.println("restClientWithJDK 创建成功");
    // 配置JDK HttpClient
    HttpClient jdkHttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();

    // 构建RestClient
    return RestClient.builder().requestFactory(new JdkClientHttpRequestFactory(jdkHttpClient)).build();
  }

  /**
   * - 连接超时时间
   * - 读取超时时间
   */
  @Bean("restClientWithH5")
  @LoadBalanced
  public RestClient restClientWithH5() {
    IO.println("restClientWithH5 创建成功");
    // 1. 配置Apache HttpClient的连接池和超时
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    ConnectionConfig connectionConfig = ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(5)).build();
    connectionManager.setDefaultConnectionConfig(connectionConfig);

    CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();

    // 2. 包装为RestClient所需的RequestFactory
    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

    // 3. 构建RestClient
    return RestClient.builder()
        .requestFactory(requestFactory) // 关联Apache HttpClient的配置
        .build();
  }
}

在订单服务配置中启用自定义负载均衡
# application.properties
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================

# 订单服务实例端口,集群部署时切忌重复
server.port=8083

#=============================================
# 1. Spring 基础配置
#=============================================

# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring.application.name=order-service

#=============================================
# 2. Eureka 实例级配置
#=============================================

# 在 Eureka 控制台显示的主机名,可读性优先
eureka.instance.hostname=order-service

# 注册时优先使用 IP,防止 hostname 解析失败
eureka.instance.prefer-ip-address=true

# 唯一标识,格式 IP:端口
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

# 心跳间隔 5s,缩短感知时间
eureka.instance.lease-renewal-interval-in-seconds=5

# 10s 内收不到心跳即剔除,开发调试可设短
eureka.instance.lease-expiration-duration-in-seconds=10

#=============================================
# 3. Eureka 客户端行为配置
#=============================================

# 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
eureka.client.register-with-eureka=true

# 是否拉取注册表(默认 true,同上)
eureka.client.fetch-registry=true

# 注册中心地址(带安全认证)
eureka.client.service-url.defaultZone=http://admin:lihaozhe@localhost:8761/eureka/

#=============================================
# 4. LoadBalancer配置
#=============================================

# 为所有服务启用 Spring Cloud LoadBalancer
spring.cloud.loadbalance.enabled=true

# 禁用缓存,每次请求都从注册中心获取最新实例(生产环境不建议)
spring.cloud.loadbalance.cache.ttl=0

# 启用自定义负载均衡配置
# spring.cloud.loadbalancer.configurations=custom

#=============================================
# 5. Actuator 监控配置
#=============================================

# 按需暴露,生产勿暴露 env/beans
management.endpoints.web.exposure.include=health,info,metrics

# 默认 never,always 方便排查
management.endpoint.health.show-details=always

#=============================================
# 6. 日志级别
#=============================================

# 业务包日志级别,上线后改为 INFO 或 WARN
logging.level.com.lihaozhe.orderservice=DEBUG

# application.yml
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================
# 订单服务实例端口,集群部署时切忌重复
server:
  port: 8083

#=============================================
# 1. Spring 基础配置
#=============================================
# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring:
  application:
    name: order-service
  cloud:
    loadbalancer:
      # 为所有服务启用 Spring Cloud LoadBalancer
      enabled: true
      cache:
        # 禁用缓存,每次请求都从注册中心获取最新实例(生产环境不建议)
        ttl: 0

#=============================================
# 2. Eureka 实例级配置
#=============================================
# 在 Eureka 控制台显示的主机名,可读性优先
eureka:
  instance:
    hostname: order-service
    # 注册时优先使用 IP,防止 hostname 解析失败
    prefer-ip-address: true
    # 唯一标识,格式 IP:端口
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    # 心跳间隔 5s,缩短感知时间
    lease-renewal-interval-in-seconds: 5
    # 10s 内收不到心跳即剔除,开发调试可设短
    lease-expiration-duration-in-seconds: 10

  #=============================================
  # 3. Eureka 客户端行为配置
  #=============================================
  # 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
  client:
    register-with-eureka: true
    # 是否拉取注册表(默认 true,同上)
    fetch-registry: true
    service-url:
      # 注册中心地址(带安全认证)
      defaultZone: http://admin:lihaozhe@localhost:8761/eureka/

#=============================================
# 4. Actuator 监控配置
#=============================================
# 按需暴露,生产勿暴露 env/beans
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      # 默认 never,always 方便排查
      show-details: always

#=============================================
# 5. 日志级别
#=============================================
# 业务包日志级别,上线后改为 INFO 或 WARN
logging:
  level:
    com.lihaozhe.orderservice: DEBUG
    org.springframework.cloud.loadbalancer: DEBUG

5. Inventory Service库存服务(新增)

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>springcloud-course</artifactId>
    <groupId>com.lihaozhe</groupId>
    <version>1.0.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>inventory-service</artifactId>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 替换为你的启动类全路径 -->
          <mainClass>com.lihaozhe.inventoryservice.InventoryServiceApplication</mainClass>
        </configuration>
        <!-- 可选:如果需要打包为可执行jar,添加此配置 -->
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
应用启动类
package com.lihaozhe.inventoryservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;

/**
 * 库存服务应用启动类
 * 演示如何作为服务消费者使用负载均衡
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@SpringBootApplication
@EnableDiscoveryClient
public class InventoryServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(InventoryServiceApplication.class, args);
  }

  @Bean("loadBalancedRestClient")
  @LoadBalanced
  public RestClient loadBalancedRestClient() {
    return RestClient.builder().build();
  }
}

库存控制器
package com.lihaozhe.inventoryservice.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 库存服务控制器
 * 演示库存服务如何通过负载均衡调用商品服务
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
  private final RestClient loadBalancedRestClient;
  private final LoadBalancerClient loadBalancerClient;
  private final Logger logger = LoggerFactory.getLogger(InventoryController.class);
  @Value("${server.port}")
  private String serverPort;

  public InventoryController(RestClient loadBalancedRestClient, LoadBalancerClient loadBalancerClient) {
    this.loadBalancedRestClient = loadBalancedRestClient;
    this.loadBalancerClient = loadBalancerClient;
  }

  /**
   * 检查商品库存 - 通过负载均衡调用商品服务
   * GET /api/inventory/check/{productId}
   */
  @GetMapping("/check/{productId}")
  public Map<String, Object> checkInventory(@PathVariable("productId") Long productId) {
    ServiceInstance instance = loadBalancerClient.choose("product-service");
    // 使用服务名进行调用,LoadBalancer会自动处理负载均衡
    String url = instance.getUri() + "/api/products/" + productId;

    try {
      // 定义泛型类型:Map<String, User>
      ParameterizedTypeReference<Map<String, Object>> typeRef = new ParameterizedTypeReference<Map<String, Object>>() {
      };
      Map<String, Object> response = loadBalancedRestClient.get()
          .uri(url)
          .retrieve()
          .body(typeRef);

      if (response != null && response.get("product") != null) {
        Map<String, Object> productMap = (Map<String, Object>) response.get("product");
        Map<String, Object> instanceInfo = (Map<String, Object>) response.get("instanceInfo");

        return Map.of(
            "productId", productId,
            "productName", productMap.get("name"),
            "stock", productMap.get("stock"),
            "price", productMap.get("price"),
            "checkedBy", "inventory-service:" + serverPort,
            "productServiceInstance", instanceInfo.get("instanceId"),
            "timestamp", System.currentTimeMillis()
        );
      }

      return Map.of(
          "productId", productId,
          "error", "商品不存在",
          "checkedBy", "inventory-service:" + serverPort
      );

    } catch (Exception e) {
      return Map.of(
          "productId", productId,
          "error", "检查库存失败: " + e.getMessage(),
          "checkedBy", "inventory-service:" + serverPort
      );
    }
  }

  /**
   * 批量检查库存
   * POST /api/inventory/batch-check
   */
  @PostMapping("/batch-check")
  public Map<String, Object> batchCheckInventory(@RequestBody Map<String, List<Long>> request) {
    List<Long> productIds = request.get("productIds");
    Map<String, Object> results = new HashMap<>();

    for (Long productId : productIds) {
      Map<String, Object> checkResult = checkInventory(productId);
      results.put("product_" + productId, checkResult);
    }

    return Map.of(
        "totalChecked", productIds.size(),
        "results", results,
        "checkedBy", "inventory-service:" + serverPort
    );
  }

  @GetMapping("/health")
  public Map<String, Object> health() {
    return Map.of(
        "status", "UP",
        "service", "inventory-service",
        "port", serverPort,
        "timestamp", System.currentTimeMillis()
    );
  }
}

库存服务配置
# application.properties
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================

# 库存服务实例端口,集群部署时切忌重复
server.port=8085

#=============================================
# 1. Spring 基础配置
#=============================================

# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring.application.name=inventory-service

#=============================================
# 2. Eureka 实例级配置
#=============================================

# 在 Eureka 控制台显示的主机名,可读性优先
eureka.instance.hostname=inventory-service

# 注册时优先使用 IP,防止 hostname 解析失败
eureka.instance.prefer-ip-address=true

# 唯一标识,格式 IP:端口
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

# 心跳间隔 5s,缩短感知时间
eureka.instance.lease-renewal-interval-in-seconds=5

# 10s 内收不到心跳即剔除,开发调试可设短
eureka.instance.lease-expiration-duration-in-seconds=10

#=============================================
# 3. Eureka 客户端行为配置
#=============================================

# 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
eureka.client.register-with-eureka=true

# 是否拉取注册表(默认 true,同上)
eureka.client.fetch-registry=true

# 注册中心地址(带安全认证)
eureka.client.service-url.defaultZone=http://admin:lihaozhe@localhost:8761/eureka/

#=============================================
# 4. LoadBalancer配置
#=============================================

# 为所有服务启用 Spring Cloud LoadBalancer
spring.cloud.loadbalance.enabled=true

# 禁用缓存,每次请求都从注册中心获取最新实例(生产环境不建议)
spring.cloud.loadbalance.cache.ttl=0

# 启用自定义负载均衡配置
# spring.cloud.loadbalancer.configurations=custom

#=============================================
# 5. Actuator 监控配置
#=============================================

# 按需暴露,生产勿暴露 env/beans
management.endpoints.web.exposure.include=health,info,metrics

# 默认 never,always 方便排查
management.endpoint.health.show-details=always

#=============================================
# 6. 日志级别
#=============================================

# 业务包日志级别,上线后改为 INFO 或 WARN
logging.level.com.lihaozhe.inventoryservice=DEBUG

# application.yml
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================
# 库存服务实例端口,集群部署时切忌重复
server:
  port: 8085

#=============================================
# 1. Spring 基础配置
#=============================================
# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring:
  application:
    name: inventory-service
  security:
    user:
      # 开启 Spring Security 时的登录账号
      name: admin
      # 生产环境务必换成 16 位以上随机复杂密码
      password: lihaozhe
  cloud:
    loadbalancer:
      # 为所有服务启用 Spring Cloud LoadBalancer
      enabled: true
      # 启用自定义负载均衡配置
      # configurations: custom
      cache:
        # 禁用缓存,每次请求都从注册中心获取最新实例(生产环境不建议)
        ttl: 0

#=============================================
# 2. Eureka 实例级配置
#=============================================
# 在 Eureka 控制台显示的主机名,可读性优先
eureka:
  instance:
    hostname: inventory-service
    # 注册时优先使用 IP,防止 hostname 解析失败
    prefer-ip-address: true
    # 唯一标识,格式 IP:端口
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    # 心跳间隔 5s,缩短感知时间
    lease-renewal-interval-in-seconds: 5
    # 10s 内收不到心跳即剔除,开发调试可设短
    lease-expiration-duration-in-seconds: 10

  #=============================================
  # 3. Eureka 客户端行为配置
  #=============================================
  # 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
  client:
    register-with-eureka: true
    # 是否拉取注册表(默认 true,同上)
    fetch-registry: true
    service-url:
      # 注册中心地址(带安全认证)
      defaultZone: http://admin:lihaozhe@localhost:8761/eureka/

#=============================================
# 4. Actuator 监控配置
#=============================================
# 按需暴露,生产勿暴露 env/beans
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      # 默认 never,always 方便排查
      show-details: always

#=============================================
# 5. 日志级别
#=============================================
# 业务包日志级别,上线后改为 INFO 或 WARN
logging:
  level:
    com.lihaozhe.inventoryservice: DEBUG

开发思路和过程

  1. 负载均衡原理

    • 客户端从注册中心获取所有可用服务实例
    • 根据负载均衡策略选择一个实例
    • 向选中的实例发送请求
  2. 实现策略

    • 使用@LoadBalanced注解启用负载均衡
    • 配置多个商品服务实例模拟真实环境
    • 实现不同的负载均衡测试场景
  3. 负载均衡策略

    • 轮询(Round Robin):依次选择每个实例
    • 随机(Random):随机选择实例
    • 权重(Weighted):根据实例权重分配
    • 最少连接(Least Connections):选择连接数最少的实例
  4. 监控和调试

    • 添加实例信息输出便于观察
    • 实现负载均衡测试端点
    • 配置详细的日志输出

运行测试

启动顺序:

  1. 启动Eureka Server (端口8761)

  2. 启动User Service (端口8081)

  3. 启动Product Service实例1:

    java -jar product-service-1.0.0.jar --spring.config.location=classpath:/application-8082.properties

  4. 启动Product Service实例2:

    java -jar .\product-service-1.0.0.jar --spring.config.location=classpath:/application-8084.properties

  5. 启动Order Service (端口8083)

  6. 启动Inventory Service (端口8085)

测试步骤:

# 1. 查看Eureka注册中心,确认所有实例已注册
打开浏览器访问: http://localhost:8761

# 2. 测试负载均衡 - 连续调用观察实例分布
curl "http://localhost:8083/api/orders/loadbalance-test?calls=10"

# 3. 查看服务实例信息
curl http://localhost:8083/api/orders/service-instances

# 4. 创建订单(会自动负载均衡)
curl -X POST http://localhost:8083/api/orders \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "productId": 1, "quantity": 2}'

# 5. 测试手动实例选择
curl -X POST http://localhost:8083/api/orders/manual \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "productId": 1, "quantity": 1}'

# 6. 测试库存服务的负载均衡调用
curl http://localhost:8085/api/inventory/check/1

# 7. 批量检查库存
curl -X POST http://localhost:8085/api/inventory/batch-check \
  -H "Content-Type: application/json" \
  -d '{"productIds": [1, 2, 3]}'

观察负载均衡效果:

  • 多次调用/loadbalance-test端点,观察请求在不同实例间的分布
  • 查看控制台日志,观察哪个实例处理了请求
  • 在Eureka界面确认所有实例状态正常

这一章我们学习了LoadBalancer负载均衡的使用,实现了请求在多个服务实例间的智能分配。

在下一章中,我们将学习Feign客户端,它提供了更简洁的服务调用方式。

更多推荐