一、核心概念负载均衡Load Balancing是分布式系统中的核心机制用于在多个服务提供者之间分配请求流量。1. 为什么需要负载均衡在RPC框架中一个服务可能有多个提供者节点提高可用性某个节点故障时请求可以转发到其他节点提升性能将请求分散到多个节点避免单点过载支持扩展可以动态增加服务节点来应对流量增长。2. 负载均衡与容错的区别负载均衡在正常情况下如何选择一个最优的服务节点容错机制在异常情况下如何处理失败的请求。两者配合使用构成完整的服务调用保障体系。3. 负载均衡器接口设计public interface LoadBalancer { ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList); }接口说明requestParams请求参数可用于某些算法如一致性哈希serviceMetaInfoList可用的服务节点列表返回值选中的服务节点。二、随机算法Random1. 算法原理从服务列表中随机选择一个节点。2. 实现代码public class RandomLoadBalancer implements LoadBalancer { private final Random random new Random(); Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { int size serviceMetaInfoList.size(); if (size 0) { return null; } if (size 1) { return serviceMetaInfoList.get(0); } return serviceMetaInfoList.get(random.nextInt(size)); } }3. 算法特点优点实现简单分布均匀长期来看缺点短期内可能不均匀无法保证顺序适用场景服务节点性能相近无状态服务。三、轮询算法Round Robin1. 算法原理按顺序依次选择服务节点循环往复。2. 实现代码public class RoundRobinLoadBalancer implements LoadBalancer { private final AtomicInteger currentIndex new AtomicInteger(0); Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { if (serviceMetaInfoList.isEmpty()) { return null; } int size serviceMetaInfoList.size(); if (size 1) { return serviceMetaInfoList.get(0); } // 取模算法轮询 int index currentIndex.getAndIncrement() % size; return serviceMetaInfoList.get(index); } }3. AtomicInteger 详解为什么使用 AtomicInteger在多线程环境下普通的 int 类型不是线程安全的// 错误示例线程不安全 private int currentIndex 0; int index currentIndex % size; // 可能导致多个线程获得相同的indexAtomicInteger 如何保证线程安全// 正确示例线程安全 private final AtomicInteger currentIndex new AtomicInteger(0); int index currentIndex.getAndIncrement() % size;getAndIncrement() 方法是原子操作获取当前值将值加1返回原始值。这三步在底层通过 CASCompare-And-Swap机制保证原子性不会被其他线程打断。4. 执行示例假设有3个服务节点[Node-A, Node-B, Node-C]请求1: currentIndex0 → 0%30 → 选择 Node-A → currentIndex变为1 请求2: currentIndex1 → 1%31 → 选择 Node-B → currentIndex变为2 请求3: currentIndex2 → 2%32 → 选择 Node-C → currentIndex变为3 请求4: currentIndex3 → 3%30 → 选择 Node-A → currentIndex变为4 ...循环往复5. 算法特点优点分布均匀实现简单可预测缺点不考虑节点性能差异适用场景服务节点性能相近需要均匀分配。四、一致性哈希算法Consistent Hash1. 算法原理一致性哈希是一种特殊的哈希算法主要解决分布式系统中节点动态增减时的数据重新分配问题。2. 核心概念哈希环Hash Ring将哈希值空间0 ~ 2^32-1组织成一个虚拟的环形结构。虚拟节点Virtual Nodes为每个真实节点创建多个虚拟节点提高分布均匀性。真实节点: 192.168.1.1:8080 虚拟节点: - 192.168.1.1:8080#0 → hash值: 12345 - 192.168.1.1:8080#1 → hash值: 67890 - 192.168.1.1:8080#2 → hash值: 23456 ...共100个虚拟节点3. 实现代码public class ConsistentHashLoadBalancer implements LoadBalancer { // TreeMap 自动按 key 排序用于实现哈希环 private final TreeMapInteger, ServiceMetaInfo virtualNodes new TreeMap(); private static final int VIRTUAL_NODE_NUM 100; // 记录上次的服务节点列表用于判断是否需要重建哈希环 private ListServiceMetaInfo lastServiceList null; Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { if (serviceMetaInfoList.isEmpty()) { return null; } // 只有节点列表变化时才重建哈希环 if (lastServiceList null || !isSameServiceList(lastServiceList, serviceMetaInfoList)) { buildConsistentHashRing(serviceMetaInfoList); lastServiceList new ArrayList(serviceMetaInfoList); } // 获取请求的 hash 值 int hash getHash(requestParams); // 选择最接近且大于等于请求 hash 值的虚拟节点 Map.EntryInteger, ServiceMetaInfo entry virtualNodes.ceilingEntry(hash); if (entry null) { // 如果没有大于等于的节点返回环首部节点形成环状 entry virtualNodes.firstEntry(); } return entry.getValue(); } /** * 构建一致性哈希环 */ private void buildConsistentHashRing(ListServiceMetaInfo serviceMetaInfoList) { // 清空旧的虚拟节点 virtualNodes.clear(); // 为每个服务节点创建虚拟节点 for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) { for (int i 0; i VIRTUAL_NODE_NUM; i) { int hash getHash(serviceMetaInfo.getServiceAddress() # i); virtualNodes.put(hash, serviceMetaInfo); } } } /** * 判断服务列表是否相同 */ private boolean isSameServiceList(ListServiceMetaInfo list1, ListServiceMetaInfo list2) { if (list1.size() ! list2.size()) { return false; } // 比较服务地址集合 SetString set1 list1.stream() .map(ServiceMetaInfo::getServiceAddress) .collect(Collectors.toSet()); SetString set2 list2.stream() .map(ServiceMetaInfo::getServiceAddress) .collect(Collectors.toSet()); return set1.equals(set2); } private int getHash(Object key) { return key.hashCode(); } }4. TreeMap 和 ceilingEntry() 详解TreeMap 是什么TreeMap 是一个有序的 Map内部使用红黑树实现key 会自动按照自然顺序排序。TreeMapInteger, String map new TreeMap(); map.put(100, A); map.put(50, B); map.put(200, C); // 内部顺序50→100→200ceilingEntry() 方法ceilingEntry(key) 返回大于或等于给定 key 的最小 Entry。TreeMapInteger, String map new TreeMap(); map.put(10, A); map.put(20, B); map.put(30, C); map.ceilingEntry(15); // 返回 (20, B) map.ceilingEntry(20); // 返回 (20, B) map.ceilingEntry(35); // 返回 null5. 一致性哈希执行流程假设有2个服务节点每个节点100个虚拟节点步骤1构建哈希环仅在节点列表变化时执行Node-A (192.168.1.1:8080): - 192.168.1.1:8080#0 → hash: 1234 - 192.168.1.1:8080#1 → hash: 5678 - ... - 192.168.1.1:8080#99 → hash: 9999 Node-B (192.168.1.2:8080): - 192.168.1.2:8080#0 → hash: 2345 - 192.168.1.2:8080#1 → hash: 6789 - ... - 192.168.1.2:8080#99 → hash: 8888TreeMap 自动排序后1234→Node-A, 2345→Node-B, 5678→Node-A, 6789→Node-B, ...步骤2计算请求哈希值requestParams {method: getUser, userId: 123} hash requestParams.hashCode() 45678步骤3查找节点// 在 TreeMap 中查找 45678 的最小 key entry virtualNodes.ceilingEntry(45678); // 假设找到 key50000对应 Node-B return Node-B;6. virtualNodes 的生命周期所有请求共用同一个 virtualNodes。由于 SpiLoader 使用单例模式ConsistentHashLoadBalancer 只会创建一个实例// 所有请求共用同一个负载均衡器实例 LoadBalancer lb LoadBalancerFactory.getInstance(consistentHash); // 因此 virtualNodes 也是共享的 private final TreeMapInteger, ServiceMetaInfo virtualNodes new TreeMap();执行流程演示请求1: serviceMetaInfoList [A, B, C] → lastServiceList null → 构建哈希环: virtualNodes {A的100个, B的100个, C的100个} → 保存 lastServiceList [A, B, C] → 选择节点 请求2: serviceMetaInfoList [A, B, C] → 检测到节点列表未变化isSameServiceList 返回 true → 不重建哈希环直接使用现有 virtualNodes → 选择节点性能高 请求3: serviceMetaInfoList [A, B, C] → 节点列表仍未变化 → 继续使用现有 virtualNodes → 选择节点 (此时发生节点上线: [A, B, C, D]) 请求4: serviceMetaInfoList [A, B, C, D] → 检测到节点列表变化多了 D → 清空 virtualNodes → 重建哈希环: virtualNodes {A的100个, B的100个, C的100个, D的100个} → 保存 lastServiceList [A, B, C, D] → 选择节点 请求5: serviceMetaInfoList [A, B, C, D] → 节点列表未变化 → 使用现有 virtualNodes → 选择节点7. 为什么需要虚拟节点没有虚拟节点的问题假设只有2个真实节点Node-A hash: 1000 Node-B hash: 9000哈希环上只有2个点分布极不均匀hash值在 1000~9000 之间的请求全部打到 Node-Bhash值在 9000~1000 之间的请求全部打到 Node-A。使用虚拟节点后每个节点有100个虚拟节点哈希环上有200个点分布更均匀1234→A, 2345→B, 3456→A, 4567→B, 5678→A, 6789→B, ...这样请求会更均匀地分配到各个节点。8. 一致性哈希的优势场景节点动态变化假设有3个节点 [A, B, C]使用普通哈希index hash(request) % 3; // 节点数3当增加一个节点D后index hash(request) % 4; // 节点数4几乎所有请求的目标节点都会改变一致性哈希的优势增加或删除节点时只影响哈希环上相邻的部分节点大部分请求的目标节点不变。具体示例初始状态3个节点 [A, B, C] 哈希环简化 0 ----A(1000)---- B(5000) ----C(8000)---- 10000(环回到A) 请求1 (hash3000) → ceilingEntry(3000) → B (第一个 3000 的节点) 请求2 (hash7000) → ceilingEntry(7000) → C (第一个 7000 的节点) 请求3 (hash9000) → ceilingEntry(9000) → A (没有 9000 的返回首节点) 删除 Node-B 后哈希环变为 0 ----A(1000)---- C(8000)---- 10000(环回到A) 请求1 (hash3000) → ceilingEntry(3000) → C (改变B删除顺延到C) 请求2 (hash7000) → ceilingEntry(7000) → C (不变仍然是C) 请求3 (hash9000) → ceilingEntry(9000) → A (不变仍然是A)结论只有原本打到 B 的请求受影响其他请求不变。对比普通哈希删除 Node-B 后请求1: hash % 3 0 →A变为 hash % 2 0 →A(可能改变)请求2: hash % 3 1 →B变为 hash % 2 1 →C(改变)请求3: hash % 3 2 →C变为 hash % 2 0 →A(改变)(几乎所有请求都改变了)9. 节点变化如何影响 virtualNodes关键机制单例共享所有请求共用同一个 ConsistentHashLoadBalancer 实例变化检测通过 isSameServiceList() 比较节点列表是否变化按需重建只有节点列表变化时才重建哈希环。节点变化的完整流程时刻1注册中心有 [A, B, C]请求1 → 获取 [A, B, C] → 构建哈希环300个虚拟节点请求2/3 → 获取 [A, B, C] → 检测未变化复用哈希环时刻2Node-D 上线注册中心变为[A, B, C, D]请求4 → 获取 [A, B, C, D] →检测到变化重建哈希环400个虚拟节点请求5/6 → 获取 [A, B, C, D] → 检测未变化复用哈希环时刻3Node-B 下线注册中心变为 [A, C, D]请求7 → 获取 [A, C, D] →检测到变化重建哈希环300个虚拟节点请求8 → 获取 [A, C, D] → 检测未变化复用哈希环注册中心如何影响 virtualNodes// ServiceProxy.java public Object invoke(...) { // 1. 从注册中心获取最新的服务节点列表 ListServiceMetaInfo serviceMetaInfoList registry.serviceDiscovery(serviceKey); // 2. 传给负载均衡器 LoadBalancer loadBalancer LoadBalancerFactory.getInstance(...); ServiceMetaInfo selected loadBalancer.select(requestParams, serviceMetaInfoList); // 3. 负载均衡器内部检测节点列表是否变化 // - 如果变化重建 virtualNodes // - 如果不变复用 virtualNodes }10. 算法特点优点节点变化时影响范围小适合缓存场景缺点实现复杂需要维护哈希环适用场景需要会话保持相同请求打到同一节点。五、负载均衡器工厂与SPI机制1. 工厂类实现public class LoadBalancerFactory { static { SpiLoader.load(LoadBalancer.class); } private static final LoadBalancer DEFAULT_LOAD_BALANCER new RoundRobinLoadBalancer(); public static LoadBalancer getInstance(String key) { return SpiLoader.getInstance(LoadBalancer.class, key); } }2. 策略常量public interface LoadBalancerKeys { String ROUND_ROBIN roundRobin; String RANDOM random; String CONSISTENT_HASH consistentHash; }3. SPI 配置文件文件路径META-INF/rpc/system/你的接口路径roundRobincom.szj.example.szjrpceasy.loadBalancer.RoundRobinLoadBalancer(你的实现类路径) randomcom.szj.example.szjrpceasy.loadBalancer.RandomLoadBalancer consistentHashcom.szj.example.szjrpceasy.loadBalancer.ConsistentHashLoadBalancer六、在 ServiceProxy 中的使用1. 集成方式public class ServiceProxy implements InvocationHandler { Override public Object invoke(Object proxy, Method method, Object[] args) { // 1. 从注册中心获取服务节点列表 ListServiceMetaInfo serviceMetaInfoList registryService.serviceDiscovery(serviceKey); // 2. 使用负载均衡器选择节点 LoadBalancer loadBalancer LoadBalancerFactory.getInstance( rpcConfig.getLoadBalancer() ); ServiceMetaInfo selectedService loadBalancer.select( requestParams, serviceMetaInfoList ); // 3. 向选中的节点发起 RPC 调用 RpcResponse rpcResponse doRequest(rpcRequest, selectedService); return rpcResponse.getData(); } }2. 执行流程消费者发起调用 ↓ ServiceProxy.invoke() ↓ 从注册中心获取服务列表: [Node-A, Node-B, Node-C] ↓ 负载均衡器选择节点: Node-B ↓ 向 Node-B 发起 RPC 请求 ↓ 返回结果七、三种算法对比算法分布均匀性实现复杂度线程安全会话保持适用场景随机中等长期均匀简单是否无状态服务轮询高简单是AtomicInteger否通用场景一致性哈希高需虚拟节点复杂是是缓存、会话保持八、算法选择指南选择随机算法服务节点性能相近无状态服务对顺序无要求。选择轮询算法推荐通用场景需要均匀分配服务节点性能相近。选择一致性哈希需要会话保持同一用户请求打到同一节点缓存场景避免缓存失效节点频繁变化的场景。九、与容错机制的配合负载均衡和容错机制是两层保障第一层负载均衡在正常情况下选择最优节点目标性能优化、流量分配第二层容错机制在异常情况下处理失败目标提高可用性、降级保护执行流程消费者调用 ↓ 负载均衡器选择节点: Node-A ↓ 发起 RPC 请求 ↓ 请求失败 ↓ 容错策略介入: - FailFast: 立即抛出异常 - FailSafe: 记录日志返回空结果 - FailOver: 重试其他节点 (Node-B, Node-C) - FailBack: 返回降级结果十、实际应用示例示例1电商系统用户服务// 场景用户服务有3个节点 ListServiceMetaInfo userServiceNodes Arrays.asList( 192.168.1.1:8080, 192.168.1.2:8080, 192.168.1.3:8080 ); // 使用轮询算法 LoadBalancer lb new RoundRobinLoadBalancer(); // 10个请求的分配 // 请求1 → 192.168.1.1:8080 // 请求2 → 192.168.1.2:8080 // 请求3 → 192.168.1.3:8080 // 请求4 → 192.168.1.1:8080 // ...示例2缓存服务需要会话保持// 场景缓存服务相同用户的请求需要打到同一节点 MapString, Object requestParams Map.of(userId, 12345); // 使用一致性哈希 LoadBalancer lb new ConsistentHashLoadBalancer(); // 同一用户的多次请求 ServiceMetaInfo node1 lb.select(requestParams, cacheServiceNodes); ServiceMetaInfo node2 lb.select(requestParams, cacheServiceNodes); // node1 node2 (始终分配到同一个节点)十一、总结核心要点回顾负载均衡的本质在多个服务节点中选择一个最优节点。三种算法随机、轮询、一致性哈希各有适用场景。线程安全轮询算法使用 AtomicInteger 保证线程安全。一致性哈希通过虚拟节点和 TreeMap 实现适合会话保持。设计模式使用工厂模式 SPI机制可以灵活切换负载均衡策略。
RPC框架负载均衡机制深度解析
一、核心概念负载均衡Load Balancing是分布式系统中的核心机制用于在多个服务提供者之间分配请求流量。1. 为什么需要负载均衡在RPC框架中一个服务可能有多个提供者节点提高可用性某个节点故障时请求可以转发到其他节点提升性能将请求分散到多个节点避免单点过载支持扩展可以动态增加服务节点来应对流量增长。2. 负载均衡与容错的区别负载均衡在正常情况下如何选择一个最优的服务节点容错机制在异常情况下如何处理失败的请求。两者配合使用构成完整的服务调用保障体系。3. 负载均衡器接口设计public interface LoadBalancer { ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList); }接口说明requestParams请求参数可用于某些算法如一致性哈希serviceMetaInfoList可用的服务节点列表返回值选中的服务节点。二、随机算法Random1. 算法原理从服务列表中随机选择一个节点。2. 实现代码public class RandomLoadBalancer implements LoadBalancer { private final Random random new Random(); Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { int size serviceMetaInfoList.size(); if (size 0) { return null; } if (size 1) { return serviceMetaInfoList.get(0); } return serviceMetaInfoList.get(random.nextInt(size)); } }3. 算法特点优点实现简单分布均匀长期来看缺点短期内可能不均匀无法保证顺序适用场景服务节点性能相近无状态服务。三、轮询算法Round Robin1. 算法原理按顺序依次选择服务节点循环往复。2. 实现代码public class RoundRobinLoadBalancer implements LoadBalancer { private final AtomicInteger currentIndex new AtomicInteger(0); Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { if (serviceMetaInfoList.isEmpty()) { return null; } int size serviceMetaInfoList.size(); if (size 1) { return serviceMetaInfoList.get(0); } // 取模算法轮询 int index currentIndex.getAndIncrement() % size; return serviceMetaInfoList.get(index); } }3. AtomicInteger 详解为什么使用 AtomicInteger在多线程环境下普通的 int 类型不是线程安全的// 错误示例线程不安全 private int currentIndex 0; int index currentIndex % size; // 可能导致多个线程获得相同的indexAtomicInteger 如何保证线程安全// 正确示例线程安全 private final AtomicInteger currentIndex new AtomicInteger(0); int index currentIndex.getAndIncrement() % size;getAndIncrement() 方法是原子操作获取当前值将值加1返回原始值。这三步在底层通过 CASCompare-And-Swap机制保证原子性不会被其他线程打断。4. 执行示例假设有3个服务节点[Node-A, Node-B, Node-C]请求1: currentIndex0 → 0%30 → 选择 Node-A → currentIndex变为1 请求2: currentIndex1 → 1%31 → 选择 Node-B → currentIndex变为2 请求3: currentIndex2 → 2%32 → 选择 Node-C → currentIndex变为3 请求4: currentIndex3 → 3%30 → 选择 Node-A → currentIndex变为4 ...循环往复5. 算法特点优点分布均匀实现简单可预测缺点不考虑节点性能差异适用场景服务节点性能相近需要均匀分配。四、一致性哈希算法Consistent Hash1. 算法原理一致性哈希是一种特殊的哈希算法主要解决分布式系统中节点动态增减时的数据重新分配问题。2. 核心概念哈希环Hash Ring将哈希值空间0 ~ 2^32-1组织成一个虚拟的环形结构。虚拟节点Virtual Nodes为每个真实节点创建多个虚拟节点提高分布均匀性。真实节点: 192.168.1.1:8080 虚拟节点: - 192.168.1.1:8080#0 → hash值: 12345 - 192.168.1.1:8080#1 → hash值: 67890 - 192.168.1.1:8080#2 → hash值: 23456 ...共100个虚拟节点3. 实现代码public class ConsistentHashLoadBalancer implements LoadBalancer { // TreeMap 自动按 key 排序用于实现哈希环 private final TreeMapInteger, ServiceMetaInfo virtualNodes new TreeMap(); private static final int VIRTUAL_NODE_NUM 100; // 记录上次的服务节点列表用于判断是否需要重建哈希环 private ListServiceMetaInfo lastServiceList null; Override public ServiceMetaInfo select(MapString, Object requestParams, ListServiceMetaInfo serviceMetaInfoList) { if (serviceMetaInfoList.isEmpty()) { return null; } // 只有节点列表变化时才重建哈希环 if (lastServiceList null || !isSameServiceList(lastServiceList, serviceMetaInfoList)) { buildConsistentHashRing(serviceMetaInfoList); lastServiceList new ArrayList(serviceMetaInfoList); } // 获取请求的 hash 值 int hash getHash(requestParams); // 选择最接近且大于等于请求 hash 值的虚拟节点 Map.EntryInteger, ServiceMetaInfo entry virtualNodes.ceilingEntry(hash); if (entry null) { // 如果没有大于等于的节点返回环首部节点形成环状 entry virtualNodes.firstEntry(); } return entry.getValue(); } /** * 构建一致性哈希环 */ private void buildConsistentHashRing(ListServiceMetaInfo serviceMetaInfoList) { // 清空旧的虚拟节点 virtualNodes.clear(); // 为每个服务节点创建虚拟节点 for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) { for (int i 0; i VIRTUAL_NODE_NUM; i) { int hash getHash(serviceMetaInfo.getServiceAddress() # i); virtualNodes.put(hash, serviceMetaInfo); } } } /** * 判断服务列表是否相同 */ private boolean isSameServiceList(ListServiceMetaInfo list1, ListServiceMetaInfo list2) { if (list1.size() ! list2.size()) { return false; } // 比较服务地址集合 SetString set1 list1.stream() .map(ServiceMetaInfo::getServiceAddress) .collect(Collectors.toSet()); SetString set2 list2.stream() .map(ServiceMetaInfo::getServiceAddress) .collect(Collectors.toSet()); return set1.equals(set2); } private int getHash(Object key) { return key.hashCode(); } }4. TreeMap 和 ceilingEntry() 详解TreeMap 是什么TreeMap 是一个有序的 Map内部使用红黑树实现key 会自动按照自然顺序排序。TreeMapInteger, String map new TreeMap(); map.put(100, A); map.put(50, B); map.put(200, C); // 内部顺序50→100→200ceilingEntry() 方法ceilingEntry(key) 返回大于或等于给定 key 的最小 Entry。TreeMapInteger, String map new TreeMap(); map.put(10, A); map.put(20, B); map.put(30, C); map.ceilingEntry(15); // 返回 (20, B) map.ceilingEntry(20); // 返回 (20, B) map.ceilingEntry(35); // 返回 null5. 一致性哈希执行流程假设有2个服务节点每个节点100个虚拟节点步骤1构建哈希环仅在节点列表变化时执行Node-A (192.168.1.1:8080): - 192.168.1.1:8080#0 → hash: 1234 - 192.168.1.1:8080#1 → hash: 5678 - ... - 192.168.1.1:8080#99 → hash: 9999 Node-B (192.168.1.2:8080): - 192.168.1.2:8080#0 → hash: 2345 - 192.168.1.2:8080#1 → hash: 6789 - ... - 192.168.1.2:8080#99 → hash: 8888TreeMap 自动排序后1234→Node-A, 2345→Node-B, 5678→Node-A, 6789→Node-B, ...步骤2计算请求哈希值requestParams {method: getUser, userId: 123} hash requestParams.hashCode() 45678步骤3查找节点// 在 TreeMap 中查找 45678 的最小 key entry virtualNodes.ceilingEntry(45678); // 假设找到 key50000对应 Node-B return Node-B;6. virtualNodes 的生命周期所有请求共用同一个 virtualNodes。由于 SpiLoader 使用单例模式ConsistentHashLoadBalancer 只会创建一个实例// 所有请求共用同一个负载均衡器实例 LoadBalancer lb LoadBalancerFactory.getInstance(consistentHash); // 因此 virtualNodes 也是共享的 private final TreeMapInteger, ServiceMetaInfo virtualNodes new TreeMap();执行流程演示请求1: serviceMetaInfoList [A, B, C] → lastServiceList null → 构建哈希环: virtualNodes {A的100个, B的100个, C的100个} → 保存 lastServiceList [A, B, C] → 选择节点 请求2: serviceMetaInfoList [A, B, C] → 检测到节点列表未变化isSameServiceList 返回 true → 不重建哈希环直接使用现有 virtualNodes → 选择节点性能高 请求3: serviceMetaInfoList [A, B, C] → 节点列表仍未变化 → 继续使用现有 virtualNodes → 选择节点 (此时发生节点上线: [A, B, C, D]) 请求4: serviceMetaInfoList [A, B, C, D] → 检测到节点列表变化多了 D → 清空 virtualNodes → 重建哈希环: virtualNodes {A的100个, B的100个, C的100个, D的100个} → 保存 lastServiceList [A, B, C, D] → 选择节点 请求5: serviceMetaInfoList [A, B, C, D] → 节点列表未变化 → 使用现有 virtualNodes → 选择节点7. 为什么需要虚拟节点没有虚拟节点的问题假设只有2个真实节点Node-A hash: 1000 Node-B hash: 9000哈希环上只有2个点分布极不均匀hash值在 1000~9000 之间的请求全部打到 Node-Bhash值在 9000~1000 之间的请求全部打到 Node-A。使用虚拟节点后每个节点有100个虚拟节点哈希环上有200个点分布更均匀1234→A, 2345→B, 3456→A, 4567→B, 5678→A, 6789→B, ...这样请求会更均匀地分配到各个节点。8. 一致性哈希的优势场景节点动态变化假设有3个节点 [A, B, C]使用普通哈希index hash(request) % 3; // 节点数3当增加一个节点D后index hash(request) % 4; // 节点数4几乎所有请求的目标节点都会改变一致性哈希的优势增加或删除节点时只影响哈希环上相邻的部分节点大部分请求的目标节点不变。具体示例初始状态3个节点 [A, B, C] 哈希环简化 0 ----A(1000)---- B(5000) ----C(8000)---- 10000(环回到A) 请求1 (hash3000) → ceilingEntry(3000) → B (第一个 3000 的节点) 请求2 (hash7000) → ceilingEntry(7000) → C (第一个 7000 的节点) 请求3 (hash9000) → ceilingEntry(9000) → A (没有 9000 的返回首节点) 删除 Node-B 后哈希环变为 0 ----A(1000)---- C(8000)---- 10000(环回到A) 请求1 (hash3000) → ceilingEntry(3000) → C (改变B删除顺延到C) 请求2 (hash7000) → ceilingEntry(7000) → C (不变仍然是C) 请求3 (hash9000) → ceilingEntry(9000) → A (不变仍然是A)结论只有原本打到 B 的请求受影响其他请求不变。对比普通哈希删除 Node-B 后请求1: hash % 3 0 →A变为 hash % 2 0 →A(可能改变)请求2: hash % 3 1 →B变为 hash % 2 1 →C(改变)请求3: hash % 3 2 →C变为 hash % 2 0 →A(改变)(几乎所有请求都改变了)9. 节点变化如何影响 virtualNodes关键机制单例共享所有请求共用同一个 ConsistentHashLoadBalancer 实例变化检测通过 isSameServiceList() 比较节点列表是否变化按需重建只有节点列表变化时才重建哈希环。节点变化的完整流程时刻1注册中心有 [A, B, C]请求1 → 获取 [A, B, C] → 构建哈希环300个虚拟节点请求2/3 → 获取 [A, B, C] → 检测未变化复用哈希环时刻2Node-D 上线注册中心变为[A, B, C, D]请求4 → 获取 [A, B, C, D] →检测到变化重建哈希环400个虚拟节点请求5/6 → 获取 [A, B, C, D] → 检测未变化复用哈希环时刻3Node-B 下线注册中心变为 [A, C, D]请求7 → 获取 [A, C, D] →检测到变化重建哈希环300个虚拟节点请求8 → 获取 [A, C, D] → 检测未变化复用哈希环注册中心如何影响 virtualNodes// ServiceProxy.java public Object invoke(...) { // 1. 从注册中心获取最新的服务节点列表 ListServiceMetaInfo serviceMetaInfoList registry.serviceDiscovery(serviceKey); // 2. 传给负载均衡器 LoadBalancer loadBalancer LoadBalancerFactory.getInstance(...); ServiceMetaInfo selected loadBalancer.select(requestParams, serviceMetaInfoList); // 3. 负载均衡器内部检测节点列表是否变化 // - 如果变化重建 virtualNodes // - 如果不变复用 virtualNodes }10. 算法特点优点节点变化时影响范围小适合缓存场景缺点实现复杂需要维护哈希环适用场景需要会话保持相同请求打到同一节点。五、负载均衡器工厂与SPI机制1. 工厂类实现public class LoadBalancerFactory { static { SpiLoader.load(LoadBalancer.class); } private static final LoadBalancer DEFAULT_LOAD_BALANCER new RoundRobinLoadBalancer(); public static LoadBalancer getInstance(String key) { return SpiLoader.getInstance(LoadBalancer.class, key); } }2. 策略常量public interface LoadBalancerKeys { String ROUND_ROBIN roundRobin; String RANDOM random; String CONSISTENT_HASH consistentHash; }3. SPI 配置文件文件路径META-INF/rpc/system/你的接口路径roundRobincom.szj.example.szjrpceasy.loadBalancer.RoundRobinLoadBalancer(你的实现类路径) randomcom.szj.example.szjrpceasy.loadBalancer.RandomLoadBalancer consistentHashcom.szj.example.szjrpceasy.loadBalancer.ConsistentHashLoadBalancer六、在 ServiceProxy 中的使用1. 集成方式public class ServiceProxy implements InvocationHandler { Override public Object invoke(Object proxy, Method method, Object[] args) { // 1. 从注册中心获取服务节点列表 ListServiceMetaInfo serviceMetaInfoList registryService.serviceDiscovery(serviceKey); // 2. 使用负载均衡器选择节点 LoadBalancer loadBalancer LoadBalancerFactory.getInstance( rpcConfig.getLoadBalancer() ); ServiceMetaInfo selectedService loadBalancer.select( requestParams, serviceMetaInfoList ); // 3. 向选中的节点发起 RPC 调用 RpcResponse rpcResponse doRequest(rpcRequest, selectedService); return rpcResponse.getData(); } }2. 执行流程消费者发起调用 ↓ ServiceProxy.invoke() ↓ 从注册中心获取服务列表: [Node-A, Node-B, Node-C] ↓ 负载均衡器选择节点: Node-B ↓ 向 Node-B 发起 RPC 请求 ↓ 返回结果七、三种算法对比算法分布均匀性实现复杂度线程安全会话保持适用场景随机中等长期均匀简单是否无状态服务轮询高简单是AtomicInteger否通用场景一致性哈希高需虚拟节点复杂是是缓存、会话保持八、算法选择指南选择随机算法服务节点性能相近无状态服务对顺序无要求。选择轮询算法推荐通用场景需要均匀分配服务节点性能相近。选择一致性哈希需要会话保持同一用户请求打到同一节点缓存场景避免缓存失效节点频繁变化的场景。九、与容错机制的配合负载均衡和容错机制是两层保障第一层负载均衡在正常情况下选择最优节点目标性能优化、流量分配第二层容错机制在异常情况下处理失败目标提高可用性、降级保护执行流程消费者调用 ↓ 负载均衡器选择节点: Node-A ↓ 发起 RPC 请求 ↓ 请求失败 ↓ 容错策略介入: - FailFast: 立即抛出异常 - FailSafe: 记录日志返回空结果 - FailOver: 重试其他节点 (Node-B, Node-C) - FailBack: 返回降级结果十、实际应用示例示例1电商系统用户服务// 场景用户服务有3个节点 ListServiceMetaInfo userServiceNodes Arrays.asList( 192.168.1.1:8080, 192.168.1.2:8080, 192.168.1.3:8080 ); // 使用轮询算法 LoadBalancer lb new RoundRobinLoadBalancer(); // 10个请求的分配 // 请求1 → 192.168.1.1:8080 // 请求2 → 192.168.1.2:8080 // 请求3 → 192.168.1.3:8080 // 请求4 → 192.168.1.1:8080 // ...示例2缓存服务需要会话保持// 场景缓存服务相同用户的请求需要打到同一节点 MapString, Object requestParams Map.of(userId, 12345); // 使用一致性哈希 LoadBalancer lb new ConsistentHashLoadBalancer(); // 同一用户的多次请求 ServiceMetaInfo node1 lb.select(requestParams, cacheServiceNodes); ServiceMetaInfo node2 lb.select(requestParams, cacheServiceNodes); // node1 node2 (始终分配到同一个节点)十一、总结核心要点回顾负载均衡的本质在多个服务节点中选择一个最优节点。三种算法随机、轮询、一致性哈希各有适用场景。线程安全轮询算法使用 AtomicInteger 保证线程安全。一致性哈希通过虚拟节点和 TreeMap 实现适合会话保持。设计模式使用工厂模式 SPI机制可以灵活切换负载均衡策略。