問題描述
我們在調(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
,源碼如下:
所有屬性性中,只有configuration
看著跟重試有點關系,再看到javadoc
提示的@see FeignClientsConfiguration for the defaults
,該配置為Feign
的默認配置,源碼如下:
終于見到Retryer
本尊,可以看到,當容器中缺少實現(xiàn)Retryer
接口的Bean
時則自動生成實例并注入(@ConditionalOnMissingBean
的功勞),這里注入的是Retryer.NEVER_RETRY
,一個不進行重試的Retryer
實現(xiàn)類。實現(xiàn)為:
Retryer
有2個方法,重點放在continueOrPropagate(RetryableException e)
上,從方法簽名上看,要么等待下一次重試,要么將異常Propagate(傳播)出去,通俗點就是拋異常。
大家應該都知道,Feign
底層使用的是動態(tài)代理,才能實現(xiàn)如此簡單易用的使用體驗。這里主要講一個類:SynchronousMethodHandler
,該類動態(tài)代理的主要類,所以它的屬性跟Feign
的參數(shù)很相似,主要源碼如下:
在invoke(Object[] argv)
方法中,當捕獲RetryableException
異常時,會調(diào)用Retryer
的continueOrPropagate
方法,根據(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
的重試機制,這里主要說明兩個類:FeignLoadBalancer
、RequestSpecificRetryHandler
,關鍵代碼如下:
首先分析一下接口RetryHandler
(屬于ribbon
),關鍵方法isRetriableException(Throwable e, boolean sameServer)
,用于判斷此次請求失敗拋出的異常是否需要重試。其實現(xiàn)類RequestSpecificRetryHandler
有2個比較重要的參數(shù):okToRetryOnAllErrors
、okToRetryOnConnectErrors
。
-
okToRetryOnAllErrors
:為true
時,無論是接口請求超時、服務端處理失敗、建立連接失敗等,統(tǒng)一返回true,即可以重試; -
okToRetryOnConnectErrors
:為true
時,只要是在跟服務端建立連接時出現(xiàn)錯誤,無論建立連接超時、建立連接失敗等,統(tǒng)一返回true。
注:這里有2個超時概念,建立連接超時、接口請求超時。
- 建立連接超時: 是發(fā)生在與服務端建立連接時出現(xiàn)超時;對應
ribbon
配置:ribbon.ConnectTimeout- 接口請求超時是在連接建立成功的前提下,服務端處理超時、或接受響應超時等;對應
ribbon
配置:ribbon.ReadTimeout。
而
FeignLoadBalancer
在獲取請求重試處理器時,根據(jù)不同情況實例化不同的RequestSpecificRetryHandler
,首先okToRetryOnConnectErrors
參數(shù)都為true
;而okToRetryOnAllErrors
參數(shù),有2種情況:第一種情況,當配置
ribbon.OkToRetryOnAllOperations
為true
時,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
- 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 的重試機制