美文网首页Spring Cloud javaWeb学习
Spring Cloud OpenFeign 重试造成插入多条数

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

作者: sprainkle | 来源:发表于2019-05-14 14:34 被阅读88次

    问题描述

    我们在调试接口时,接口很容易超时,当然线上环境因为网络抖动、接口响应慢等,也造成接口超时,强大的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,源码如下:

    FeignClient

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

    FeignClientsConfiguration

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

    Retryer

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

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

    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,关键代码如下:

    RequestSpecificRetryHandler

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

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

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

    • 建立连接超时: 是发生在与服务端建立连接时出现超时;对应ribbon配置:ribbon.ConnectTimeout
    • 接口请求超时是在连接建立成功的前提下,服务端处理超时、或接受响应超时等;对应ribbon配置:ribbon.ReadTimeout。
    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 OpenFeign 重试造成插入多条数

        本文链接:https://www.haomeiwen.com/subject/uwrdaqtx.html