一、接入
spring boot 2.7.14
spring retry 从2.0.2版本之后,从spring batch里剥离出来成为一个单独的工程,因此我们引入spring retry最新版本可以直接如下引入
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
启动类上打上注解@EnableRetry
二、使用注解
Spring retry作为重试组件,可以直接使用@Retryable注解;废话不多说,直接上代码
@Component
public class UserService {
@Retryable(retryFor = BizException.class, maxAttempts = 5, backoff = @Backoff(delay = 1000L, multiplier=1))
public int service(int throwErr) throws BizException {
System.out.println("===== service =====" + DateUtil.getDateTime(new Date()));
if (throwErr==1) {
throw new BizException();
}
return 1;
}
}
执行效果如下
retryFor指定异常进行重试,如果不指定的话,默认任何异常都会重试;
maxAttempts重试次数,默认值是3次;
backoff是用于控制延迟重试策略,@Backoff(delay = 1000L, multiplier=2)表示每次执行失败,再次延迟时间=上次延迟时间*2
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
//最终失败情况下,调用recover进行恢复
String recover() default "";
//拦截器,主要是aop切面逻辑,具体待验证, 目前看下来主要是配合stateful一起使用,带验证
String interceptor() default "";
//包含哪些异常
@AliasFor("include")
Class<? extends Throwable>[] retryFor() default {};
//哪些异常不重试
@AliasFor("exclude")
Class<? extends Throwable>[] noRetryFor() default {};
//哪些异常不作回滚
Class<? extends Throwable>[] notRecoverable() default {};
//标签,没啥意义
String label() default "";
//有状态的重试,这个我们单独开讲
boolean stateful() default false;
//最大重试次数
int maxAttempts() default 3;
//最大重试次数表达式
String maxAttemptsExpression() default "";
//延迟策略
Backoff backoff() default @Backoff;
//异常过滤
String exceptionExpression() default "";
//监听器
String[] listeners() default {};
}
三、使用RetryTemplate
上面通过注解@Retryable(retryFor = BizException.class, maxAttempts = 5, backoff = @Backoff(delay = 1000L, multiplier=1)) 的效果,可以通过如下编码的形式来实现
public int service2(int throwErr) throws BizException {
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.retryOn(BizException.class)
.customBackoff(
BackOffPolicyBuilder
.newBuilder()
.delay(1000)
.multiplier(2)
.build()
)
.build();
return template.execute(ctx->{
System.out.println("===== service =====" + DateUtil.getDateTime(new Date()));
if (throwErr==1) {
throw new BizException("system error");
}
return 1;
});
}
四、关于RetryContentxt
普通场景下,重试是不需要获取之前重试的状态的,但是某些场景下,每次重试可能都需要打印当前重试次数,并且塞进去相关信息等
template.execute(ctx->{
System.out.println("===== service =====" + DateUtil.getDateTime(new Date()));
//上下文获取当前重试次数
System.out.println("retry count="+ctx.getRetryCount());
//也可以塞一些东西进去
System.out.println("retry count="+ctx.getAttribute("attr"))
//从属性里取值
ctx.setAttribute("attr", "test"+ctx.getRetryCount());
//直接终止当前重试,这个比较牛逼,配合固定重试次数来搞
ctx.setExhaustedOnly()
if (throwErr==1) {
throw new BizException("system error");
}
return 1;
});
五、重试策略&延迟重试
我们直接把重试策略
public interface RetryPolicy extends Serializable {
/**
* @param context the current retry status
* @return true if the operation can proceed
*/
boolean canRetry(RetryContext context);
/**
* Acquire resources needed for the retry operation. The callback is passed in so that
* marker interfaces can be used and a manager can collaborate with the callback to
* set up some state in the status token.
* @param parent the parent context if we are in a nested retry.
* @return a {@link RetryContext} object specific to this policy.
*
*/
RetryContext open(RetryContext parent);
/**
* @param context a retry status created by the {@link #open(RetryContext)} method of
* this policy.
*/
void close(RetryContext context);
/**
* Called once per retry attempt, after the callback fails.
* @param context the current status object.
* @param throwable the exception to throw
*/
void registerThrowable(RetryContext context, Throwable throwable);
}
从上图我们可以看到很多重试策略的实现,
六、recover
retry组件重试最终失败后,会调用recover方法(有点像回滚)
@Component
public class TestService {
@Retryable(retryFor = RemoteAccessException.class)
public void service() {
System.out.println("===== service =====" + DateUtil.getDateTime(new Date()));
throw new RemoteAccessException("xx");
}
@Recover
public void recover(RemoteAccessException e) {
System.out.println("===== recover =====" + DateUtil.getDateTime(new Date()));
}
}
七、监听器Listeners
参考接口:
public interface RetryListener {
void open(RetryContext context, RetryCallback<T> callback);
void onSuccess(RetryContext context, T result);
void one rror(RetryContext context, RetryCallback<T> callback, Throwable e);
void close(RetryContext context, RetryCallback<T> callback, Throwable e);
}
实现如下:
@Bean("listener1")
public RetryListener getListener() {
return new RetryListener() {
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
System.out.println("===== close =====");
}
@Override
public <T, E extends Throwable> void onSuccess(RetryContext context, RetryCallback<T, E> callback, T result) {
System.out.println("===== close =====");
}
};
}
然后在注解上引用
@Retryable(retryFor = RemoteAccessException.class, maxAttempts = 4,
backoff = @Backoff(delay = 1000L, multiplier=2),
listeners = {"listener1"}
)
public void service() {
System.out.println("===== service =====" + DateUtil.getDateTime(new Date()));
throw new RemoteAccessException("xx");
}
最终执行效果如下
八、有状态的重试stateful
有状态重试通常是用在message-driven 的应用中,从消息中间件比如RabbitMQ等接收到的消息,如果应用处理失败,那么消息中间件服务器会再次投递,再次投递时,对于集成了Spring Retry的应用来说,再次处理之前处理失败的消息,就是一次重试;也就是说,Spring Retry能够识别出,当前正在处理的消息是否是之前处理失败过的消息;
如果是之前处理过的消息,Spring Retry就可以使用 back off policies 阻塞当前线程;Spring Retry同时追踪重试的次数,支持处理彻底失败后的recover,这也是使用有状态重试的理由;
有状态重试的另一个典型应用场景是跟Spring Transaction框架集成。在集成了Spring Transaction框架的MVC应用中,通过TransactionInterceptor,开启对Service层的事务管理;在这种情况下,Spring Retry会提供让每一次重试和重试次数耗尽之后的recover都在一个新的事务中执行。
九、retry组件的忧缺点
整个使用下来,retry组件的优点:
- 无侵入式的实现了重试,大大减小了重试代码成本
- 重试策略比较灵活,支持固定频率重试、延迟重试等策略
缺点:
- 不支持异步重试,且重试过程是阻塞当前程序的,当然,如果要实现异步重试,需要配合@Async注解来搞