springcloud-02-微服务间通信及熔断组件

第二章 微服务间通信及熔断组件

1. 微服务间通信组件

1.1 基于RestTemplate的服务调用

Spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClientRestTemplate是一种更优雅的调用RESTful服务的方式。
模拟服务间的调用:
创建2个服务:
用户服务springcloud-04user
订单服务springcloud-05order,并将其注册到consul注册中心。
在这里插入图片描述
2个服务的配置文件分别如下:
在这里插入图片描述
订单服务中的被调用的接口:

package com.jitazheng.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @title: 订单服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class OrderController {

    @Value("${server.port}")
    private int port;

    @GetMapping("/order")
    public String test1() {
        return "orders 服务OK,当前提供服务的端口是:" + port;
    }

}

使用RestTemplate调用服务时,我们可以使用如下2种方式:

1.1.1 自己维护RestTemplate实例

在用户服务中调用订单服务:

/**
 * @title: 用户服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class UserController {
	// 1.1自己手动new RestTemplate()
    @GetMapping("/user")
    public String test1() {
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.getForObject("http://localhost:9998/order", String.class);
        return result;
    }
}
1.1.2 由Spring工厂维护Rest Template实例

配置工厂实例:

package com.jitazheng.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @title: Bean实例配置
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@Configuration
public class BeansConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

用户服务调用订单服务:

/**
 * @title: 用户服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class UserController {

	@Autowired
    private RestTemplate restTemplate;
    
	// 1.2 将RestTemplate注册到工厂中
    @GetMapping("/user")
    public String test2() {
        System.out.println("user 服务被调用了。。。");
        String result = restTemplate.getForObject("http://localhost:9998/order", String.class);
        return result;
    }
}
1.1.3小结

RestTemplate是直接基于服务地址调用没有在服务注册中心获取服务,也没有办法完成服务的负载均衡如果需要实现服务的负载均衡需要自己书写服务负载均衡策略。

那么要如何从注册中心拿到可用的服务呢?

1.2 基于Ribbon的服务调用

  • 官方网址: https://github.com/Netflix/ribbon
  • Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
  • 说明:
    1.如果使用的是eureka client 和 consul client,无须引入依赖,因为在eureka,consul中 默认集成了ribbon组件
    2.如果使用的client中没有ribbon依赖需要显式引入如下依赖
<!--引入ribbon依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

本案例中,使用了consul,所以默认已经引入了Ribbon
在这里插入图片描述
使用RestTemplate + Ribbon进行服务间的调用的3种方式:

  • 方式1:使用Discovery client 进行客户端调用;
  • 方式2:使用LoadBalanceClient 进行客户端调用;
  • 方式3:使用@LoadBalanced 进行客户端调用。
    方式1:
    既然所有的微服务都在注册中心进行了注册,那么我们肯定要从注册中心获取要调用的服务,这才符合微服务的理念。因此我们使用DiscoveryClient从注册中心获取服务,然后使用RestTemplate调用服务的组合从而完成服务间的调用。
/**
 * @title: 用户服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class UserController {

	@Autowired
    private RestTemplate restTemplate;
    
	// Spring工厂已经创建了DiscoveryClient实例
	@Autowired
    private DiscoveryClient discoveryClient;
    
	@GetMapping("/user")
    public String test3() {
        // 根据服务名获取到n个服务
        List<ServiceInstance> orderServiceList = discoveryClient.getInstances("orders");
        orderServiceList.forEach(serviceInstance -> {
            System.out.println("服务主机:"+serviceInstance.getHost()+",端口:"+serviceInstance.getPort()+", 服务地址:"+serviceInstance.getUri());
        });
        //我们自定义的负载均衡策略:每次只取可用服务集合中的第1个
        String result = restTemplate.getForObject(orderServiceList.get(1).getUri() + "/order", String.class);
        return result;
    }
}

使用DiscoveryClient + RestTemplate的方式虽然可以解决服务间的调用,但是DiscoveryClient从注册中心获取服务没有做到负载均衡,而自定义的负载均衡策略总归是太low,这时该怎么办?
答案是:使用LoadBalancerClient的负载均衡及方式2
方式2:

/**
 * @title: 用户服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class UserController {

	@Autowired
    private RestTemplate restTemplate;
    
	@Autowired
    private LoadBalancerClient loadBalancerClient;
    
	// 使用LoadBalancerClient的负载均衡
    @GetMapping("/user")
    public String test4() {
        System.out.println("user 服务被调用了。。。");
        // 默认使用了轮询的策略
        ServiceInstance serviceInstance = loadBalancerClient.choose("orders");
        System.out.println("服务主机:"+serviceInstance.getHost()+",端口:"+serviceInstance.getPort()+", 服务地址:"+serviceInstance.getUri());
        String result = restTemplate.getForObject(serviceInstance.getUri() + "/order", String.class);
        return result;
    }
}

当然,我们可以修改负载均衡的策略,在调用方(本例中是用户服务)application.properties中配置:
在这里插入图片描述
方式3:
在把RestTemplate交给Spring容器管理时,添加一个注解@LoadBalanced,该注解的作用就是让RestTemplate具有负载均衡的特性:

package com.jitazheng.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @title: Bean实例配置
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@Configuration
public class BeansConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

调用订单服务:

/**
 * @title: 用户服务Controller
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class UserController {

	@Autowired
    private RestTemplate restTemplate;
    
	//使用Ribbon提供的一个基于HTTP和TCP的负载均衡工具,已经被Springcloud封装,同时使用@LoadBalanced注解 作用:可以让对象具有ribbon负载均衡特性
    @GetMapping("/user")
    public String test5() {
        System.out.println("user 服务被调用了。。。");
        String result = restTemplate.getForObject("http://orders/order", String.class);
        return result;
    }
}

在这里插入图片描述

1.3 Openfeign组件

思考:使用RestTemplate+ribbon已经可以完成对端的调用,为什么还要使用feign?
存在问题:

  • 1.每次调用服务都需要写这些代码,存在大量的代码冗余;
  • 2.服务地址如果修改,维护成本增高;
  • 3.使用时不够灵活。

Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,默认实现了负载均衡的效果并且springcloud为feign添加了springmvc注解的支持。
官网地址:https://cloud.spring.io/spring-cloud-openfeign/reference/html/
Open的使用步骤:
这里我们创建2个微服务:
类别服务springcloud-06category;
商品服务springcloud-07product。
同样的,将2个服务注册到consul注册中心。
在这里插入图片描述
使用Openfeign调用服务的一般流程示意图:
在这里插入图片描述

在这里插入图片描述

商品服务:
在这里插入图片描述

在类别服务中:
在这里插入图片描述

1.3.1 添加依赖和注解
<!--OpenFeign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在调用方开启Openfeign调用服务:

package com.jitazheng;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @title: 分类入口程序
 * 注意:如果添加了OpenFeign的注解@EnableFeignClients,可以不用添加作为服务注册客户端的注解@EnableDiscoveryClient
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@SpringBootApplication
//@EnableDiscoveryClient
@EnableFeignClients
public class CategoryApplication {

    public static void main(String[] args) {
        SpringApplication.run(CategoryApplication.class, args);
    }

}
1.3.2 在调用方编写接口

在调用方(本例中为类别服务)编写接口:
创建接口ProductClient:

package com.jitazheng.feignclient;

import com.jitazheng.entity.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * @title: 调用商品服务接口
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@FeignClient("PRODUCT")
public interface ProductClient {

    @GetMapping("/test")
    String test(@RequestParam String name, @RequestParam Integer age);

}

这里,特别注意:
1. 服务调用方入口类添加注解@EnableFeignClients,如果添加了OpenFeign的注解@EnableFeignClients,可以不用添加作为服务注册客户端的注解@EnableDiscoveryClient

2. 接口类上使用注解@FeignClient(被调用服务的服务名),表明该接口是要调用哪个服务;

3. 在调用方(本例中为ProductClient)中定义的方法名可以和被调用方(本例中为ProductController)中定义的方法名不同,但是,接收的参数类型必须相同,因为Openfeign实际上是一个伪HTTP客户端,如果我们在参数中不使用注解指定请求参数是从请求参数中获取?从请求体中获取?从请求路径中获取?那么到了底层,是无法知道参数从哪里获取的。因此我们必须严格遵守。
如果不遵守3,没有使用注解指定参数的获取方式,则可能出现如下错误:
在这里插入图片描述

1.3.3 编写调用方和被调用方的controller

调用方controller,本例中为类别服务controller:

package com.jitazheng.controller;

import com.jitazheng.feignclient.ProductClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class CategoryController {

    @Autowired
    private ProductClient productClient;

	@GetMapping("/category")
    public String test() {
        System.out.println("Category 服务list()方法被调用了。。。");
        String result = productClient.test("萧炎", 23);
        return "category ok!" + result;
    }
}

被调用方controller,本例中为商品服务Controller:

package com.jitazheng.controller;

import com.jitazheng.entity.Product;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/6
 * @version: v1.0.0
 */
