Java:实现负载均衡轮询算法(附带源码)
1. 项目背景详细介绍
随着微服务架构和高并发 Web 应用的普及,如何将客户端的请求在多台后端服务器之间均匀分配,成为保证系统性能与可用性的关键一环。最基础、最常见的负载均衡算法就是“轮询”(Round Robin):按顺序将请求依次分发给列表中的每一台服务器,当达到末尾时再从头开始。
-
简单易懂:轮询算法实现逻辑直观明了,不依赖复杂状态。
-
零配置:无需为服务器分配权重、无需监控实时负载,即可上线运行。
-
通用场景:适用于后端实例性能、规格基本一致的场景。
本项目旨在使用 Java 语言,从零实现一个线程安全的简单轮询负载均衡器,帮助大家深入理解基本原理,并为后续优化(如加权轮询、健康检查等)铺垫基础。
2. 项目需求详细介绍
-
功能需求
-
支持动态添加、删除后端服务器实例;
-
按“轮询”策略,将请求依次分发给各实例;
-
支持多线程并发调用,保证调度结果正确。
-
-
非功能需求
-
性能:在节点数量≤50、并发线程数≤2000 的情况下,调度延迟应在微秒级;
-
线程安全:在高并发场景下,无重复选中或漏选;
-
可扩展性:后续可接入权重、健康检查、优先级等功能;
-
易用性:API 简洁,上手成本低;
-
可测试性:附带单元测试案例,验证轮询顺序。
-
3. 相关技术详细介绍
-
Java 基础
-
List 集合:保存服务器列表,支持 add、remove、get 操作;
-
原子变量:
AtomicInteger或AtomicLong记录当前索引,保证并发安全; -
锁机制:在复杂场景下可选用
ReentrantLock; -
并发工具:如 JUnit +
ExecutorService模拟高并发测试。
-
-
轮询算法原理
-
顺序遍历:维护一个全局索引
currentIndex,每次调度前做currentIndex = (currentIndex + 1) % size; -
循环复用:当索引达到末尾,自动回绕到列表头部。
-
-
线程安全策略
-
无锁方案:使用
AtomicInteger做自增取模,避免锁竞争; -
有锁方案:在列表变更(增删节点)或调度过程中加锁,保证状态一致。
-
-
测试方法
-
使用 JUnit 编写多线程测试,验证在并发环境下输出的节点序列与预期轮询顺序一致。
-
4. 实现思路详细介绍
4.1 数据模型
-
定义
class ServerNode表示后端实例,包含唯一标识(如id、address等); -
维护
List<ServerNode>作为可用节点列表。
4.2 核心调度逻辑
-
初始化
-
currentIndex初始为-1; -
节点列表可为空,调度时应返回
null或抛出异常。
-
-
轮询选取
-
每次调用
selectNode():-
先获取列表长度
n; -
通过原子操作:
int idx = (atomicIndex.incrementAndGet() & Integer.MAX_VALUE) % n; -
返回
nodes.get(idx)。
-
-
-
并发安全
-
增删节点:在写操作时使用
synchronized块或写锁,确保列表修改与读取互斥; -
取模计算:使用原子变量无锁实现,实现高吞吐;
-
边界处理:动态增删节点后,
atomicIndex可能大于新列表长度,通过取模自动回绕。
-
4.3 API 设计
public class RoundRobinLoadBalancer {
public void addNode(ServerNode node);
public void removeNode(String nodeId);
public ServerNode selectNode();
public List<ServerNode> listNodes();
}
-
addNode、removeNode:在写锁保护下修改列表; -
selectNode:无锁读取列表长度并通过原子自增取模选取; -
listNodes:返回当前活跃节点快照,用于监控或调试。
4.4 扩展思考
-
自恢复索引:当
atomicIndex达到Integer.MAX_VALUE临界,可在低峰期重置; -
快速失败:若列表空,可直接抛出异常或 fallback 到默认节点;
-
日志监控:在选取过程中记录节点命中日志,便于后期分析流量分布。
5. 完整实现代码
// ==================== 文件:ServerNode.java ====================
package com.example.loadbalancer;
/**
* 表示一个后端服务器节点
*/
public class ServerNode {
/** 节点唯一标识 */
private final String id;
/** 节点地址或其它信息 */
private final String address;
public ServerNode(String id, String address) {
this.id = id;
this.address = address;
}
public String getId() {
return id;
}
public String getAddress() {
return address;
}
@Override
public String toString() {
return "ServerNode{id='" + id + "', address='" + address + "'}";
}
}
// ==================== 文件:RoundRobinLoadBalancer.java ====================
package com.example.loadbalancer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* 轮询(Round Robin)负载均衡器实现
*/
public class RoundRobinLoadBalancer {
/** 活跃的服务器列表 */
private final List<ServerNode> nodes = new ArrayList<>();
/** 原子自增索引,保证并发安全 */
private final AtomicLong index = new AtomicLong(0);
/**
* 增加一个服务器节点
* @param node 新增的节点
*/
public synchronized void addNode(ServerNode node) {
nodes.add(node);
}
/**
* 删除一个服务器节点
* @param nodeId 要删除的节点ID
*/
public synchronized void removeNode(String nodeId) {
nodes.removeIf(n -> n.getId().equals(nodeId));
}
/**
* 列出当前所有节点的快照
* @return 节点列表快照
*/
public synchronized List<ServerNode> listNodes() {
return new ArrayList<>(nodes);
}
/**
* 执行一次轮询选取,返回被选中的节点
* @return 本次调度选中的 ServerNode;若无节点则返回 null
*/
public ServerNode selectNode() {
List<ServerNode> snapshot;
synchronized (this) {
if (nodes.isEmpty()) {
return null;
}
snapshot = new ArrayList<>(nodes);
}
// 原子自增并取模
long current = index.getAndIncrement();
int idx = (int) (Math.floorMod(current, snapshot.size()));
return snapshot.get(idx);
}
}
// ==================== 文件:TestRoundRobin.java ====================
package com.example.loadbalancer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
/**
* JUnit 单元测试,验证轮询顺序
*/
public class TestRoundRobin {
private RoundRobinLoadBalancer lb;
@Before
public void setup() {
lb = new RoundRobinLoadBalancer();
lb.addNode(new ServerNode("A", "192.168.0.1"));
lb.addNode(new ServerNode("B", "192.168.0.2"));
lb.addNode(new ServerNode("C", "192.168.0.3"));
}
@Test
public void testSequence() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++) {
ServerNode node = lb.selectNode();
sb.append(node.getId());
}
// 期望顺序:ABCABC
Assert.assertEquals("ABCABC", sb.toString());
}
@Test
public void testConcurrent() throws Exception {
// 模拟多线程并发选取,确保无异常、顺序整体符合轮询模式
int threads = 50;
int callsPerThread = 100;
StringBuilder sb = new StringBuilder();
java.util.concurrent.ExecutorService exec = java.util.concurrent.Executors.newFixedThreadPool(threads);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(threads);
for (int t = 0; t < threads; t++) {
exec.submit(() -> {
for (int i = 0; i < callsPerThread; i++) {
ServerNode node = lb.selectNode();
synchronized (sb) {
sb.append(node.getId());
}
}
latch.countDown();
});
}
latch.await();
exec.shutdown();
// 这里只验证总长度正确即可
Assert.assertEquals(threads * callsPerThread, sb.length());
}
}
6. 代码详细解读
-
ServerNode.java
-
ServerNode(String id, String address):构造方法,初始化节点的唯一 ID 和地址信息。 -
toString():打印节点信息,便于调试时输出。
-
-
RoundRobinLoadBalancer.java
-
addNode(ServerNode)/removeNode(String)/listNodes():在synchronized保护下对nodes列表进行增删改查,保证修改的线程安全。 -
selectNode():-
在
synchronized块中生成列表快照,避免在计算时列表被并发修改; -
使用
AtomicLong index做无锁自增; -
通过
Math.floorMod(current, size)取模,避免负值并正确回绕; -
从快照列表中按序取出对应节点并返回。
-
-
-
TestRoundRobin.java
-
testSequence():验证在单线程调用下,节点按照 “A→B→C→A→B→C” 的循环顺序分配; -
testConcurrent():使用线程池模拟高并发场景,批量调用selectNode(),主要验证在并发下无抛出异常且总调用次数正确。
-
7. 项目详细总结
本项目通过 Java 语言实现了最基础的轮询(Round Robin)负载均衡算法,核心特点如下:
-
简单高效:利用
AtomicLong无锁自增和取模操作,实现毫秒级甚至微秒级的调度; -
线程安全:列表修改与读取分离,写操作加锁,读操作无锁,仅保留快照,兼顾安全与性能;
-
易于扩展:在此基础上可接入加权、健康检查、故障剔除等高级功能;
-
可测试性强:附带单元测试,验证顺序和并发场景下的正确性。
8. 项目常见问题及解答
Q1:为何要生成快照再取模?
A:避免在 selectNode() 的计算过程中,另一线程同时执行 addNode() 或 removeNode() 导致 nodes 列表变化,保证索引和列表长度的一致性。
Q2:使用 AtomicLong 而非 AtomicInteger 的考虑?
A:请求量极大时,AtomicInteger 达到上限后会回到负数,需额外处理;AtomicLong 范围更大,且通过 floorMod 可正确取模。
Q3:当没有节点时如何处理?
A:当前实现返回 null,可根据业务需求抛出自定义异常或返回默认节点。
Q4:索引何时重置?
A:本示例未主动重置 index,在长期运行且调用次数极多下可监控 index 值,在低峰期调用 index.set(index % size) 进行重置。
9. 扩展方向与性能优化
-
无锁节点列表
-
使用
CopyOnWriteArrayList或读写分离的并发集合,减少synchronized范围;
-
-
支持加权轮询
-
在本轮询框架上加权,或直接集成前文提到的“平滑加权轮询”算法;
-
-
健康检查集成
-
定时探测实例可用性,自动上下线,提高可用性;
-
-
监控与告警
-
接入 Prometheus/Grafana,实时监控各节点的命中次数、响应延迟,并在异常时告警;
-
-
分布式协同
-
在多台负载均衡器之间同步节点列表,确保一致性;
-
-
优先级与隔离
-
支持不同优先级的请求流量,或不同流量类型的隔离调度。
-
更多推荐



所有评论(0)