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,源码如下:

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 进阶玩法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容