@RestController
public class ProductController {

    @Value("${server.port}")
    private int port;

    @GetMapping("/test")
    public String test(String name, Integer age) {
        System.out.println("name:" + name + "age:" + age);
        return "test ok, 当前服务的端口为:" + port;
    }

}

1.4 使用Openfeign的其它注意点和配置

1.4.1 配置超时时间

Openfeign默认调用服务1s内要得到被调用服务的响应,但是实际生产中,往往业务逻辑执行时间会大于1s,此时可能出现以下错误:
在这里插入图片描述
要解决超时问题,需要在调用方添加如下配置:

# 修改OpenFeign的超时时间
# 配置指定客户端服务的连接超时时间(毫秒)
feign.client.config.PRODUCT.connectTimeout=5000
# 配置指定客户端服务的等待超时时间(毫秒)
feign.client.config.PRODUCT.readTimeout=2000

说明一下:
在这里插入图片描述

记录一下:我这里被调用的服务开了2个,模拟让服务方调用被服务方的服务一次,但是发现实际上由于被调用的服务处理复杂的业务逻辑致使响应时间过长,超过了设定时间而导致被服务方被调用了多次的结果。我猜测可能是由于OpenFeign在发现被调用方集群中的一个服务响应超时后,会根据自己的一套策略调用另外的一个服务导致的。如下图所示:
在这里插入图片描述
我担心在实际业务中,由于每个服务部署在集群里,一个集群中的服务调用另外一个集群中的正常服务时,由于服务没有在设定的时间内响应,而导致调用方可能错误的认为被调用的服务不可用,然后根据一些策略又会调用集群中的另外的服务,从而导致调用了服务的重复调用问题。到目前我还不知道怎么处理!!!

