Spring Cloud OpenFeign 重試造成插入多條數(shù)據(jù)

問題描述

我們在調(diào)試接口時,接口很容易超時,當然線上環(huán)境因為網(wǎng)絡抖動、接口響應慢等,也造成接口超時,強大的feign也提供了超時/失敗重試功能,然而,請求重試必須建立在該接口具備冪等性的前提下,一般情況下,Get請求都是冪等的,但是如果是Post請求呢,重試后會是什么結果?

如果接口沒有保證冪等性,那么重試Post請求(假設重試2次),就相當于成功調(diào)用了2次接口,數(shù)據(jù)庫也就多出了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

其實,F(xiàn)eign本身默認是沒有開啟重試的,具體可見下文的源碼分析。

2. 修改ribbon的重試次數(shù)

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

同一個服務實例的重試次數(shù)(MaxAutoRetries)、不同服務實例(MaxAutoRetriesNextServer)的重試次數(shù)都設置為0,即可達到不重試的目的。

3. 關閉重試機制

spring:
    cloud:
        loadbalancer:
            retry:
                enabled: false

終極大招,索性把重試直接關閉,不過這種是最不推薦的做法。

源碼分析

Feign自帶的重試機制

首先,在Spring Cloud OpenFeign中,入口為FeignClient,源碼如下:

FeignClient

所有屬性性中,只有configuration看著跟重試有點關系,再看到javadoc提示的@see FeignClientsConfiguration for the defaults,該配置為Feign的默認配置,源碼如下:

FeignClientsConfiguration

終于見到Retryer本尊,可以看到,當容器中缺少實現(xiàn)Retryer接口的Bean時則自動生成實例并注入(@ConditionalOnMissingBean的功勞),這里注入的是Retryer.NEVER_RETRY,一個不進行重試的Retryer實現(xiàn)類。實現(xiàn)為:

Retryer

Retryer有2個方法,重點放在continueOrPropagate(RetryableException e)上,從方法簽名上看,要么等待下一次重試,要么將異常Propagate(傳播)出去,通俗點就是拋異常。

大家應該都知道,Feign底層使用的是動態(tài)代理,才能實現(xiàn)如此簡單易用的使用體驗。這里主要講一個類:SynchronousMethodHandler,該類動態(tài)代理的主要類,所以它的屬性跟Feign的參數(shù)很相似,主要源碼如下:

SynchronousMethodHandler and Feign

invoke(Object[] argv)方法中,當捕獲RetryableException異常時,會調(diào)用RetryercontinueOrPropagate方法,根據(jù)執(zhí)行結果,該重試的重試,該拋異常的拋異常。

而根據(jù)NERVER_RETRY的源碼,就是直接拋異常,不進行重試。

那就奇怪了,既然重試策略為NERVER_RETRY,為何接口調(diào)用超時還是會重試呢?

依賴Ribbon的重試機制

Spring Cloud OpenFeign 默認是使用Ribbon實現(xiàn)負載均衡和重試機制的,雖然feign有自己的重試機制,但該功能在Spring Cloud OpenFeign基本用不上,除非有特定的業(yè)務需求,則可以實現(xiàn)自己的Retryer,然后在全局注入或者針對特定的客戶端使用特定的Retryer

對于Spring Cloud OpenFeign的重試機制,這里主要說明兩個類:FeignLoadBalancerRequestSpecificRetryHandler,關鍵代碼如下:

RequestSpecificRetryHandler

首先分析一下接口RetryHandler(屬于ribbon),關鍵方法isRetriableException(Throwable e, boolean sameServer),用于判斷此次請求失敗拋出的異常是否需要重試。其實現(xiàn)類RequestSpecificRetryHandler有2個比較重要的參數(shù):okToRetryOnAllErrorsokToRetryOnConnectErrors

  • okToRetryOnAllErrors:為true時,無論是接口請求超時、服務端處理失敗、建立連接失敗等,統(tǒng)一返回true,即可以重試;
  • okToRetryOnConnectErrors:為true時,只要是在跟服務端建立連接時出現(xiàn)錯誤,無論建立連接超時、建立連接失敗等,統(tǒng)一返回true。

注:這里有2個超時概念,建立連接超時接口請求超時

  • 建立連接超時: 是發(fā)生在與服務端建立連接時出現(xiàn)超時;對應ribbon配置:ribbon.ConnectTimeout
  • 接口請求超時是在連接建立成功的前提下,服務端處理超時、或接受響應超時等;對應ribbon配置:ribbon.ReadTimeout。

FeignLoadBalancer

FeignLoadBalancer在獲取請求重試處理器時,根據(jù)不同情況實例化不同的RequestSpecificRetryHandler,首先okToRetryOnConnectErrors參數(shù)都為true;而okToRetryOnAllErrors參數(shù),有2種情況:
第一種情況,當配置ribbon.OkToRetryOnAllOperationstrue時,okToRetryOnAllErrors始終為true
第二種情況:根據(jù)請求的HTTP_METHOD取不同的值,當為Get請求時為true,其他都為false,不難理解,Get請求一般都是冪等的,而其他請求則不一定。

因此,ribbon.OkToRetryOnAllOperations這個參數(shù),強烈建議設置為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 有自己的服務發(fā)現(xiàn),通過服務名就能定位到該服務的可用實例列表,再通過負載均衡策略選取其中一個實例,最后向該服務實例發(fā)起請求。

上面的配置是針對某個服務的,而其他服務的配置可能基本都一樣,這時,只需要將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 進階玩法

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容