一、接入
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;
});
}
常见重试策略:
策略类 | 效果 | 关键参数 |
---|---|---|
MaxAttemptsRetryPolicy | 设置最大的重试次数,超过之后执行recover | maxAttempts:最大重试次数 |
BinaryExceptionClassifierRetryPolicy | 可以指定哪些异常需要重试,哪些异常不需要重试 | exceptionClassifier:BinaryExceptionClassifier,异常识别类,其实就是存在一个Map映射 |
SimpleRetryPolicy | 上面两者的结合,支持次数和自定义异常重试 | maxAttempts 和 exceptionClassifier |
TimeoutRetryPolicy | 在指定的一段时间内重试 | timeout:单位ms |
ExceptionClassifierRetryPolicy | 支持异常和重试策略的映射,比如异常A对应重试策略A,异常B对应重试策略B | exceptionClassifier:本质就是异常和重试策略的映射 |
CompositeRetryPolicy | 策略类的组合类,支持两种模式,乐观模式:只要一个重试策略满足即执行,悲观模式:另一种是所有策略模式满足再执行 | policies:策略数组optimistic:true-乐观;false-悲观 |
CircuitBreakerRetryPolicy | 熔断器模式 | delegate:策略代理类openTimeout:[0,openTimeout]会一直执行代理类策略,(openTimeout, resetTimeout] 会半打开,如果代理类判断不可以重试,就会熔断,执行recover逻辑,如果代理类判断还可以重试且重试成功,开关会闭合。 |
resetTimeout:超过指定时间,开关重置闭合 | ||
ExpressionRetryPolicy | 符合表达式就重试 | expression:表达式,见 org.springframework.expression.Expression实现 |
常见回退策略:
策略类 | 效果 | 关键参数 |
---|---|---|
FixedBackOffPolicy | 间隔固定时间重试 | sleeper:支持线程sleep和Object#wait,区别在于是否释放锁backOffPeriod:等待间隔 |
NoBackOffPolicy | 无等待直接重试 | |
ExponentialBackOffPolicy | 在一个设置的时间区间内,等待时长为上一次时长的递增 | initialInterval:默认起始等待时间 maxInterval:最大等待时间 multiplier:递增倍数 sleeper:同上 |
ExponentialRandomBackOffPolicy | 在 ExponentialBackOffPolicy 基础上,乘数随机 |
四、关于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 onError(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注解来搞