1.4.2 开启日志展示
# OpenFeign日志
# 开启指定服务的日志展示
feign.client.config.PRODUCT.loggerLevel=full
logging.level.com.jitazheng.feignclient=debug

在这里插入图片描述
测试服务日志的功能,可以看到详细的请求内容:
在这里插入图片描述

2. 微服务间熔断组件Hystrix

2.1 Hystrix是什么?

In a distributed environment, inevitably some of the many service dependencies will fail. Hystrix is a library that helps you control the interactions between these distributed services by adding latency tolerance and fault tolerance logic. Hystrix does this by isolating points of access between the services, stopping cascading failures across them, and providing fallback options, all of which improve your system’s overall resiliency.
以上来自官方文档:https://github.com/Netflix/Hystrix/wiki/
中文翻译:在分布式环境中,众多独立服务中的一些服务不可避免的会调用失败。Hystrix是一个库,它通过添加延迟容忍和容错逻辑,帮助我们控制分布式服务之间的交互。Hystrix能够做到这些就是通过隔离服务之间的访问点,停止服务之间的级联故障以及提供访问故障服务时可以提供快速的回退选项,以上种种提高了系统的整体弹性的稳定可靠。
总结来看Hystrix用来保护微服务系统,作用主要有3种:

  • 解决服务雪崩问题;
  • 提供服务降级功能;
  • 提供服务服务熔断功能。
2.2.1 服务雪崩

在微服务之间进行服务调用是由于某一个服务故障,导致级联服务故障的现象,称为雪崩效应。雪崩效应描述的是提供方不可用,导致消费方不可用并将不可用逐渐放大的过程。
服务雪崩图解:
在这里插入图片描述

2.2.2 服务降级

服务压力剧增的时候根据当前的业务情况及流量对一些服务和页面有策略的降级,以此缓解服务器的压力,以保证核心任务的进行。同时保证部分甚至大部分任务客户能得到正确的响应。也就是当前的请求处理不了了或者出错了,给一个默认的返回。

服务降级的目的就是保证核心业务服务运行正常,边缘业务可暂停运行,当用户访问边缘业务时,给予适当的响应。

