news 2026/7/27 5:57:42

Spring Cloud:分布式系统的“粘合剂”(四)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Cloud:分布式系统的“粘合剂”(四)

专栏:Spring Cloud

个人主页:手握风云

目录

一、负载均衡

1.1. 问题

1.2. 定义

1.3. 分类

二、Spring Cloud LoadBalancer

2.1. 快速上手

2.2. 负载均衡策略

2.3. 底层原理

三、多机部署

3.1. 前置准备

3.2. Maven 项目打包

3.3. 服务器 Jar 包上传与后台启动


一、负载均衡

1.1. 问题

在前面的远程调用基础代码中,程序会通过 DiscoveryClient 根据服务名称拉取完整的服务实例列表,但代码逻辑固定选取列表下标为 0 的第一个实例发起请求。这种写法在服务仅部署单实例时不会出现问题,可一旦同一服务启动多份实例,流量会全部集中到首个实例,多节点扩容的价值完全无法发挥。

右键服务启动配置选择 Copy Configuration 复制配置,再通过 Modify options 菜单栏添加 VM 启动参数 -Dserver.port=端口号,以此修改不同实例的占用端口。本次演示共启动 3 个 product-service 服务,端口分别为 9090、9091、9092,全部注册至 Eureka 注册中心。

package com.yang.order.service; import com.yang.order.mapper.OrderMapper; import com.yang.order.model.OrderInfo; import com.yang.order.model.ProductInfo; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; private AtomicInteger count = new AtomicInteger(1); private List<ServiceInstance> instances; @PostConstruct public void init() { //从Eureka中获取服务列表 instances = discoveryClient.getInstances("product-service"); } public OrderInfo selectOrderById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); // 通过计数器 % 实例 = 下标索引 int index = count.getAndIncrement() % instances.size(); String uri = instances.get(index).getUri().toString(); String url = uri + "/product/" + orderInfo.getProductId(); log.info("远程调用url:{}", url); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } }

借助 AtomicInteger 实现原子自增计数,通过计数值对实例列表长度取模,循环选中不同服务实例,改造代码后重新测试,请求会均匀分配至三台实例,直观展现出负载均衡的基础效果。

1.2. 定义

负载均衡英文缩写为 LB,是构建高并发、高可用分布式系统不可或缺的核心组件。随着业务流量持续上涨,开发者通常会通过新增服务节点完成水平扩容,而负载均衡的核心作用,就是按照预设规则,将客户端流量合理分摊到多台服务机器,避免单节点压力过载,整体提升集群的处理效率。

团队初期仅一名员工,工作量提升后新增多名员工,负载均衡就相当于分配工作的规则,均衡分摊任务,充分发挥团队整体处理能力,和多服务实例分流流量的设计思路完全一致。

1.3. 分类

真实业务场景下负载均衡的分配规则十分丰富,除基础轮询外,还可根据服务器硬件性能分配流量,硬件配置更高的节点承载更多请求。为统一标准化实现,行业内衍生出两类成熟的负载均衡架构,分别是服务端负载均衡与客户端负载均衡,二者最核心的区分标准是服务实例清单的存储位置。

第一类是服务端负载均衡,典型代表工具为 Nginx。整体流程为用户请求先统一发送到独立的负载均衡中间设备,由均衡器存储全部后端服务清单,通过内置算法挑选节点后转发请求,所有流量都需要经过这一台集中式设备中转。

第二类是客户端负载均衡,功能以 SDK 类库的形式集成在调用方服务内部,无需独立中间设备。Spring Cloud 早期采用 Ribbon 组件实现该能力,但该组件已停止维护,2020.0.1 及以上新版本统一使用官方自研的 Spring Cloud LoadBalancer。执行流程为客户端先向 Eureka 注册中心拉取全部可用服务实例列表,在本地运行负载均衡算法筛选目标节点,之后直接与选中的服务实例建立连接发起调用。

二、Spring Cloud LoadBalancer

2.1. 快速上手

Spring Cloud 在 2020.0.1 版本之后正式移除了原有的 Ribbon 组件,改用 Spring Cloud LoadBalancer 作为官方默认的客户端负载均衡实现方案,也是当前新项目统一采用的标准组件。想要快速启用该组件实现负载均衡,仅需完成两步核心配置。

第一步是改造 RestTemplate 的 Bean 配置,在创建 RestTemplate 的方法上添加 @LoadBalanced 注解,该注解会开启负载均衡拦截能力,为后续服务名转发提供基础支撑。第二步需要修改远程调用的请求地址,摒弃硬编码的 IP 与端口,直接使用注册中心内注册的服务名称拼接 URL,例如把 http://127.0.0.1:9090/product/{id} 替换为 http://product-service/product/{id}。

package com.yang.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 CustomLoadBalancerConfiguration { @Bean ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RandomLoadBalancer(loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }
package com.yang.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; @LoadBalancerClient(name = "product-service", configuration = CustomLoadBalancerConfiguration.class) @Configuration public class BeanConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }

Spring Cloud LoadBalancer

package com.yang.order.service; import com.yang.order.mapper.OrderMapper; import com.yang.order.model.OrderInfo; import com.yang.order.model.ProductInfo; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * @author gao * @date 2026/7/22 11:34 */ @Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; private AtomicInteger count = new AtomicInteger(1); private List<ServiceInstance> instances; @PostConstruct public void init() { //从Eureka中获取服务列表 instances = discoveryClient.getInstances("product-service"); } public OrderInfo selectOrderById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); String url = "http://product-service/product/" + orderInfo.getProductId(); log.info("远程调用url:{}", url); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } }

持续重复调用订单接口 http://127.0.0.1:8080/order/1 进行功能验证,查看三个 product-service 实例的运行日志能够清晰发现,请求会自动分发到三台不同实例,负载均衡能力正式生效。

2.2. 负载均衡策略

负载均衡策略是一套通用的流量分配逻辑,不管是服务端均衡器还是客户端均衡组件,底层设计思路都高度相似。Spring Cloud LoadBalancer 内置仅提供两种原生负载均衡策略,分别是轮询策略与随机策略。
轮询策略 RoundRobin 是组件默认使用的分配方案,逻辑为请求按照顺序轮流分配给集群内每一台可用实例,实现简单、适用绝大多数常规业务场景。随机选择 Random 策略则是每次发起请求时,从全部可用实例中随机挑选一台处理请求,分配逻辑无固定顺序。
如果业务场景需要切换为随机策略,支持自定义负载均衡算法实现。首先需要新建负载均衡配置类 LoadBalancerConfig,在类中注入 RandomLoadBalancer 类型的 Bean,同时有两个硬性要求:配置类不能标注 @Configuration 注解,且该类需要处于 Spring 组件扫描范围内。
完成策略 Bean 编写后,还需要借助 @LoadBalancerClient 注解绑定策略与目标服务。该注解添加在 RestTemplate 配置类上方,其中 name 属性指定需要生效负载均衡策略的服务名称,configuration 属性指向自定义的策略配置类。项目中仅存在 product-service 一个服务提供者,因此直接使用单服务绑定注解@LoadBalancerClient即可完成配置。

2.3. 底层原理

Spring Cloud LoadBalancer 的核心拦截载体是 LoadBalancerInterceptor,该拦截器会拦截所有通过 RestTemplate 发起的 HTTP 远程调用,自动完成服务名解析、实例筛选、地址替换全流程操作。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) { this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; } public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer)); } 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 拦截方法后,第一步会读取请求原始 URI,提取 URI 中的 Host 作为服务 ID,也就是我们拼接 URL 时填写的服务名称 product-service;第二步调用 LoadBalancerClient 的 execute 执行方法,传入解析得到的服务 ID,进入实例选择逻辑。

