本文所构建的代码已上传至Github(注意切换分支) ,所有代码均亲测有效,祝食用愉快。
之前的项目springcloud-demo中,我们使用了单机的服务提供者(user-service),而在实际的生产环境中,服务提供方肯定会以多台部署(集群)的方式提供以保障服务高可用,这种情况下,很容易想到需要写一个负载均衡算法来调用。而Spring Cloud Eureka已提供了负载均衡组件Robbin,只需要少量代码、配置即可快速投入使用
一、基础使用
1.1 提供2个user-service实例

1.2 修改注入的RestTemplate实例
@Configuration
public class SysConfiguration {
// 添加@LoadBalanced注解
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
1.3 修改服务调用方的服务调用方式:不再使用host:port方式调用,而直接根据服务名(spring.application.name)调用
@RestController
@RequestMapping("consumer/sysUser/")
public class SysUserController {
@Resource
private RestTemplate restTemplate;
@RequestMapping("selectById")
public SysUser selectById(Integer id) {
String url = "http://user-service/sysUser/selectById?id=" + id;
return restTemplate.getForObject(url, SysUser.class);
}
}
1.4 改造服务提供方,输出请求ip
@RequestMapping("sysUser")
@RestController
public class SysUserController {
@Resource
private SysUserService sysUserService;
@Resource
private HttpServletRequest request;
@RequestMapping("selectById")
public SysUser selectById(Integer id ){
System.err.println(request.getRequestURL());
return sysUserService.selectById(id);
}
}
1.5 多次请求后,user-service的打印信息
http://127.0.0.1:8081/sysUser/selectById
http://127.0.0.1:8082/sysUser/selectById
http://127.0.0.1:8081/sysUser/selectById
http://127.0.0.1:8082/sysUser/selectById
二、源码进阶
2.1 核心对象
2个重点自动配置类:
- RibbonAutoConfiguration.java:将
RibbonLoadBalancerClient注入Spring容器 - LoadBalancerAutoConfiguration.java:将
LoadBalancerRequestFactory对象、LoadBalancerInterceptor对象注入Spring容器,获取所有被@LoadBalance标记的RestTemplate对象,并挨个添加LoadBalancerInterceptor对象。
2.1.1 @LoadBalanced
- javadoc:Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient.
- 含义:标记会被转换成
LoadBalancerClient的RestTemplate对象
2.1.2 LoadBalancerClient:
- javadoc:Represents a client-side load balancer.
- 大致含义:客户端负载均衡器。
- 由RibbonAutoConfiguration默认提供的RibbonLoadBalancerClient对象:
@Configuration @Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) @RibbonClients @AutoConfigureAfter( name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration") @AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class }) @EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class }) public class RibbonAutoConfiguration { ... @Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); } ... }
2.1.3 LoadBalancerRequestFactory:
- javadoc:Creates LoadBalancerRequests for LoadBalancerInterceptor and RetryLoadBalancerInterceptor. Applies LoadBalancerRequestTransformers to the intercepted HttpRequest.
- 大致含义:将
HttpRequest转化为LoadBalancerRequest并提供给LoadBalancerRequests或LoadBalancerInterceptor。(如果有提供自定义LoadBalancerRequestTransformers实现,也可在转化中执行) LoadBalancerRequestFactory实例在LoadBalancerAutoConfiguration中注入:@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestTemplate.class) @ConditionalOnBean(LoadBalancerClient.class) @EnableConfigurationProperties(LoadBalancerRetryProperties.class) public class LoadBalancerAutoConfiguration { // 获取到所有被@LoadBalanced标记的RestTemplate对象 @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); ... @Bean @ConditionalOnMissingBean public LoadBalancerRequestFactory loadBalancerRequestFactory( LoadBalancerClient loadBalancerClient) { return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers); } ... }
2.1.4 LoadBalancerInterceptor (implements ClientHttpRequestInterceptor)
ClientHttpRequestInterceptor的javadoc:Intercepts client-side HTTP requests. Implementations of this interface can be registered with the RestTemplate, as to modify the outgoing ClientHttpRequest and/or the incoming ClientHttpResponse.- 含义:拦截客户端的http请求,将接收到的
HttpRequest转化成ClientHttpRequest,并最终返回ClientHttpResponse - 可以看到,
loadBalancerInterceptor在LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig#restTemplateCustomizer添加到restTemplate的拦截器列表。而创建loadBalancerInterceptor所需的loadBalancerClient、requestFactory就是上面Ribbon默认注入的。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
...
}
...
}
2.2 请求地址替换
上一章节中,服务调用者在代码中请求的地址是"http://user-service/sysUser/selectById",盲猜是Ribbon将地址中的“user-service”替换成了实际的请求地址,实际上是LoadBalancerInterceptor帮助我们做了这件事。
通过前一小结的分析
现在我们来简单地追踪一下源码,看一下Ribbon是如何实现的:

