Spring Cloud OpenFeign 重试造成插入多条数据

问题描述

我们在调试接口时,接口很容易超时,当然线上环境因为网络抖动、接口响应慢等,也造成接口超时,强大的feign也提供了超时/失败重试功能,然而,请求重试必须建立在该接口具备幂等性的前提下,一般情况下,Get请求都是幂等的,但是如果是Post请求呢,重试后会是什么结果?

如果接口没有保证幂等性,那么重试Post请求(假设重试2次),就相当于成功调用了2次接口,数据库也就多出了2条记录。这显然不是我们希望看到的。下面给出解决方案。

springcloud openfeign版本

spring-cloud-starter-openfeign:2.0.1.RELEASE

解决方案

1. 修改ribbon配置只针对Get请求重试

### 请求处理的超时时间
ribbon:
  # 等待请求响应的超时时间. 单位:ms
  ReadTimeout: 5000
  # 连接超时时间. 单位:ms
  ConnectTimeout: 1000
  # 是否对所有请求进行失败重试, 设置为 false, 让feign只针对Get请求进行重试. 
  OkToRetryOnAllOperations: false

其实,Feign本身默认是没有开启重试的,具体可见下文的源码分析。

2. 修改ribbon的重试次数

ribbon:
  # Max number of retries on the same server (excluding the first try)
  MaxAutoRetries: 0
  # Max number of next servers to retry (excluding the first server)
  MaxAutoRetriesNextServer: 0

同一个服务实例的重试次数(MaxAutoRetries)、不同服务实例(MaxAutoRetriesNextServer)的重试次数都设置为0,即可达到不重试的目的。

3. 关闭重试机制

spring:
    cloud:
        loadbalancer:
            retry:
                enabled: false

终极大招,索性把重试直接关闭,不过这种是最不推荐的做法。

源码分析

Feign自带的重试机制

首先,在Spring Cloud OpenFeign中,入口为FeignClient,源码如下:

Spring Cloud OpenFeign 重试造成插入多条数据_第1张图片
FeignClient

所有属性性中,只有configuration看着跟重试有点关系,再看到javadoc提示的@see FeignClientsConfiguration for the defaults,该配置为Feign的默认配置,源码如下:

Spring Cloud OpenFeign 重试造成插入多条数据_第2张图片
FeignClientsConfiguration

终于见到Retryer本尊,可以看到,当容器中缺少实现Retryer接口的Bean时则自动生成实例并注入(@ConditionalOnMissingBean的功劳),这里注入的是Retryer.NEVER_RETRY,一个不进行重试的Retryer实现类。实现为:

Spring Cloud OpenFeign 重试造成插入多条数据_第3张图片
Retryer

Retryer有2个方法,重点放在continueOrPropagate(RetryableException e)上,从方法签名上看,要么等待下一次重试,要么将异常Propagate(传播)出去,通俗点就是抛异常。

大家应该都知道,Feign底层使用的是动态代理,才能实现如此简单易用的使用体验。这里主要讲一个类:SynchronousMethodHandler,该类动态代理的主要类,所以它的属性跟Feign的参数很相似,主要源码如下:

Spring Cloud OpenFeign 重试造成插入多条数据_第4张图片
SynchronousMethodHandler and Feign

invoke(Object[] argv)方法中,当捕获RetryableException异常时,会调用RetryercontinueOrPropagate方法,根据执行结果,该重试的重试,该抛异常的抛异常。

而根据NERVER_RETRY的源码,就是直接抛异常,不进行重试。

那就奇怪了,既然重试策略为NERVER_RETRY,为何接口调用超时还是会重试呢?

依赖Ribbon的重试机制

Spring Cloud OpenFeign 默认是使用Ribbon实现负载均衡和重试机制的,虽然feign有自己的重试机制,但该功能在Spring Cloud OpenFeign基本用不上,除非有特定的业务需求,则可以实现自己的Retryer,然后在全局注入或者针对特定的客户端使用特定的Retryer

对于Spring Cloud OpenFeign的重试机制,这里主要说明两个类:FeignLoadBalancerRequestSpecificRetryHandler,关键代码如下:

Spring Cloud OpenFeign 重试造成插入多条数据_第5张图片
RequestSpecificRetryHandler

首先分析一下接口RetryHandler(属于ribbon),关键方法isRetriableException(Throwable e, boolean sameServer),用于判断此次请求失败抛出的异常是否需要重试。其实现类RequestSpecificRetryHandler有2个比较重要的参数:okToRetryOnAllErrorsokToRetryOnConnectErrors

  • okToRetryOnAllErrors:为true时,无论是接口请求超时、服务端处理失败、建立连接失败等,统一返回true,即可以重试;
  • okToRetryOnConnectErrors:为true时,只要是在跟服务端建立连接时出现错误,无论建立连接超时、建立连接失败等,统一返回true。

注:这里有2个超时概念,建立连接超时接口请求超时

  • 建立连接超时: 是发生在与服务端建立连接时出现超时;对应ribbon配置:ribbon.ConnectTimeout
  • 接口请求超时是在连接建立成功的前提下,服务端处理超时、或接受响应超时等;对应ribbon配置:ribbon.ReadTimeout。

Spring Cloud OpenFeign 重试造成插入多条数据_第6张图片
FeignLoadBalancer

FeignLoadBalancer在获取请求重试处理器时,根据不同情况实例化不同的 RequestSpecificRetryHandler,首先 okToRetryOnConnectErrors参数都为 true;而 okToRetryOnAllErrors参数,有2种情况:
第一种情况,当配置 ribbon.OkToRetryOnAllOperationstrue时, okToRetryOnAllErrors始终为 true
第二种情况:根据请求的 HTTP_METHOD取不同的值,当为 Get请求时为 true,其他都为 false,不难理解, Get请求一般都是幂等的,而其他请求则不一定。

因此,ribbon.OkToRetryOnAllOperations这个参数,强烈建议设置为false

扩展

1. 对不同服务使用不同的重试机制

上面的配置ribbon.**,即以ribbon开头的配置,是针对全局的,如果需要对不同服务定制化配置呢?参考如下:

# 针对 app1
app1:
  ribbon:
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 1
# 针对 app2
app2:
  ribbon:
    MaxAutoRetriesNextServer: 2
# 针对 app3
app3:
   ribbon:
     MaxAutoRetries: 1
  1. feign使用自定义配置(包含Retryer)
feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

上面的feignName,一般为远端服务的服务名。因为Spring Cloud 有自己的服务发现,通过服务名就能定位到该服务的可用实例列表,再通过负载均衡策略选取其中一个实例,最后向该服务实例发起请求。

上面的配置是针对某个服务的,而其他服务的配置可能基本都一样,这时,只需要将feignName替换成default即可。这样即可全局自定义配置。

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

参考

spring-cloud-feign-overriding-defaults
https://github.com/Netflix/ribbon/wiki/Getting-Started
SpringCloud Feign重试详解
feign 的重试机制

推荐阅读

Spring Cloud 进阶玩法

你可能感兴趣的:(Spring Cloud OpenFeign 重试造成插入多条数据)