上一篇 《Dubbo 注册中心与直连调试:只订阅、只注册、多注册中心》 讲了怎么连服务。这一篇讲流量分发:一个服务有多个 Provider 节点时,Consumer 把请求发给谁?——这就是 Dubbo 的负载均衡策略(LoadBalance)。
四种策略一览
Dubbo 内置四种负载均衡策略,接口 LoadBalance 的 select 方法负责从 List<Invoker> 中选出一个:
| 策略 | 类名 | 原理 | 特点 |
|---|---|---|---|
| 按权重随机 | RandomLoadBalance | 按权重随机概率 | 默认策略,最常用 |
| 轮询 | RoundRobinLoadBalance | 按顺序轮流 | 均摊,但有慢节点时会”打满” |
| 最少活跃数 | LeastActiveLoadBalance | 活跃数最少的优先 | 自动避让慢节点 |
| 一致性哈希 | ConsistentHashLoadBalance | 相同参数落同一节点 | 无状态节点缓存友好 |
<!-- 消费方指定负载均衡策略 -->
<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="leastactive"/>
<!-- 注解方式 -->
@Reference(loadbalance = "leastactive")
private UserService userService;
Random:按权重随机(默认)
假设有 4 个集群节点 A、B、C、D,对应的权重分别是 1、2、3、4,总权重为 10(1+2+3+4)。怎么做到按权重随机呢?
根据 10 随机出一个整数,然后依次和权重相减:
随机数为 2:
2 - A的权重(1) = 1
1 - B的权重(2) = -1 ← 变成负数,命中 B
随机数为 7:
7 - A(1) = 6
6 - B(2) = 4
4 - C(3) = 1
1 - D(4) = -3 ← 命中 D
权重越大,被随机命中的区间越大——这就是”按权重随机”的本质:把总权重当成一条线段,随机落点落在哪个节点的区间,就选谁。
源码分析

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 总个数
int totalWeight = 0; // 总权重
boolean sameWeight = true; // 权重是否都一样
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // 累计总权重
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false; // 计算所有权重是否一样
}
}
if (totalWeight > 0 && !sameWeight) {
// 如果权重不相同且权重大于 0,则按总权重数随机
int offset = random.nextInt(totalWeight);
// 并确定随机值落在哪个片段上
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果权重相同或权重为 0,则均等随机
return invokers.get(random.nextInt(length));
}
算法三步:
- 第一遍循环:累加总权重
totalWeight,同时判断所有节点权重是否相同。 - 权重不同:
random.nextInt(totalWeight)生成[0, totalWeight)的随机数,依次减去各节点权重,第一个使 offset 变负的节点被选中。 - 权重相同或为 0:退化为
random.nextInt(length)的均等随机。
权重通过
getWeight()获取,默认权重 100;可以在注册中心动态调整节点权重(如给新扩容的机器临时调低权重做灰度)。
RoundRobin:轮询
按顺序轮流调用每个 Provider,权重高的节点被轮到的次数多。
节点 A(权重1) B(权重2)
第 1 次调用 → A
第 2 次调用 → B
第 3 次调用 → B
第 4 次调用 → A
第 5 次调用 → B
第 6 次调用 → B
优点:请求均匀分布,实现简单。 缺点:如果某节点处理慢,请求会排队堆积——因为轮询不管节点当前忙不忙,照样平均分配。这是它后来被很多场景弃用的原因。
LeastActive:最少活跃数
每个服务有一个活跃计数器。假如有 A、B 两个提供者,计数均为 0。当 A 提供者开始处理请求,该计数 +1(此时 A 还没处理完);处理完后计数 -1。而 B 请求接收后处理得很快,B 处理完时 A 还没处理完,所以此时 A、B 的计数为 1、0。
那么当有新的请求来的时候,就会选择 B 提供者(B 的活跃计数比 A 小)。
节点 活跃数 状态
A 1 正在处理一个慢请求
B 0 空闲
→ 新请求选择 B(活跃数最小)
活跃数 = 正在处理中的请求数。活跃数越少说明节点越闲,新请求优先打给最闲的节点——天然避让慢节点。
源码分析

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 总个数
int leastActive = -1; // 最小的活跃数
int leastCount = 0; // 相同最小活跃数的个数
int[] leastIndexes = new int[length]; // 相同最小活跃数的下标
int totalWeight = 0; // 总权重
int firstWeight = 0; // 第一个权重,用于计算是否相同
boolean sameWeight = true; // 是否所有权重相同
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
int active = RpcStatus.getStatus(invoker.getUrl(),
invocation.getMethodName()).getActive(); // 活跃数
int weight = invoker.getUrl().getMethodParameter(
invocation.getMethodName(),
Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重
if (leastActive == -1 || active < leastActive) {
// 发现更小的活跃数,重新开始
leastActive = active; // 记录最小活跃数
leastCount = 1; // 重新统计相同最小活跃数的个数
leastIndexes[0] = i; // 重新记录最小活跃数下标
totalWeight = weight; // 重新累计总权重
firstWeight = weight; // 记录第一个权重
sameWeight = true; // 还原权重相同标识
} else if (active == leastActive) {
// 累计相同最小的活跃数
leastIndexes[leastCount++] = i; // 累计相同最小活跃数下标
totalWeight += weight; // 累计总权重
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false; // 判断所有权重是否一样
}
}
}
if (leastCount == 1) {
// 如果只有一个最小则直接返回
return invokers.get(leastIndexes[0]);
}
if (!sameWeight && totalWeight > 0) {
// 如果权重不相同且权重大于 0,则按总权重数随机
int offsetWeight = random.nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
// 如果权重相同或权重为 0,则均等随机
return invokers.get(leastIndexes[random.nextInt(leastCount)]);
}
算法逻辑分五步:
- 遍历所有节点:通过
RpcStatus.getStatus(...).getActive()拿到每个节点的活跃数,同时取权重。 - 找出最小活跃数:
active < leastActive时更新最小值;active == leastActive时把下标收进leastIndexes数组。 - 只有一个最小活跃节点:直接返回它。
- 多个最小活跃节点且权重不同:在最小活跃节点内部,按权重随机(复用 Random 的线段算法)。
- 权重相同或为 0:在最小活跃节点内部均等随机。
关键点:LeastActive 是两级选择——第一级找最闲的节点,第二级在同样闲的节点里按权重挑。笔记里的例子”A、B 权重 1、2,则 A 概率 1/3、B 概率 2/3”说的就是第二级。
ConsistentHash:一致性哈希
一致性哈希解决的是”相同参数的请求固定打到同一节点”——对无状态的节点缓存(如本地缓存)特别友好。
原理(摘自当年笔记):
假如有 N 个真实节点,把每个真实节点映射成 M 个虚拟节点,再把 M×N 个虚拟节点散列在圆环上。各真实节点对应的虚拟节点相互交错分布,这样某真实节点 down 后,则把其影响平均分担到其他所有节点上。
也就是 a、b、c、d 的虚拟节点
a0,a1,a2、b0,b1,b2、c0,c1,c2、d0,d1,d2散落在圆环上。假设 C 号节点 down,则c0,c1,c2的压力分别传给d0,a1,b1,如下图:

