随笔分类
spring-retry
Spring-retry是 spring提供的一个重试框架
基于 spring-retry,我们可以针对业务去做些定制化的重试机制,包括自定义重试策略、规避策略,以及在什么情况下会触发重试机制,在重试下也没有成功下,也可以去做些故障恢复操作,甚至还可以对重试过程进行监听,做些相对于的回调处理,这块橡胶而言比较灵活
retry中默认提供了几种重试策略,如基于次数、基于耗时,但也只能去指定一种策略
而规避策略其实是对失败预测的策略,即此次失败那么预测在未来一小段时间内很大概率下仍然有可能继续失败,那么就有必要去对失败进行规避 back off
思考下:什么场景下适合我们去使用 retry?
-
短暂性的错误
即非业务逻辑处理上的错误,可能是第三方依赖服务、网络延时等造成的短暂性的失败
-
即时类异常
适用于关键型业务,业务做了幂等性处理,当多次执行失败后在故障恢复中一方面可以做保底,另一方面可以去做相关告警功能,且保底可以去通用化的保底策略
那么 retry底层是怎么去玩的呢,其实也很简单,通过 aop去代理目标方法的执行,以及根据不同的尝试策略下是否继续执行重试的判断,重试过程中上下文的保存使用 threadLocal进行绑定
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly())
这块我感觉是比较有意思的地方,假设重试是有代价的,基于默认配置下的重试策略并不能很好反应资源是否足以支撑继续重试,因此可以通过对上下文的配置来去终止重试
/**
* Public accessor for the exhausted flag {@link #setExhaustedOnly()}.
* @return true if the flag has been set.
*/
boolean isExhaustedOnly();
具体代码实现就不分析了,比较简单,感兴趣的可以去自由探索
来看个 demo:
/**
* retry invoke, cnt:1
* retry happens error
* retry invoke, cnt:2
* retry happens error
* Deletion of resource, end attempts, total attempts:3
* retry invoke, cnt:3
* retry happens error
* null attempt to recover fail, remember it!
* @throws Exception
*/
@Test
public void testOnRetryTemplate() throws Exception {
// 硬编码 - 背压策略只能选择一个
RetryTemplate retryTemplate = RetryTemplate.builder()
// 最多尝试次数
//.maxAttempts(30)
// timeout
.withinMillis(7000)
// 间隔 1s
//.fixedBackoff(1000)
// 退避策略(当前执行失败, 极有可能在接下来一小段时间内大概率也会执行失败)
.exponentialBackoff(1000, 2, 3000)
// 针对哪类异常触发尝试(实际开发禁止这么写, 尝试应应用于不确定失败场景下, 如果失败具有确定性, 那也就没有尝试的必要)
.retryOn(Exception.class)
// 添加监听器
.withListener(new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true;
}
@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) {
System.out.println("retry happens error");
}
})
.build();
retryTemplate.execute(new RetryCallback<Void, Exception>() {
@Override
public Void doWithRetry(RetryContext ctx) throws Exception {
// 存放业务逻辑
if (Objects.isNull(ctx.getAttribute("cnt"))) {
AtomicLong cnt = new AtomicLong(1L);
ctx.setAttribute("cnt", cnt);
}
AtomicLong cnt = (AtomicLong)ctx.getAttribute("cnt");
if (cnt.get() == 3) { // depletion of resource
System.out.println("Deletion of resource, end attempts, total attempts:" + cnt.get());
ctx.setExhaustedOnly();
}
System.out.println("retry invoke, cnt:" + cnt.getAndIncrement());
throw new RuntimeException("exp happens!");
}
},
// 指定容错措施(有指定的话, 这块内部实现会把异常吃掉)
new RecoveryCallback<Void>() {
@Override
public Void recover(RetryContext context) throws Exception {
String busi = String.valueOf(context.getAttribute("busi"));
System.out.println(busi + " attempt to recover fail, remember it!");
return null;
}
});
}
基于 mock:
public class Service {
AtomicLong cnt = new AtomicLong(1L);
@Retryable
public void methodA() {
System.out.println("method A invoke!");
}
@Retryable(include = { RuntimeException.class }, maxAttempts = 4, recover = "recover")
public void methodB(Collection<?> collection) {
System.out.println("method B invoke happens exp cnt:" + cnt.getAndIncrement());
if (cnt.get() <= 40) {
throw new RuntimeException("runTimeExp happens!");
}
}
/**
* 指定该类重试失败后也未成功会去回调的函数
* 如果执行成功了后就不会去调用此函数了
* 默认会去调用当前实例下声明的恢复方法
*/
@Recover
public void recover(RuntimeException exp) {
System.out.println("recover cnt:" + cnt);
}
@Mock
private Service service;
/**
* 01:59:18.176 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
* retry invoke, cnt:1
* retry happens error
* 01:59:18.181 [main] DEBUG org.springframework.retry.backoff.ExponentialBackOffPolicy - Sleeping for 1000
* 01:59:19.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
* 01:59:19.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1
* retry invoke, cnt:2
* retry happens error
* 01:59:19.182 [main] DEBUG org.springframework.retry.backoff.ExponentialBackOffPolicy - Sleeping for 2000
* 01:59:21.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2
* 01:59:21.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=2
* Deletion of resource, end attempts, total attempts:3
* retry invoke, cnt:3
* retry happens error
* 01:59:21.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=3
* 01:59:21.182 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=3
* null attempt to recover fail, remember it!
*/
@Test
public void testOnServiceInvoke() {
doNothing()
.doCallRealMethod()
.when(service)
.methodB(isA(Collection.class));
service.methodB(Sets.newHashSet());
}