Java:实现负载均衡加权轮询算法(附带源码)
1. 项目背景详细介绍
在分布式系统和高并发架构中,如何将请求均匀且有策略地分配到后端服务器,是提升系统吞吐量与可靠性的重要手段。传统的轮询(Round Robin)算法在权重相同的场景下能够实现基本的负载分担,但面对不同性能、不同规格的服务器时,往往无法充分利用资源。
因此,引入“加权轮询”(Weighted Round Robin, WRR)算法:为每台服务器分配权重(weight),权重越高,获取请求的概率和频率越大;权重越低,则反之。这样既能保证各节点有请求落地,又能让性能更强、更空闲的节点承担更多负载,从而提升系统整体性能和稳定性。
本项目旨在使用 Java 语言,从零构建一个简单易用的 WRR 负载均衡实现,帮助大家学习并掌握该算法的原理与实现细节,便于在实际业务中进行二次开发和扩展。
2. 项目需求详细介绍
-
功能需求
-
能够动态添加、删除后端服务器实例;
-
为每个服务器设置可调节的权重值;
-
根据加权轮询算法,均匀地将请求分配给后端服务器;
-
支持多线程并发调用,保证线程安全。
-
-
非功能需求
-
性能:在节点数量≤20、并发线程数≤1000 的情况下,算法的调度开销应当可接受;
-
可扩展性:后期可接入健康检查、权重动态调整等功能;
-
易用性:API 简洁易懂,上手成本低;
-
可测试性:附带单元测试示例,验证分配结果的正确性。
-
3. 相关技术详细介绍
-
Java 基础
-
集合框架:如
List、Map等,用于存储服务器列表与权重映射; -
并发包:如
ReentrantLock、AtomicInteger等,保证多线程环境下的安全。
-
-
加权轮询算法原理
-
静态权重轮询:简单地根据固定权重计数循环分配;
-
平滑加权轮询(Smooth WRR):维护每个节点的动态“当前权重”,每次从中选择当前权重大者,并在分配后减少总权重,实现更均匀的调度效果。
-
-
单元测试
-
使用 JUnit 框架验证调度结果符合预期。
-
-
日志与监控(可选)
-
日志框架(如 Log4j、SLF4J)记录调度决策过程;
-
指标上报(如 Prometheus)监控调度性能。
-
4. 实现思路详细介绍
4.1 数据结构设计
-
使用
class ServerNode表示后端服务器,包含字段:id、weight(静态权重)、currentWeight(动态权重)等。 -
使用
List<ServerNode>存储活跃服务器列表。
4.2 算法流程(平滑加权轮询)
-
初始化
-
将所有
ServerNode.currentWeight置零; -
计算所有权重之和
totalWeight。
-
-
每次调度
-
遍历所有节点,执行
node.currentWeight += node.weight; -
从中选择
currentWeight最大的节点selected; -
对
selected.currentWeight执行selected.currentWeight -= totalWeight; -
返回
selected。
-
该算法保证在一轮调度中,高权重节点被选中的次数成比例增加,同时“平滑”地分散请求,避免权重不等时短期内请求聚集到某些节点。
4.3 并发安全
-
读写分离:采用读写锁(
ReentrantReadWriteLock)或在简单场景下使用synchronized对关键方法加锁; -
原子变量:如
AtomicInteger管理totalWeight,防止并发更新冲突。
4.4 扩展与优化点
-
健康检查集成:定期探测节点健康状况,剔除失效节点;
-
动态权重调整:根据节点负载、响应时间等动态调整
weight; -
缓存结果:多线程高并发时,可做短期结果缓存,降低锁竞争。
5. 完整实现代码
// ==================== 文件:ServerNode.java ====================
package com.example.loadbalancer;
/**
* 表示一个后端服务器节点
*/
public class ServerNode {
/** 节点唯一标识 */
private final String id;
/** 静态权重:初始化时配置,不随调度变化 */
private final int weight;
/** 动态权重:每次调度时累加,用于平滑加权轮询 */
private int currentWeight;
public ServerNode(String id, int weight) {
this.id = id;
this.weight = weight;
this.currentWeight = 0;
}
public String getId() {
return id;
}
public int getWeight() {
return weight;
}
public int getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(int currentWeight) {
this.currentWeight = currentWeight;
}
@Override
public String toString() {
return "ServerNode{id='" + id + "', weight=" + weight + ", currentWeight=" + currentWeight + '}';
}
}
// ==================== 文件:WeightedRoundRobinLoadBalancer.java ====================
package com.example.loadbalancer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 平滑加权轮询负载均衡器实现
*/
public class WeightedRoundRobinLoadBalancer {
/** 活跃的服务器列表 */
private final List<ServerNode> nodes = new ArrayList<>();
/** 所有节点静态权重之和 */
private int totalWeight = 0;
/** 读写锁保证并发安全 */
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 增加一个服务器节点
* @param node 新增的节点
*/
public void addNode(ServerNode node) {
lock.writeLock().lock();
try {
nodes.add(node);
totalWeight += node.getWeight();
} finally {
lock.writeLock().unlock();
}
}
/**
* 删除一个服务器节点
* @param nodeId 要删除的节点ID
*/
public void removeNode(String nodeId) {
lock.writeLock().lock();
try {
nodes.removeIf(n -> n.getId().equals(nodeId));
// 重算总权重
totalWeight = nodes.stream().mapToInt(ServerNode::getWeight).sum();
} finally {
lock.writeLock().unlock();
}
}
/**
* 执行一次平滑加权轮询,返回被选中的节点
* @return 本次调度选中的 ServerNode
*/
public ServerNode selectNode() {
lock.writeLock().lock();
try {
if (nodes.isEmpty()) {
return null;
}
ServerNode selected = null;
for (ServerNode node : nodes) {
// 增加当前权重
node.setCurrentWeight(node.getCurrentWeight() + node.getWeight());
// 选出当前权重大者
if (selected == null || node.getCurrentWeight() > selected.getCurrentWeight()) {
selected = node;
}
}
// 减去总权重,实现平滑效果
if (selected != null) {
selected.setCurrentWeight(selected.getCurrentWeight() - totalWeight);
}
return selected;
} finally {
lock.writeLock().unlock();
}
}
/**
* 打印当前所有节点状态,用于调试
*/
public void printStatus() {
lock.readLock().lock();
try {
System.out.println("Total weight = " + totalWeight);
for (ServerNode node : nodes) {
System.out.println(node);
}
} finally {
lock.readLock().unlock();
}
}
}
// ==================== 文件:TestWeightedRoundRobin.java ====================
package com.example.loadbalancer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
/**
* JUnit 单元测试,验证加权轮询分布比例
*/
public class TestWeightedRoundRobin {
private WeightedRoundRobinLoadBalancer lb;
@Before
public void setup() {
lb = new WeightedRoundRobinLoadBalancer();
// 添加三个节点,权重分别为 5、1、1
lb.addNode(new ServerNode("A", 5));
lb.addNode(new ServerNode("B", 1));
lb.addNode(new ServerNode("C", 1));
}
@Test
public void testDistribution() {
Map<String, Integer> countMap = new HashMap<>();
countMap.put("A", 0);
countMap.put("B", 0);
countMap.put("C", 0);
int totalRequests = 7 * 100; // 模拟 700 次请求
for (int i = 0; i < totalRequests; i++) {
ServerNode node = lb.selectNode();
countMap.put(node.getId(), countMap.get(node.getId()) + 1);
}
// A 应获得约 500 次,B、C 各约 100 次
System.out.println(countMap);
Assert.assertTrue(Math.abs(countMap.get("A") - 500) < 50);
Assert.assertTrue(Math.abs(countMap.get("B") - 100) < 20);
Assert.assertTrue(Math.abs(countMap.get("C") - 100) < 20);
}
}
6. 代码详细解读
-
ServerNode.java
-
ServerNode(String id, int weight):构造方法,初始化节点 ID、静态权重,并将动态权重置零。 -
getCurrentWeight()/setCurrentWeight(int):获取和设置当前动态权重,用于轮询算法计算。 -
toString():输出节点状态,包含 ID、静态权重和当前权重,方便调试。
-
-
WeightedRoundRobinLoadBalancer.java
-
addNode(ServerNode):在写锁保护下将新节点添加到列表,并更新总权重。 -
removeNode(String):删除指定 ID 的节点,重算总权重,保持一致性。 -
selectNode():核心调度方法(写锁保护):-
遍历所有节点,累加
currentWeight += weight; -
选出
currentWeight最大的节点; -
对选中节点做
currentWeight -= totalWeight; -
返回选中节点。
-
-
printStatus():读锁保护,打印所有节点及总权重信息,用于在线或本地调试。
-
-
TestWeightedRoundRobin.java
-
使用 JUnit 框架进行单元测试;
-
在
setup()方法中预置三节点; -
testDistribution()模拟大量请求,统计各节点命中次数,并断言比例在容忍范围内。
-
7. 项目详细总结
本项目通过 Java 语言完整实现了“平滑加权轮询”负载均衡算法,具有以下特点:
-
简洁明了:核心算法仅几十行,易于理解;
-
线程安全:利用读写锁保护,适应多线程高并发场景;
-
易扩展:节点管理(增删)与调度分离,后续可接入健康检查、动态权重;
-
可测试:单元测试覆盖,验证分配比例,增强信心。
通过本实现,读者可以深刻理解加权轮询原理,并在实际业务中快速接入或二次开发,以满足不同场景需求。
8. 项目常见问题及解答
Q1:为什么要使用平滑加权而非简单加权?
A:简单加权在短时间内可能产生请求聚集;平滑加权通过 currentWeight 动态调整,保证请求在整个调度周期内均匀分散。
Q2:多线程场景会不会影响 currentWeight 计算准确性?
A:本实现对 selectNode() 加写锁,保证一次调度完整原子;读操作仅打印状态使用读锁,不影响性能。
Q3:如何在运行时动态调整某个节点的权重?
A:可先使用 removeNode() 移除旧节点,再 addNode() 添加新权重节点;或扩展接口直接修改 ServerNode.weight 并同步更新 totalWeight。
Q4:节点宕机时如何剔除?
A:可结合健康检查模块,定期探测节点可用性,在线调用 removeNode() 剔除失效节点。
9. 扩展方向与性能优化
-
健康检查集成
-
定时 HTTP/TCP 探针,自动上下线节点;
-
对失败率、延迟等指标加权,动态调整权重。
-
-
动态权重调整
-
实时监控节点的 CPU、内存、响应时间;
-
根据负载情况自动增减
weight,实现自适应调度。
-
-
无锁优化
-
对
selectNode()使用原子变量和无锁算法(如 CAS)替代读写锁,减少线程争用; -
或基于
StampedLock实现乐观读写。
-
-
高可用部署
-
将负载均衡器自身做成集群,通过一致性协议(如 Raft)同步节点列表;
-
支持滚动升级、灰度发布。
-
-
分布式追踪与监控
-
集成 OpenTelemetry,采集每次请求分配路径;
-
在 Prometheus + Grafana 查看各节点的调用次数、延迟分布。
-
更多推荐

所有评论(0)