0. 引言

上一期我们搭建了网关和配置中心、注册中心。如果还没有看过往期文章的可以先了解:
springcloud:什么是分布式微服务,如何学习微服务(一)
springcloud:微服务涉及哪些技术、有哪些核心组件(二)
springcloud:网关组件gateway详解(三)
springcloud:注册中心、配置中心组件nacos详解(四)
springcloud:保姆式教程-从零搭建微服务(五)

但是还有一个核心问题没有解决,那就是微服务间的通信问题。比如我们订单服务中需要调用商品列表数据,而商品列表数据是在商品服务中获取的,那么我们就需要在订单服务中调用商品服务的接口。这种服务间调用,就称为组间通信

目前常用的组件通信有RestTemplate和feign。RestTemplate的使用相对比较麻烦,需要写上服务IP等比较底层的信息,而feign则不需要,更加符合本地调用的方式。

本着最核心技术快速上手的理念,我们先针对更加常用的feign组件进行讲解。后续再详谈RestTemplate

1. Feign简介

Feign是声明式的web service客户端,它让服务间的调用更加简单。使得整个调用变得像本地调用一样简单。

Feign中包含了Hystrix和ribbon。所以feign支持接口负载均衡,以及接口的熔断、降级、限流等处理。并且支持Http请求和响应的压缩。

feignClient参数详解

参数名说明
value指定feignClient的名称,会注册成bean,也就是bean名,如果使用了注册中心,就是服务名
name同value
serviceId当要定义多个相同name的feignClient时,会报错,需要通过定义不同的serviceId来区分,新版本已弃用
contextId当要定义多个相同name的feignClient时,会报错,需要通过定义不同的serviceId来区分,会被用作bean名
qualifier指定@Qualifier注解的值,调用方就可通过该值来进行引用feignclient,当bean名相同时,用于标明唯一的bean。@Qualifier与@Autowired配合使用
url手动指定接口调用地址
decode404当接口请求404时,如果该字段为true,则会调用decoder进行解码(可在decoder中自定义错误类型、错误信息等),否则报错FeignException
configurationFeign配置类,可以自定义Feign的Encoder,Decoder等
fallback用于定义接口调用报错时的回调函数
fallbackFactory工厂类,用于生成fallback类示例
path定义当前feignClient的统一前缀
primary是否将外部代理标记为主bean,将当前类转换为bean,可以通过依赖注入引用

2. Feign使用

1、要使用feign,我们需要引入feign的依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId> 
            <version>2.2.6.RELEASE</version>
</dependency>

需要注意的是,Feign默认是POST请求,如果要支持带参数的GET请求,那么还需要引入feign-httpclient依赖

<dependency>
     <groupId>io.github.openfeign</groupId>
     <artifactId>feign-httpclient</artifactId>
     <version>10.10.1</version>
</dependency>

同时因为是web api,那么肯定还要引入Spring web依赖,已经引入过就不用重复引用了

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId> 
</dependency>

2、在调用方服务的启动类中添加注解@EnableFeignClients

2.1 通过IP直接调用服务

直接通过IP来调用的话只需要在调用方的服务中创建Feign接口类。其中name是服务名,url是实际的接口地址,如果有多个可以用逗号隔开

接口中书写的是对应到controller的URL地址。比如说用户controller中有一个list接口,url是list。那么Feign接口类中对应的方法也要和controller中的接口地址保持一致

@FeignClient(name = "order-server",url = "localhost:8081")
public interface OrderApi {
    
    @GetMapping("list")
    public List<Order> list();
    
}

有一个注意的点,如果controller中统一的接口前缀,比如说/user。在feign接口类中是不能通过@RequestMapping来申明的,而是必须在每个方法中添加上这个前缀,如下所示:

@FeignClient(name = "order-server",url = "localhost:8081")
public interface OrderApi {

    String PREFIX = "order";
    String LIST = PREFIX + "/list";
    String PAGE = PREFIX + "/page";

    @GetMapping(LIST)
    public List<Order> list();

    @GetMapping(PAGE)
    public List<Order> page();
    
}

2.2 通过nacos调用

直接使用服务名即可,无需定义url。在调用方中创建该接口即可

@FeignClient(name = "product-server")
public interface ProductApi {

    @GetMapping("list")
    public List<Product> list();
}

测试

我们基于上节课搭建的springcloud项目来进行测试,为了不影响上节课的代码,单独创建一个springcloud2用于本章测试

1、在order-server服务中引入依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>10.10.1</version>
        </dependency>
        <-- 因为要使用Product实体类,所以需要引入product-server -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>product-server-feign</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

2、在order-server中创建ProductApi接口类

@FeignClient(name = "product-server",url = "localhost:9091")
public interface ProductApi {

    @GetMapping("list")
    public List<Product> list();
}

3、在order-server启动类中添加@EnableFeignClients注解

4、在在order-server的OrderController中创建接口用于调用feign接口类中的方法

@RestController
@AllArgsConstructor
public class OrderController {
    private final IOrderService orderService;
    private final ProductApi productApi;

    @GetMapping("list")
    public List<Order> list(){
        return orderService.list();
    }

    @GetMapping("listProduct")
    public List<Product> listProduct(){
        return productApi.list();
    }

}

5、测试访问http://localhost/order-server/listProduct

可以看到我们在order服务用调用了product服务的list接口,其结果返回来商品列表信息

2.3 重试与超时