其中的loadBalancer、requestFactory就是上面提到的
RibbonLoadBalancerClient与LoadBalancerRequestFactory对象。
2.3 负载均衡策略
2.3.1 轮询:默认的负载均衡策略
2.3.1.1 负载均衡策略模式的选取
追踪源码可以发现,是BaseLoadBalancer中的rule对象,默认注入的是轮询类型的Rule。
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
private final static IRule DEFAULT_RULE = new RoundRobinRule();
...
protected IRule rule = DEFAULT_RULE;
...
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
...
}
也可以写个单元测试来验证:
@SpringBootTest(classes = ConsumerDemoApplication.class)
public class LoadBalanceTest {
@Autowired
RibbonLoadBalancerClient client;
@Test
public void test(){
for (int i = 0; i < 10; i++) {
ServiceInstance instance = this.client.choose("user-service");
System.out.println(instance.getHost() + ":" + instance.getPort());
}
}
}
可以看到,缺失是轮询请求的:
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8081
127.0.0.1:8082
2.3.2 修改负载均衡策略
- 可以根据IRule接口的实现,看看SpringCloud提供了那些负载均衡实现
AbstractLoadBalancerRule.java AvailabilityFilteringRule.java BestAvailableRule.java ClientConfigEnabledRoundRobinRule.java PredicateBasedRule.java RandomRule.java ResponseTimeWeightedRule.java RetryRule.java RoundRobinRule.java WeightedResponseTimeRule.java ZoneAvoidanceRule.java - 修改yml配置:
格式是:
{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的实现类。user-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule - 重新执行单测,可以发现已经变成随机请求:
127.0.0.1:8082 127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8081 127.0.0.1:8081 127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8081 127.0.0.1:8081 127.0.0.1:8081
三、重试机制
Eureka的服务治理强调了CAP原则中的AP,即可用性和可靠性。它与Zookeeper这一类强调CP(一致性,可靠性)的服务治理框架最大的区别在于:Eureka为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下它宁愿接收故障实例也不愿丢掉健康实例,正如我们上面所说的自我保护机制。
但是,此时如果我们调用了这些不正常的服务,调用就会失败,从而导致其它服务不能正常工作!这显然不是我们愿意看到的。如果现在user-service注册了8081、8082台实例,consumer-demo启动Ribbon负载均衡,此时我们停掉user-service的8082示例。eureka-server中针对user-service中还记录着8082实例,此时consumer-demo轮询到8082将报如下错误信息,但user-service的8081实例是可以提供服务的:

3.1 新增pom依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
3.2 新增配置
# 开启Spring Cloud的重试功能
spring.cloud.loadbalancer.retry.enabled=true
# Ribbon的连接超时时间
user-service.ribbon.ConnectTimeout=250
# Ribbon的数据读取超时时间
user-service.ribbon.ReadTimeout=1000
# 是否对所有操作都进行重试
user-service.ribbon.OkToRetryOnAllOperations=true
# 切换实例的重试次数
user-service.ribbon.MaxAutoRetriesNextServer=1
# 对当前实例的重试次数
user-service.ribbon.MaxAutoRetries=1
可以看到,Ribbon能够实现自动重试而不会返回8082实例的错误信息。
四、补充知识
4.1 @Autowire冷知识
在使用@Autowire或@Resource获取Spring容器中对象时,同时写上自定义注解,将或获取到被自定义注解标记的对象,下面用代码来简单演示一下:
- 定义2个注解:
Anno1.java、Anno2.java注意需要在注解上添加@Qualifier注解@Documented @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.FIELD}) public @interface Anno1 { }@Documented @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.FIELD}) public @interface Anno2 { } - 定义一个类,用于提交到Spring容器:
MyService.javapublic class MyService { } - 定义配置类,用于提交多个
MyService实例:MyConfig.java@Configuration public class MyConfig { @Bean @Anno1 public MyService instance11(){ return new MyService(); } @Bean @Anno1 public MyService instance12(){ return new MyService(); } @Bean @Anno2 public MyService instance21(){ return new MyService(); } @Bean @Anno2 public MyService instance22(){ return new MyService(); } @Bean @Anno2 public MyService instance23(){ return new MyService(); } @Bean public MyService instance01(){ return new MyService(); } @Bean public MyService instance02(){ return new MyService(); } } - 从Spring容器获取
MyService实例@Autowired private List<MyService> instanceList00; @Anno1 @Autowired private List<MyService> instanceList01; @Autowired @Anno2 private List<MyService> instanceList02; @Test public void AutowireTest(){ System.err.println(instanceList00.size()); System.err.println(instanceList01.size()); System.err.println(instanceList02.size()); } - 效果:
