• 微服务是什么?它的优缺点有哪些?
  • Spring Boot简介
  • Spring Boot项目搭建步骤(超详细)
  • 使用Eureka编写服务提供者
  • 使用Eureka编写服务消费者
  • Eureka注册中心开启密码认证
  • Spring Cloud使用Eureka集群搭建实现高可用服务注册中心
  • Eureka自我保护模式和InstanceID的配置
  • Eureka开发时快速移除失效服务
  • Eureka开发时快速移除失效服务
  • Spring Cloud Ribbon(负载均衡器)介绍及使用
  • Spring Cloud Ribbon结合RestTemplate实现负载均衡
  • Spring Cloud Ribbon配置详解
  • Spring Cloud使用Feign调用服务接口
  • Spring Cloud Feign的自定义配置及使用
  • Spring Cloud Hystrix缓存与合并请求
  • Spring Cloud Zuul网关的介绍及使用
  • Spring Cloud Zuul过滤器介绍及使用(传递数据、拦截请求和异常处理)
  • Spring Cloud使用Zuul实现容错回退功能
  • Spring Cloud Zuul请求响应信息输出
  • Spring Cloud实现Zuul自带的Debug功能
  • Spring Cloud Gateway整合Eureka路由转发
  • Spring Cloud Gateway的常用路由断言工厂
  • Spring Cloud Gateway过滤器工厂的使用
  • Spring Cloud Gateway全局过滤器(GlobalFilter)
  • Smconf(分布式配置管理框架)概述
  • Apollo(分布式配置中心)核心概念及核心功能介绍
  • Apollo本地部署详细步骤
  • Apollo Portal管理后台的使用
  • Apollo高可用设计分析
  • Spring Cloud使用Sleuth在应用中进行日志跟踪
  • Spring Cloud Sleuth与ELK(日志分析系统)配合使用
  • Spring Cloud整合Zipkin进行服务跟踪
  • JWT(Json Web Token)是什么?
  • Spring Cloud基于JWT创建统一的认证服务
  • Zuul中传递Token到路由的服务中
  • Spring Boot Admin的介绍及使用
  • Swagger是什么?Swagger怎么用?
  • 使用Zuul聚合多个微服务的Swagger文档
  • 微服务架构下如何获取用户信息并认证?
  • 服务降级是什么?Spring Cloud如何实现?
  • Guava Cache本地缓存介绍及使用
  • 防止缓存雪崩的方案
  • Spring Cloud Ribbon结合RestTemplate实现负载均衡

    在《Spring Cloud Ribbon介绍及使用》教程中我们简单地使用 Ribbon 进行了负载的一个调用,这意味着 Ribbon 是可以单独使用的。

    在 Spring Cloud 中使用 Ribbon 会更简单,因为 Spring Cloud 在 Ribbon 的基础上进行了一层封装,将很多配置都集成好了。本节将在 Spring Cloud 项目中使用 Ribbon。

    使用 RestTemplate 与整合 Ribbon

    Spring 提供了一种简单便捷的模板类来进行 API 的调用,那就是 RestTemplate。

    1. 使用 RestTemplate

    在前面介绍 Eureka 时,我们已经使用过 RestTemplate 了,本节会更加详细地跟大家讲解 RestTemplate 的具体使用方法。

    首先我们来看看 GET 请求的使用方式:创建一个新的项目 spring-rest-template,配置好 RestTemplate:

    1. @Configuration
    2. public class BeanConfiguration {
    3. @Bean
    4. public RestTemplate getRestTemplate() {
    5. return new RestTemplate();
    6. }
    7. }

    新建一个 HouseController,并增加两个接口,一个通过 @RequestParam 来传递参数,返回一个对象信息;另一个通过 @PathVariable 来传递参数,返回一个字符串。请尽量通过两个接口组装不同的形式,具体代码如下所示。

    1. @GetMapping("/house/data")
    2. public HouseInfo getData(@RequestParam("name") String name) {
    3. return new HouseInfo(1L, "上海" "虹口" "东体小区");
    4. }
    5. @GetMapping("/house/data/{name}")
    6. public String getData2(@PathVariable("name") String name) {
    7. return name;
    8. }

    新建一个 HouseClientController 用于测试,使用 RestTemplate 来调用我们刚刚定义的两个接口,代码如下所示。

    1. @GetMapping("/call/data")
    2. public HouseInfo getData(@RequestParam("name") String name) {
    3. return restTemplate.getForObject( "http://localhost:8081/house/data?name="+ name, HouseInfo.class);
    4. }
    5. @GetMapping("/call/data/{name}")
    6. public String getData2(@PathVariable("name") String name) {
    7. return restTemplate.getForObject( "http://localhost:8081/house/data/{name}", String.class, name);
    8. }

    获取数据结果可通过 RestTemplate 的 getForObject 方法(如下代码所示)来实现,此方法有三个重载的实现:

    • url:请求的 API 地址,有两种方式,其中一种是字符串,另一种是 URI 形式。
    • responseType:返回值的类型。
    • uriVariables:PathVariable 参数,有两种方式,其中一种是可变参数,另一种是 Map 形式。
    1. public <T> T getForObject(String url, Class<T> responseType,
    2. Object... uriVariables);
    3. public <T> T getForObject(String url, Class<T> responseType,
    4. Map<String, ?> uriVariables);
    5. public <T> T getForObject(URI url, Class<T> responseType);

    除了 getForObject,我们还可以使用 getForEntity 来获取数据,代码如下所示。

    1. @GetMapping("/call/dataEntity")
    2. public HouseInfo getData(@RequestParam("name") String name) {
    3. ResponseEntity<HouseInfo> responseEntity = restTemplate
    4. .getForEntity("http://localhost:8081/house/data?name=" + name, HouseInfo.class);
    5. if (responseEntity.getStatusCodeValue() == 200) {
    6. return responseEntity.getBody();
    7. }
    8. return null;
    9. }

    getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。其余的和 getForObject 一样,也是有 3 个重载的实现。

    接下来看看怎么使用 POST 方式调用接口。在 HouseController 中增加一个 save 方法用来接收 HouseInfo 数据,代码如下所示。

    1. @PostMapping("/house/save")
    2. public Long addData(@RequestBody HouseInfo houseInfo) {
    3. System.out.println(houseInfo.getName());
    4. return 1001L;
    5. }

    接着写调用代码,用 postForObject 来调用,代码如下所示。

    1. @GetMapping("/call/save")
    2. public Long add() {
    3. HouseInfo houseInfo = new HouseInfo();
    4. houseInfo.setCity("上海");
    5. houseInfo.setRegion("虹口");
    6. houseInfo.setName("×××");
    7. Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
    8. return id;
    9. }

    postForObject 同样有 3 个重载的实现。除了 postForObject 还可以使用 postForEntity 方法,用法都一样,代码如下所示。

    1. public <T> T postForObject(String url, Object request,
    2. Class<T> responseType, Object... uriVariables);
    3. public <T> T postForObject(String url, Object request,
    4. Class<T> responseType, Map<String, ?> uriVariables);
    5. public <T> T postForObject(URI url, Object request, Class<T> responseType);

    除了 get 和 post 对应的方法之外,RestTemplate 还提供了 put、delete 等操作方法,还有一个比较实用的就是 exchange 方法。exchange 可以执行 get、post、put、delete 这 4 种请求方式。更多地使用方式大家可以自行学习。

    2. 整合 Ribbon

    在 Spring Cloud 项目中集成 Ribbon 只需要在 pom.xml 中加入下面的依赖即可,其实也可以不用配置,因为 Eureka 中已经引用了 Ribbon,代码如下所示。

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    4. </dependency>

    RestTemplate 负载均衡示例

    前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate 可以结合 Eureka 来动态发现服务并进行负载均衡的调用。

    修改 RestTemplate 的配置,增加能够让 RestTemplate 具备负载均衡能力的注解 @LoadBalanced。代码如下所示。

    1. @Configuration
    2. public class BeanConfiguration {
    3. @Bean
    4. @LoadBalanced
    5. public RestTemplate getRestTemplate() {
    6. return new RestTemplate();
    7. }
    8. }

    修改接口调用的代码,将 IP+PORT 改成服务名称,也就是注册到 Eureka 中的名称,代码如下所示。

    1. @GetMapping("/call/data")
    2. public HouseInfo getData(@RequestParam("name") String name) {
    3. return restTemplate.getForObject("http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class);
    4. }

    接口调用的时候,框架内部会将服务名称替换成具体的服务 IP 信息,然后进行调用。

    @LoadBalanced 注解原理

    相信大家一定有一个疑问:为什么在 RestTemplate 上加了一个 @LoadBalanced 之后,RestTemplate 就能够跟 Eureka 结合了,不但可以使用服务名称去调用接口,还可以负载均衡?

    应该归功于 Spring Cloud 给我们做了大量的底层工作,因为它将这些都封装好了,我们用起来才会那么简单。框架就是为了简化代码,提高效率而产生的。

    这里主要的逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用,这就是 @LoadBalanced 的原理。

    下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。我们不做任何操作,就输出一句话,证明能进来就行了。具体代码如下所示。

    1. public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    2. private LoadBalancerClient loadBalancer;
    3. private LoadBalancerRequestFactory requestFactory;
    4. public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
    5. this.loadBalancer = loadBalancer;
    6. this.requestFactory = requestFactory;
    7. }
    8. public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
    9. this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    10. }
    11. @Override
    12. public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    13. final ClientHttpRequestExecution execution) throws IOException {
    14. final URI originalUri = request.getURI();
    15. String serviceName = originalUri.getHost();
    16. System.out.println("进入自定义的请求拦截器中" + serviceName);
    17. Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    18. return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    19. }
    20. }

    拦截器设置好了之后,我们再定义一个注解,并复制 @LoadBalanced 的代码,改个名称就可以了,代码如下所示。

    1. @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Inherited
    5. @Qualifier
    6. public @interface MyLoadBalanced {
    7. }

    然后定义一个配置类,给 RestTemplate 注入拦截器,代码如下所示。

    1. @Configuration
    2. public class MyLoadBalancerAutoConfiguration {
    3. @MyLoadBalanced
    4. @Autowired(required = false)
    5. private List<RestTemplate> restTemplates = Collections.emptyList();
    6. @Bean
    7. public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
    8. return new MyLoadBalancerInterceptor();
    9. }
    10. @Bean
    11. public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
    12. return new SmartInitializingSingleton() {
    13. @Override
    14. public void afterSingletonsInstantiated() {
    15. for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
    16. List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
    17. list.add(myLoad BalancerInterceptor());
    18. restTemplate.setInterceptors(list);
    19. }
    20. }
    21. };
    22. }
    23. }

    维护一个 @MyLoadBalanced 的 RestTemplate 列表,在 SmartInitializingSingleton 中对 RestTemplate 进行拦截器设置。

    然后改造我们之前的 RestTemplate 配置,将 @LoadBalanced 改成我们自定义的 @MyLoadBalanced,代码如下所示。

    1. @Bean
    2. //@LoadBalanced
    3. @MyLoadBalanced
    4. public RestTemplate getRestTemplate() {
    5. return new RestTemplate();
    6. }

    重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:

    1. 进入自定义的请求拦截器中 ribbon-eureka-demo

    通过这个小案例我们就能够清楚地知道 @LoadBalanced 的工作原理。接下来我们来看看源码中是怎样的一个逻辑。

    首先看配置类,如何为 RestTemplate 设置拦截器,代码在 spring-cloud-commons.jar 中的 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 类里面通过查看 LoadBalancerAutoConfiguration 的源码,可以看到这里也是维护了一个 @LoadBalanced 的 RestTemplate 列表,代码如下所示。

    1. @LoadBalanced
    2. @Autowired(required = false)
    3. private List<RestTemplate> restTemplates = Collections.emptyList();
    4. @Bean
    5. public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
    6. return new SmartInitializingSingleton() {
    7. @Override
    8. public void afterSingletonsInstantiated() {
    9. for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
    10. for (RestTemplateCustomizer customizer : customizers) {
    11. customizer.customize(restTemplate);
    12. }
    13. }
    14. }
    15. };
    16. }

    通过查看拦截器的配置可以知道,拦截器用的是 LoadBalancerInterceptor,RestTemplate Customizer 用来添加拦截器,代码如下所示。

    1. @Configuration
    2. @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    3. static class LoadBalancerInterceptorConfig {
    4. @Bean
    5. public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,
    6. LoadBalancerRequestFactory requestFactory) {
    7. return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    8. }
    9. @Bean
    10. @ConditionalOnMissingBean
    11. public RestTemplateCustomizer restTemplateCustomizer(
    12. final LoadBalancerInterceptor loadBalancerInterceptor) {
    13. return new RestTemplateCustomizer() {
    14. @Override
    15. public void customize(RestTemplate restTemplate) {
    16. List<ClientHttpRequestInterceptor> list = new ArrayList<>(
    17. restTemplate.getInterceptors());
    18. list.add(loadBalancerInterceptor);
    19. restTemplate.setInterceptors(list);
    20. }
    21. };
    22. }
    23. }

    拦截器的代码在 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 中,代码如下所示。

    1. public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    2. private LoadBalancerClient loadBalancer;
    3. private LoadBalancerRequestFactory requestFactory;
    4. public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
    5. this.loadBalancer = loadBalancer;
    6. this.requestFactory = requestFactory;
    7. }
    8. public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
    9. this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    10. }
    11. @Override
    12. public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    13. final ClientHttpRequestExecution execution) throws IOException {
    14. final URI originalUri = request.getURI();
    15. String serviceName = originalUri.getHost();
    16. Assert.state(serviceName != null, "Request URI does not contain a valid hostname:" + originalUri);
    17. return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    18. }
    19. }

    主要的逻辑在 intercept 中,执行交给了 LoadBalancerClient 来处理,通过 LoadBalancer RequestFactory 来构建一个 LoadBalancerRequest 对象,代码如下所示。

    1. public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request, final byte[] body,
    2. final ClientHttpRequestExecution execution) {
    3. return new LoadBalancerRequest<ClientHttpResponse>() {
    4. @Override
    5. public ClientHttpResponse apply(final ServiceInstance instance) throws Exception {
    6. HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
    7. if (transformers != null) {
    8. for (LoadBalancerRequestTransformer transformer : transformers) {
    9. serviceRequest = transformer.transformRequest(serviceRequest,instance);
    10. }
    11. }
    12. return execution.execute(serviceRequest, body);
    13. }
    14. };
    15. }

    createRequest 中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,ServiceRequest Wrapper 中将 URI 的获取交给了 org.springframework.cloud.client.loadbalancer.LoadBalancer Client#reconstructURI 方法。

    以上就是整个 RestTemplate 结合 @LoadBalanced 的执行流程,至于具体的实现大家可以自己去研究,这里只介绍原理及整个流程。

    Ribbon API 使用

    当你有一些特殊的需求,想通过 Ribbon 获取对应的服务信息时,可以使用 Load-Balancer Client 来获取,比如你想获取一个 ribbon-eureka-demo 服务的服务地址,可以通过 LoadBalancerClient 的 choose 方法来选择一个:

    1. @Autowired
    2. private LoadBalancerClient loadBalancer;
    3. @GetMapping("/choose")
    4. public Object chooseUrl() {
    5. ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
    6. return instance;
    7. }

    访问接口,可以看到返回的信息如下:

    1. {
    2. serviceId: "ribbon-eureka-demo",
    3. server: {
    4. host: "localhost",
    5. port: 8081,
    6. id: "localhost:8081",
    7. zone: "UNKNOWN",
    8. readyToServe: true,
    9. alive: true,
    10. hostPort: "localhost:8081",
    11. metaInfo: {
    12. serverGroup: null,
    13. serviceIdForDiscovery: null, instanceId: "localhost:8081",
    14. appName: null
    15. }
    16. },
    17. secure: false, metadata: { }, host: "localhost", port: 8081,
    18. uri: "http://localhost:8081"
    19. }

    Ribbon 饥饿加载

    笔者从网上看到很多博客中都提到过的一种情况:在进行服务调用的时候,如果网络情况不好,第一次调用会超时。有很多大神对此提出了解决方案,比如把超时时间改长一点、禁用超时等。

    Spring Cloud 目前正在高速发展中,版本更新很快,我们能发现的问题基本上在版本更新的时候就修复了,或者提供最优的解决方案。

    超时的问题也是一样,Ribbon 的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。

    本教程是基于 Finchley.SR2 撰写的,这个版本已经提供了一种针对上述问题的解决方法,那就是 eager-load 方式。通过配置 eager-load 来提前初始化客户端就可以解决这个问题。

    1. ribbon.eager-load.enabled=true
    2. ribbon.eager-load.clients=ribbon-eureka-demo
    • ribbon.eager-load.enabled:开启 Ribbon 的饥饿加载模式。
    • ribbon.eager-load.clients:指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。

    怎么进行验证呢?网络情况确实不太好模拟,不过通过调试源码的方式即可验证,在 org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration 中找到对应的代码,代码如下所示。

    1. @Bean
    2. @ConditionalOnProperty(value = "ribbon.eager-load.enabled")
    3. public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
    4. return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
    5. }

    在 return 这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了