一致性哈希的流程
1. 每个真实节点生成 M 个虚拟节点(如每个节点 160 个)
2. 所有虚拟节点按哈希值散列到圆环(0 ~ 2^32-1)上
3. 请求参数做同样的哈希,落到环上某点
4. 顺时针找到第一个虚拟节点,即目标节点
5. 节点 down 时,只有其环上"后继区间"的请求受影响
(转移给下一个虚拟节点),其他节点不受影响
为什么需要虚拟节点
没有虚拟节点时,真实节点在环上分布不均匀,会导致数据倾斜(某些节点负载过高)。引入虚拟节点后:
- 分布更均匀:每个真实节点对应多个虚拟节点,交错分布在环上。
- 故障影响均摊:某个真实节点 down,它的多个虚拟节点的压力分别转移给不同的相邻节点——如笔记图中 C 的
c0→b1、c1→d0、c2→a1,而不是全部压到一个节点。
参数维度
一致性哈希默认按第一个参数做哈希(hash.arguments 可配置),相同参数的请求会落到同一个节点:
<dubbo:reference interface="com.example.UserService"
loadbalance="consistenthash"/>
典型场景:
getUserById(userId)这类按 ID 查询的接口,同一 userId 的请求固定打到同一节点,可以利用该节点上的本地缓存,减少数据库压力。
四种策略选型建议
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 默认、通用场景 | Random | 权重随机,简单可靠,默认就是它 |
| 节点处理速度差异大 | LeastActive | 自动避让慢节点,防止请求堆积 |
| 有本地缓存、按 key 查询 | ConsistentHash | 相同 key 固定同节点,缓存命中率高 |
| 无状态、要求绝对均匀 | RoundRobin | 轮询均摊(注意慢节点问题) |
小结
- Random:按权重随机,默认策略;算法是”总权重线段随机落点”。
- RoundRobin:轮询均摊,但慢节点会堆积请求。
- LeastActive:两级选择——先找活跃数最小,再按权重随机;天然避让慢节点。
- ConsistentHash:虚拟节点散列圆环,相同参数固定同节点,down 节点压力均摊。
下一篇 《Dubbo 集群容错:架构、五种模式与服务暴露》 讲服务调用失败时怎么办。