Feign的详解与使用

Feign详解与使用

一、什么是Feign

1、概念

​ 学过了Ribbon和Hystrix的读者都知道,Ribbon实现客户端负载均衡是通过拦截RestTemplate进行相应的处理。而RestTemplate已经对Http请求进行了一系列模板式的封装,我们使用的时候,通常会基于RestTemplate进行封装,形成相应的业务接口。

​ 但是,这个封装业务接口的过程,需要一定的工作量;如果能有声明式的调用那么就能在开发中大大减少开发量。于是,出现了Feign,这是基于Netflix Feign开源组件进行适配SpringMVC,添加了注解支持(一些地方会有小许差别)的组件,该组件即有客户端负载均衡的功能,也有熔断器的服务降级等功能。

​ 在我看来,它最大的好处是基于声明式开发方式,能够使得代码优雅、简洁,开发量小

2、实现依赖

​ 上面,我们说了,它拥有客户端负载均衡、熔断器的服务降级等功能,那么,它是怎么实现的呢?其实,它是将Ribbon和Hystrix作为底层支持,进行高层封装,并且整合SpringMVC等功能实现的一个组件。

  • Ribbon
  • Hystrix

以上两个组件就是Feign的实现依赖。

二、Feign的使用

接下来,讲讲Feign的使用。

  • 引入依赖
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>
  • 启动类注册Bean
//添加这个注解
@EnableFeignClients

上面我一直在强调,Feign是声明式开发,接下来,就展示一下使用Feign的声明式开发究竟有多少优雅、简洁。

  • 基于Feign的声明式开发可以分为如下三步:
    • 创建业务消费接口
    • 根据相应的消费接口进行参数的绑定
    • 调用业务接口

创建业务接口:

@FeignClient(value = "provider")
public interface FeignService {
    /**
     * to server
     *
     * @return s
     */
    @GetMapping("/zone")
    String feignService();

    /**
     * @param id 通过参数请求
     * @return 用户信息
     */
    @GetMapping("/queryUserById")
    User queryUserById(@RequestParam"id") String id);
}
//用户类
public class User {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}
	//以下是在控制层调用对应的业务方法
	@AutoWired
    private FeignService service;
    @GetMapping("/consumer")
    public String test() {
        return service.feignService();
    }

以上的例子就是feign在微服务中的入门级别应用,当然,Feign的使用是基于Ribbon和Hystrix,那么,我们是无需对Feign进行单独配置,只需要修改对应的Ribbon和Hystrix的配置即可。如果对于那两个组件不熟悉,可以看我关于那两个组件的文章,里面讲解得足够详细。

三、Feign配置

  • 客户端配置
//这是最简单的客户端指定服务配置
@FeignClient(value = "provider")
  • 客户端指定fallBack(服务降级)
//指定fallBack属性的实现类
@FeignClient(value = "provider",fallback = HystrixServiceCompenent.class)
public interface FeignService {
  //这里要注意,value属性名不能省略,虽然在MVC中可以,但是在feign中不行。
    @GetMapping(value="/zone")
    String feignService();
    @GetMapping(value="/queryUserById")
    User queryUserById(@RequestParam String id);
}
//以下就是实现服务降级的组件。
@Component
public class HystrixServiceCompenent implements FeignService {
    @Override
    public String feignService() {
        return "sorry" + "this service is fail, by_hystrix";
    }
    @Override
    public User queryUserById(String id) {
        return null;
    }
}
  • 全局配置
#全局配置是最为简单的,如配置ribbon
ribbon.<key>=<value>
#example
ribbon.ConnectTimeout=500
ribbon.ReadTimeout=5000
#以下是hystrix的全局配置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
#关闭Hystrix
feign.hystrix.enabled=false
  • 指定服务实例配置
#指定服务进行配置,需要指定服务名
<service>.ribbon.<key>=<value>
#example
provider.ribbon.ConnectTimeout=500
  • 重试机制(Retry)(这是ribbon组件)
#更换实例后的重试次数为2
provider.ribbon.MaxAutoRetrikesNextServer=1
#重试策略首先访问首选服务一次,失败后再重试一次
provider.ribbon.MaxAutoRetries=1

上面的参数计算重试的次数:MaxAutoRetries+MaxAutoRetriesNextServer+(MaxAutoRetries *MaxAutoRetriesNextServer)=3次重试;那么包括第一次,则为4次。

注意:Hystrix的超时时间必须大于Ribbon的超时时间,否则触发了Hystrix的熔断命令之后,重试机制已经失效。即,运行过程应该是先进行请求,如果ribbon超时,则进行重试,如果再超时,超过Hystrix超时时间,就会触发熔断,从而进行服务降级等一系列操作。

  • 局部禁用hystrix(有时候不希望全局禁用,比如跨zone调用服务的时候)
/**
 * @author linxu
 * @date 2019/7/16
 * 用于禁用某个Feign客户端的Hystrix组件
 * 如果想要开启,这个@Configuration注解必须注释掉,否则会出现不生效的情况。
 */
@Configuration
public class DisableHystrixConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(){
        return Feign.builder();
    }
}
//在客户端中,只需要指定配置就达到禁用的效果。
@FeignClient(value = "provider",configuration = DisableHystrixConfiguration.class)
  • 请求压缩
#开启压缩
feign.compression.request.enabled=true
feign.compression.response.enabled=true
#设置压缩参数以下两个都是默认值
feign.compression.request.mime-type=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
  • Hystrix的配置更改