feign包含了ribbon,因此可以使用ribbon的重试、超时机制,只需要在配置文件中修改:

  • 重试配置
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false
  • 超时配置:这里的连接超时指的是连接接口的时间,而业务逻辑超时时间指的是连接成功后业务逻辑的执行时间
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000

2.4 服务熔断、降级

在配置熔断降级之前我们需要理解什么是服务熔断?什么是服务降级?

服务熔断:当连续连接失败次数达到阈值后,直接报错,之后的相同请求也直接报错,这个措施称为熔断。相当于我们常见的保险丝,当确定了服务不能访问时,就直接断开后续的连接

服务降级:当连续连接失败次数达到阈值或者超时后,给请求方一个保底信息,这个保底信息可以是:
(1)返回友好性的页面,比如重试按钮、联系邮箱或者一些基础的静态页面
(2)返回提示信息,并把数据写入到MQ中,后续通过MQ来重试
(3)返回友好性的提示信息,比如“稍后再来”、“服务忙,请稍后重试”等,不做其他处理
我们把以上这样的措施称为降级。

默认feign是不会用Hytrix来做服务降级等处理的,所以要使用的话需要在配置文件中开启:

feign.hystrix.enabled=true

2.4.1 降级配置

feign利用fallback类来实现请求失败后的回调方法
1、创建回调类

@Component
public class UserProviderBack implements UserApi {

    @Override
	public Map findById(@RequestParam("id") Integer id){
	// 降级,给出友好性提示
        return "服务走远了,请稍后重试";
    }
    
    @Override
	public Map getMap(@RequestParam Map<String,Object> map){
	// 降级,直接返回空数据
       return null;
   }	
 
}

2、声明回调类

@FeignClient(name="user-provider",fallback= UserProviderBack.class)
public interface UserApi {

    @GetMapping("/findById")
	public Map findById(@RequestParam("id") Integer id);
    
    @GetMapping("/getMap")
	public Map getMap(@RequestParam Map<String,Object> map);	
 
}

如果需要根据不同的错误类型来进行不同的降级处理,那么可以使用FallbackFactory

@Component
public class ProductFeignCallBackFactory implements FallbackFactory<ProductFeignNacos> {

    @Override
    public ProductFeignNacos create(Throwable cause) {
        return new ProductFeignNacos() {
            @Override
            public String getInfo() throws ClientException {
                if(cause instanceof FeignException.InternalServerError){
                    return "远程服务器挂掉咯";
                }else if(cause instanceof RuntimeException){
                    // 熔断
                    throw (RuntimeException) cause;
                }
                return "Cloud7DayProperties{'name':'兜底数据','price':0, 'size': 0}";
            }

        };
    }
}

同时@FeignClient里的调用也要改成fallbackFactory

@FeignClient(name = "product-server",
        contextId="product-server-nacos",
        fallbackFactory = ProductFeignCallBackFactory.class)
public interface ProductFeignNacos {
    ...
}

2.4.2 服务熔断

因为feign中附带了hytrix,我们通过hytrix来实现熔断

1、引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

2、启动类添加注解 @EnableHystrix

3、修改配置文件

# feign对hytrix的支持默认是关闭的
feign:
  hystrix:
    enabled: true # 降级熔断:开启hytrix
  circuitbreaker:
    enabled: true # 开启熔断

#在10秒内,发生20次以上的请求时,假如超时率达到50%以上,则断路器将被打开。
hystrix:
  command:
    default:  # 或者填写具体的服务名称,default默认为全部服务
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000       # 请求超时时间,单位毫秒
      circuitBreaker:
        sleepWindowInMilliseconds: 10000   # 窗口期,当触发熔断机制后指定时间内,此该接口不会再被访问
        requestVolumeThreshold: 20        # 请求阈值
        errorThresholdPercentage: 50      # 错误百分比阈值

2.5 限流

因为feign集成了hytrix,而hytrix可以通过信号量SEMAPHORE或线程THREAD来实现限流

信号量形式

# 熔断
#在10秒内,发生10次以上的请求时,假如超时率达到50%以上,则断路器将被打开。
hystrix:
  command:
    default:  # 或者填写具体的服务名称,default默认为全部服务
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000       # 请求熔断超时时间,单位毫秒
          strategy: SEMAPHORE # 通过信号量限流
        semaphore:
          maxConcurrentRequests: 10 # 允许的最大并发
     circuitBreaker:
        sleepWindowInMilliseconds: 10000     # 窗口期

线程池形式

# 熔断
#在10秒内,发生10次以上的请求时,假如超时率达到50%以上,则断路器将被打开。
hystrix:
  command:
    default:  # 或者填写具体的服务名称,default默认为全部服务
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000       # 请求熔断超时时间,单位毫秒
          strategy: THREAD # 通过线程池限流,默认值
  threadpool:
    default:
      coreSize: 1 # 并发执行的最大线程数,默认10
      maxQueueSize: 2 # BlockingQueue的最大队列数,默认值-1
      queueSizeRejectionThreshold: 2 #即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝,默认值5

总结

好了,本期我们针对feign组件的讲解就到这里了,针对限流的配置我们并不推荐使用feign来实现,如果要做也是在fallback做处理,后续会详细讲解微服务中的限流措施。文中的测试代码也已经放到了git项目中,欢迎大家下载体验

下期预告

1、微服务框架进阶讲解

更多推荐

springcloud:微服务如何通信?组件通信组件:feign详解(六)