在 BlockingLoadBalancerClient 的 execute 方法内部,会先调用 choose 方法执行负载均衡筛选。程序会根据服务 ID 获取对应的负载均衡器,再调用均衡器内置的 choose 方法,基于预设的轮询或随机策略,从 Eureka 拉取的服务实例列表中选出一台可用 ServiceInstance。

如果筛选后没有找到任何可用实例,程序会直接抛出无服务实例异常;若成功获取有效实例,则将请求 URL 中的服务名替换为实例真实 IP 与端口,最终发起远程调用,完成一整套客户端负载均衡流程。

三、多机部署

3.1. 前置准备

  1. 服务器安装 MySQL,导入业务初始化 SQL;
  2. 修改项目配置文件,替换数据库连接账号、密码适配服务器 MySQL。

3.2. Maven 项目打包

分别对 3 个微服务打包:eureka-server、order-service、product-service,标准 SpringBoot Maven 打包流程。

3.3. 服务器 Jar 包上传与后台启动

  • Eureka、Order 单实例启动;
  • Product 多实例通过 --server.port=端口 区分端口,分别输出独立日志文件
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/27 5:56:39

可逆数据结构在DevOps部署回滚中的实践

1. 为什么需要无损回滚机制&#xff1f; 在持续交付和DevOps实践中&#xff0c;配置变更的回滚能力一直是系统可靠性的关键指标。传统回滚方案通常采用快照备份或版本控制的方式&#xff0c;但这些方法存在两个致命缺陷&#xff1a; 数据丢失风险 &#xff1a;快照备份通常是…

