工作中,經常遇到需要重試的場景,最簡單的方式可以用try...catch...加while迴圈來實現。那麼,有沒有統一的、優雅一點兒的處理方式呢?有的,Spring Retry就可以幫我們搞定重試問題。
關於重試,我們可以關注以下以下幾個方面:
- 什麼情況下去觸發重試機制
- 重試多少次,重試的時間間隔
- 是否可以對重試過程進行監視
接下來,帶著這些思考,一起看下Spring Retry是如何解決這些問題的
首先,引入依賴。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
有兩種使用方式:命令式和宣告式
1. 命令式
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteAccessException.class)
.build();
template.execute(ctx -> {
// ... do something
});
命令式主要是利用RetryTemplate。RetryTemplate 實現了 RetryOperations 介面。
RetryTemplate template = new RetryTemplate();
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L); // 30秒內可以重試,超過30秒不再重試
template.setRetryPolicy(policy);
MyObject result = template.execute(new RetryCallback<MyObject, Exception>() {
public MyObject doWithRetry(RetryContext context) {
// Do stuff that might fail, e.g. webservice operation
return result;
}
});
RetryTemplate 也支援流式配置
// 最大重試10次,第一次間隔100ms,第二次200ms,第三次400ms,以此類推,最大間隔10000ms
RetryTemplate.builder()
.maxAttempts(10)
.exponentialBackoff(100, 2, 10000)
.retryOn(IOException.class)
.traversingCauses()
.build();
// 3秒內可以一直重試,每次間隔10毫秒,3秒以後就不再重試了
RetryTemplate.builder()
.fixedBackoff(10)
.withinMillis(3000)
.build();
// 無限重試,間隔最小1秒,最大3秒
RetryTemplate.builder()
.infiniteRetry()
.retryOn(IOException.class)
.uniformRandomBackoff(1000, 3000)
.build();
當重試耗盡時,RetryOperations可以將控制傳遞給另一個回撥:RecoveryCallback
template.execute(new RetryCallback<Object, Throwable>() {
@Override
public Object doWithRetry(RetryContext context) throws Throwable {
// 業務邏輯
return null;
}
}, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
// 恢復邏輯
return null;
}
});
如果重試次數耗盡時,業務邏輯還沒有執行成功,那麼執行恢復邏輯來進行兜底處理(兜底方案)
無狀態的重試
在最簡單的情況下,重試只是一個while迴圈:RetryTemplate可以一直嘗試,直到成功或失敗。RetryContext包含一些狀態,用於確定是重試還是中止。然而,這個狀態是在堆疊上的,不需要在全域性的任何地方儲存它。因此,我們稱之為“無狀態重試”。無狀態重試和有狀態重試之間的區別包含在RetryPolicy的實現中。在無狀態重試中,回撥總是在重試失敗時的同一個執行緒中執行。
有狀態的重試
如果故障導致事務性資源失效,則需要考慮一些特殊問題。這並不適用於簡單的遠端呼叫,因為(通常)沒有事務性資源,但它有時適用於資料庫更新,特別是在使用Hibernate時。在這種情況下,只有重新丟擲立即呼叫失敗的異常才有意義,這樣事務才能回滾,我們才能開始一個新的(有效的)事務。在這些情況下,無狀態重試還不夠好,因為重新丟擲和回滾必然涉及離開RetryOperations.execute()方法,並且可能丟失堆疊上的上下文。為了避免丟失上下文,我們必須引入一種儲存策略,將其從堆疊中取出,並(至少)將其放入堆儲存中。為此,Spring Retry提供了一個名為RetryContextCache的儲存策略,您可以將其注入到RetryTemplate中。RetryContextCache的預設實現是在記憶體中,使用一個簡單的Map。它具有嚴格強制的最大容量,以避免記憶體洩漏,但它沒有任何高階快取特性(例如生存時間)。如果需要,你應該考慮注入具有這些特性的Map。
重試策略
在RetryTemplate中,由RetryPolicy決定是重試還是失敗。RetryTemplate負責使用當前策略建立RetryContext,並在每次重試時將其傳遞給RetryCallback。回撥失敗後,RetryTemplate必須呼叫RetryPolicy,要求它更新自己的狀態(儲存在RetryContext中)。然後詢問政策是否可以再嘗試一次。如果不能進行另一次重試(例如,因為已達到限制或檢測到超時),策略還負責標識耗盡狀態——但不負責處理異常。當沒有恢復可用時,RetryTemplate丟擲原始異常,但有狀態情況除外。在這種情況下,它會丟擲RetryExhaustedException。還可以在RetryTemplate中設定一個標誌,讓它無條件地丟擲回撥(即使用者程式碼)中的原始異常。
// Set the max attempts including the initial attempt before retrying
// and retry on all exceptions (this is the default):
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<MyObject, Exception>() {
public MyObject doWithRetry(RetryContext context) {
// business logic here
}
});
監聽器
Spring Retry提供了RetryListener介面。RetryTemplate允許您註冊RetryListener例項。
template.registerListener(new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return false;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
}
});
反射方法呼叫的監聽器
template.registerListener(new MethodInvocationRetryListenerSupport() {
@Override
protected <T, E extends Throwable> void doClose(RetryContext context, MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
super.doClose(context, callback, throwable);
}
@Override
protected <T, E extends Throwable> void doOnError(RetryContext context, MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
super.doOnError(context, callback, throwable);
}
@Override
protected <T, E extends Throwable> boolean doOpen(RetryContext context, MethodInvocationRetryCallback<T, E> callback) {
return super.doOpen(context, callback);
}
});
2. 宣告式
@EnableRetry
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
class Service {
@Retryable(RemoteAccessException.class)
public void service() {
// ... do something
}
@Recover
public void recover(RemoteAccessException e) {
// ... panic
}
}
可以將@EnableRetry註釋新增到@Configuration類上,並在想要重試的方法上(或在所有方法的型別級別上)使用@Retryable,還可以指定任意數量的重試監聽器。
@Configuration
@EnableRetry
public class Application {
@Bean
public RetryListener retryListener1() {
return new RetryListener() {...}
}
@Bean
public RetryListener retryListener2() {
return new RetryListener() {...}
}
}
@Service
class MyService {
@Retryable(RemoteAccessException.class)
public void hello() {
// ... do something
}
}
可以利用 @Retryable 的屬性來控制 RetryPolicy 和 BackoffPolicy
@Service
public class MyService {
@Retryable(value = RuntimeException.class, maxAttempts = 5, backoff = @Backoff(value = 1000L, multiplier = 1.5))
public void sayHello() {
// ... do something
}
@Retryable(value = {IOException.class, RemoteAccessException.class},
listeners = {"myListener1", "myListener2", "myListener3"},
maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 500))
public void sayHi() {
// ... do something
}
@Retryable(maxAttempts = 5, backoff = @Backoff(delay = 1000, maxDelay = 30000, multiplier = 1.2, random = true))
public void sayBye() {
// ... do something
}
}
如果希望在重試耗盡時執行另外的邏輯,則可以提供恢復方法。恢復方法應該在與@Retryable例項相同的類中宣告,並標記為@Recover。返回型別必須匹配@Retryable方法。恢復方法的引數可以選擇性地包括丟擲的異常和(可選地)傳遞給原始可重試方法的引數(或它們的部分列表,只要在最後一個需要的引數之前沒有被省略)。
@Service
class MyService {
@Retryable(RemoteAccessException.class)
public void service(String str1, String str2) {
// ... do something
}
@Recover
public void recover(RemoteAccessException e, String str1, String str2) {
// ... error handling making use of original args if required
}
}
為了避免多個恢復方法搞混淆了,可以手動指定用哪個恢復方法
@Service
class Service {
@Retryable(recover = "service1Recover", value = RemoteAccessException.class)
public void service1(String str1, String str2) {
// ... do something
}
@Retryable(recover = "service2Recover", value = RemoteAccessException.class)
public void service2(String str1, String str2) {
// ... do something
}
@Recover
public void service1Recover(RemoteAccessException e, String str1, String str2) {
// ... error handling making use of original args if required
}
@Recover
public void service2Recover(RemoteAccessException e, String str1, String str2) {
// ... error handling making use of original args if required
}
}
1.3.2及以後版本支援匹配引數化(泛型)返回型別來檢測正確的恢復方法:
@Service
class Service {
@Retryable(RemoteAccessException.class)
public List<Thing1> service1(String str1, String str2) {
// ... do something
}
@Retryable(RemoteAccessException.class)
public List<Thing2> service2(String str1, String str2) {
// ... do something
}
@Recover
public List<Thing1> recover1(RemoteAccessException e, String str1, String str2) {
// ... error handling for service1
}
@Recover
public List<Thing2> recover2(RemoteAccessException e, String str1, String str2) {
// ... error handling for service2
}
}
1.2版本引入了對某些屬性使用表示式的能力
@Retryable(exceptionExpression="message.contains('this can be retried')")
public void service1() {
...
}
@Retryable(exceptionExpression="message.contains('this can be retried')")
public void service2() {
...
}
@Retryable(exceptionExpression="@exceptionChecker.shouldRetry(#root)",
maxAttemptsExpression = "#{@integerFiveBean}",
backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
public void service3() {
...
}
表示式可以包含屬性佔位符,比如:#{${max.delay}} 或者 #{@exceptionChecker.${retry.method}(#root)} 。規則如下:
- exceptionExpression 以丟擲的異常為根物件進行計算求值的
- maxAttemptsExpression 和 @BackOff 表示式屬性 只在初始化的時候被計算一次。它們沒有用於計算的根物件,但它們可以引用上下文中的其他bean
例如:
@Data
@Component("runtimeConfigs")
@ConfigurationProperties(prefix = "retry.cfg")
public class MyRuntimeConfig {
private int maxAttempts;
private long initial;
private long max;
private double mult;
}
application.properties
retry.cfg.maxAttempts=10
retry.cfg.initial=100
retry.cfg.max=2000
retry.cfg.mult=2.0
使用變數
@Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
backoff = @Backoff(delayExpression = "@runtimeConfigs.initial",
maxDelayExpression = "@runtimeConfigs.max", multiplierExpression = "@runtimeConfigs.mult"))
public void service() {
System.out.println(LocalDateTime.now());
boolean flag = sendMsg();
if (flag) {
throw new CustomException("呼叫失敗");
}
}
@Retryable(maxAttemptsExpression = "args[0] == 'something' ? 3 : 1")
public void conditional(String string) {
...
}
最後,簡單看一下原始碼org.springframework.retry.support.RetryTemplate#doExecute()
RetryContext是執行緒區域性變數
間隔時間是透過執行緒休眠來實現的
https://github.com/spring-projects/spring-retry