#在Hystrix中,我们采用了声明式配置,通过注解进行配置的修改;但是,Feign在取消了这一做法,改为配置文件配置;如下,是线程池的配置。
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: THREAD
  threadpool:
      default:
         coreSize: 20
       #这个是feign客户端的名称
      <client-Name>:
         coreSize: 10

四、Feign必须了解的源码

  • 不要使用Feign中的Retryer(使用ribbon的即可)
//可以自己实现这个,实现自己的Retryer.它的默认实现为5次重试。但是现在已经默认关闭,为了不与ribbon重叠
public interface Retryer extends Cloneable {
  public static class Default implements Retryer {

    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;
	//可以看到,默认为5次重试
    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;
      this.maxPeriod = maxPeriod;
      this.maxAttempts = maxAttempts;
      this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
      return System.currentTimeMillis();
    }
	//重试规则;超过次数,则通过抛出重试异常,取消尝试。
    public void continueOrPropagate(RetryableException e) {
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
        throw e;
      }
      sleptForMillis += interval;
    }

 	
    long nextMaxInterval() {
      long interval = (long) (period * Math.pow(1.5, attempt - 1));
      return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
  }
  //这里就是默认关闭feign中的retry。官方都关了,我们就没有必要去开启了。
    Retryer NEVER_RETRY = new Retryer() {
    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }
    @Override
    public Retryer clone() {
      return this;
    }
  };
  
}
  • OKToRetryOnAllOperations选项带来的幂等问题
//基于负载均衡策略对请求进行封装。
public class FeignLoadBalancer extends
		AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
	private final RibbonProperties ribbon;
  //获取对应ribbon的一个超时设置
	protected int connectTimeout;
	protected int readTimeout;
	protected IClientConfig clientConfig;
	protected ServerIntrospector serverIntrospector;
	public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
			ServerIntrospector serverIntrospector) {
		super(lb, clientConfig);
		this.setRetryHandler(RetryHandler.DEFAULT);
		this.clientConfig = clientConfig;
		this.ribbon = RibbonProperties.from(clientConfig);
		RibbonProperties ribbon = this.ribbon;
		this.connectTimeout = ribbon.getConnectTimeout();
		this.readTimeout = ribbon.getReadTimeout();
		this.serverIntrospector = serverIntrospector;
	}
	//运行请求的执行封装
	@Override
	public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
			throws IOException {
		Request.Options options;
      //配置重写,则获取重写后的配置
		if (configOverride != null) {
			RibbonProperties override = RibbonProperties.from(configOverride);
			options = new Request.Options(override.connectTimeout(this.connectTimeout),
					override.readTimeout(this.readTimeout));
		}
		else {
			options = new Request.Options(this.connectTimeout, this.readTimeout);
		}
      //封装响应内容
		Response response = request.client().execute(request.toRequest(), options);
		return new RibbonResponse(request.getUri(), response);
	}
	//重试的控制方法
	@Override
	public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
			RibbonRequest request, IClientConfig requestConfig) {
      //就是这个地方;
		if (this.ribbon.isOkToRetryOnAllOperations()) {
          //如果为true,默认对所有方法进行包括:超时、出现异常报错都进行重试。
			return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
					requestConfig);
		}
		if (!request.toRequest().httpMethod().name().equals("GET")) {
          //在false基础上,如果不是GET,只会进行连接超时的重试
			return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
					requestConfig);
		}
		else {
          //在false基础上,是GET,无论是超时、异常都会进行重写。(切记,不要在get方法中写数据的增、改、删请求)
			return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
					requestConfig);
		}
	}
}

 public RequestSpecificRetryHandler(boolean okToRetryOnConnectErrors, boolean okToRetryOnAllErrors, RetryHandler baseRetryHandler, @Nullable IClientConfig requestConfig) {
        Preconditions.checkNotNull(baseRetryHandler);
        this.okToRetryOnConnectErrors = okToRetryOnConnectErrors;
        this.okToRetryOnAllErrors = okToRetryOnAllErrors;
        this.fallback = baseRetryHandler;
        if (requestConfig != null) {
            if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
                retrySameServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetries); 
            }
            if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
                retryNextServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer); 
            } 
        }
    }

以上就是feign中必须了解的两个源码实现,如果不了解这两个源码实现,feign的使用中将会遭遇很多坑。

五、Feign的注意事项

  • Feign客户端默认是关闭重试功能,因为该功能会与Ribbon的重试机制重叠。

    • 建议不要开启Feign的重试机制,毫无意义。
  • 使用Feign中的Ribbon重试机制,需要注意:

    • ribbon的超时时间只有小于feign.hystrix的超时时间才会重试,否则触发熔断。

    • ribbon重试的次数计算如下:

      MaxAutoRetries+MaxAutoRetriesNextServer+(MaxAutoRetries *MaxAutoRetriesNextServer)
      
    • OkToRetryOnAllOperations选项记得关闭,否则会触发其它post、put方法的重试(不是幂等),则会出现数据问题。默认只对Get方法进行重试。

  • Hystrix的超时计算,应该满足如下公式: (1 + MaxAutoRetries + MaxAutoRetriesNextServer) * Ribbon.ReadTimeout

  • 默认情况下,GET方式请求无论是连接异常还是读取异常,都会进行重试,非GET方式请求,只有连接异常时,才会进行重试,避免了非幂等问题。

发布了57 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/rekingman/article/details/96315587