作者头像 李华
网站建设 2026/7/27 5:52:31

少样本新类发现(FSNCD)技术解析与应用实践

1. 少样本新类发现&#xff1a;从理论到实践的全面解析在机器学习领域&#xff0c;我们常常面临一个根本性挑战&#xff1a;如何让模型不仅能识别已知类别&#xff0c;还能自主发现全新的类别&#xff1f;这个问题在现实场景中尤为重要——想象一下&#xff0c;一个监控系统需要…

作者头像 李华
网站建设 2026/7/27 5:50:51

PHP开发中XSS攻击的防御实战:从原理到多层次安全方案

1. 项目概述&#xff1a;为什么XSS是PHP开发者的必修课&#xff1f;干了这么多年PHP开发&#xff0c;我敢说&#xff0c;XSS&#xff08;跨站脚本攻击&#xff09;绝对是每个后端程序员绕不开的“老朋友”。你可能觉得&#xff0c;现在框架这么成熟&#xff0c;谁还会犯这种低级…

作者头像 李华
网站建设 2026/7/27 5:50:37

《独家RT-DETR多模态改进专栏目录 》全网独家创新,多模态融合改进教程,包含早期融合、中期融合、后期融合、损失函数改进、二次创新模块、独家创新等几百种创新点改进,答疑群提供完整项目,永久更新中

&#x1f525;《全新RT-DETR多模态创新改进专栏》&#x1f525; “进入RT-DETR多模态项目答疑群&#xff01;项目永久更新与维护、答疑&#xff01;” 来自精选多模态检测相关顶会顶刊论文、更容易涨点发论文&#xff01; 多模态改进&#xff0c;只要故事编好&#xff0c;很…

作者头像 李华
网站建设 2026/7/27 5:49:43

OpenCV在vs studio中的配置

博主只是初入此行的蒻蒟&#xff0c;这篇文章内容不一定正确&#xff08;一些限定名词也不一定说的对&#xff09;&#xff0c;但一定解决了博主在初学时的问题&#xff0c;后续有变动还会继续更新&#xff0c;希望能帮到大家 目录 第一种方式CMake配置&#xff08;推荐&…

作者头像 李华
网站建设 2026/7/27 5:47:39

Godot工程化实践:从路径错误到健壮项目架构

1. 项目概述&#xff1a;从“GodotProjectDir is null”到工程化实践如果你在用Godot开发游戏&#xff0c;尤其是项目稍微复杂一点&#xff0c;或者尝试用Git进行版本管理时&#xff0c;很可能在编辑器控制台里见过这个让人心头一紧的红色错误&#xff1a;“GodotProjectDir is…

作者头像 李华