2.2.3 服务熔断

“熔断器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器(hystrix)的故障监控,某个异常条件被触发,直接熔断整个服务。向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,就保证了服务调用方的线程不会被长时间占用,避免故障在分布式系统中蔓延,乃至雪崩。如果目标服务情况好转则恢复调用。服务熔断是解决服务雪崩的重要手段。
在这里插入图片描述

2.2 使用Hystrix实现熔断和降级

2.2.1 实现熔断功能

Hystrix的maven依赖:

<!--引入Hystrix依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

创建2个项目,并注册到consul注册中心。
在这里插入图片描述
项目结构如下:
springcloud_08hystrix是被调用的一方:
在这里插入图片描述
spring_09openfeign_hystrix是调用服务的一方:
在这里插入图片描述
编写OpenfeignClient接口:

package com.jitazheng.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @title: 调用方接口
 * @author: devinChen
 * @date: 2022/11/8
 * @version: v1.0.0
 */
@FeignClient(value = "HYSTRIX")
public interface HystrixClient {

    @GetMapping("/demo1")
    String demo1();

}

编写调用方Controller:

package com.jitazheng.controller;

import com.jitazheng.feignclient.HystrixClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/8
 * @version: v1.0.0
 */
@RestController
public class OpenfeignHystrixController {

    @Autowired
    private HystrixClient hystrixClient;

    @GetMapping("/test")
    public String test1() {
        System.out.println("调用了 test1()方法...");
        String result = hystrixClient.demo1();
        return "OpenfeignHystrix 服务调用了" + result;
    }

}

编写被调用方Controller及被调用方服务不可用时的处理:

package com.jitazheng.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/8
 * @version: v1.0.0
 */
@RestController
public class HystrixController {

    @Value("${server.port}")
    private int port;

	//  @HystrixCommand作用:指定该方法发生异常时,如何处理
	// 处理异常的方法可以和出现异常的方法名不一致,如该例子种的demo1FallBack()和demo1(),但是方法
	// 的参数要一致
    @GetMapping("/demo1")
    @HystrixCommand(fallbackMethod = "demo1FallBack")
    public String demo1() {
		// 模拟服务发生异常
        int i = 1 / 0;
        System.out.println("Hystrix 服务 demo1 ok");
        return "调用Hystrix ok, 当前提供服务的端口:" + port;
    }

    public String demo1FallBack() {
        return "抱歉,Hystrix处理失败,请稍后重试!";
    }

}

按照上述调用服务方法,因为被调用的服务出现了异常,会出现以下结果:
在这里插入图片描述
服务熔断总体思路如下:
在这里插入图片描述

2.2.2 实现服务降级

有时被调用的服务由于某种原因直接宕机,导致服务调用方返回如下结果:
在这里插入图片描述
此时调用方对被调用方的情况毫不知情,此时应该怎么办?
答案:在服务的调用方也使用Hystrix来监测被调用方的状态。若服务被调用方不可用,调用方要立即知晓并结束服务调用,给出可处理的响应,保证服务方不至于给用户抛出难以理解的响应。
参照2.2.1,我们只需要改造调用方的OpenfeignClient的接口即可:
以本例所示:

  1. 添加配置:
# 在使用Openeign调用服务时,开启Hystrix的支持(默认时关闭的)
feign.hystrix.enabled=true
  1. 定义一个类HystrixClientImpl实现接口HystrixClient:
package com.jitazheng.feignclient.impl;

import com.jitazheng.feignclient.HystrixClient;
import org.springframework.context.annotation.Configuration;

/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/8
 * @version: v1.0.0
 */
@Configuration
public class HystrixClientImpl implements HystrixClient {

    @Override
    public String demo1() {
        return "调用的服务不可用或服务响应时间过长,请稍后再试!";
    }

  1. 修改接口HystrixClient :
package com.jitazheng.feignclient;

import com.jitazheng.feignclient.impl.HystrixClientImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @title:
 * @author: devinChen
 * @date: 2022/11/8
 * @version: v1.0.0
 */
@FeignClient(value = "HYSTRIX", fallback = HystrixClientImpl.class)
public interface HystrixClient {

    @GetMapping("/demo1")
    String demo1();
}

在这里插入图片描述
此时再次启动服务,被调用的服务不用启动,会发现页面返回结果变成了如下:
在这里插入图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值