初探SpringRetry机制
重试是在网络通讯中非常重要的概念,尤其是在微服务体系内重试显得格外重要。常见的场景是当遇到网络抖动造成的请求失败时,可以按照业务的补偿需求来制定重试策略。Spring框架提供了SpringRetry能让我们在项目工程中很方便的使用重试。这里我主要试着分析一下在Spring框架的各个核心模块里如何集成是使用SpringRetry。
1. SpringRetry的快速上手示例
首先我们引入springRetry
的依赖: implementation group: 'org.springframework.retry', name: 'spring-retry',version:'1.2.5.RELEASE'
,同时已SpringBoot作为项目示例基础:
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy(); //定义基于时间超时的重试策略
timeoutRetryPolicy.setTimeout(3000L);
retryTemplate.setRetryPolicy(timeoutRetryPolicy);
return retryTemplate;
}
public static void main(String[]args){
ConfigurableApplicationContext applicationContext = SpringApplication.run(RetryDemo.class, args);
RetryTemplate retryTemplate = applicationContext.getBean(RetryTemplate.class);
retryTemplate.execute(new RetryCallback<Object, Throwable>() { //2 重试执行的代码块
private int index = 1;
public Object doWithRetry(RetryContext context) throws Throwable {
System.out.println("第" + index + "次执行");
index++;
System.out.println(1 / 0);//3 重试代码块里模拟异常的操作
return null;
}
});
}
上面的代码示例展示了SpringRetry的最基本使用方式,首先在代码1处先定义了RetryTemplate的Bean,同时定义了重试的策略配置,等会说一下这个RetryPolicy
接口。在此处定义TimeoutRetryPolicy
主要控制重试多长时间,此处设置3秒。3秒后没有成功的话结束重试。在代码2处,使用RetryTemplate的execute方法来执行重试的回调,如果在回调中出现异常则执行重试,因此我们可以把执行重试的代码块放在这里。在代码3处模拟了抛出异常的操作
这里运行的结果:
....
第110823次执行
第110824次执行
第110825次执行
第110826次执行
Exception in thread "main" java.lang.ArithmeticException: / by zero
2. SpringRetry的核心代码
在RetryTemplate
里核心方法时exectute方法,这里可以从这个入口方法来了解代码的核心。
- 初始化
RetryContext
// Allow the retry policy to initialise itself...
RetryContext context = open(retryPolicy, state);
if (this.logger.isTraceEnabled()) {
this.logger.trace("RetryContext retrieved: " + context);
}
在这里会调用给RetryPolicy
接口的open方法,目的是为了初始化RetryContext(重试上下文),RetryContext可以扩展我们的业务字段,这里我举个例子TimeoutRetryContext
中定义了timeout与start属性。LoadBalancedRetryContext
中定义了request与serviceInstance
- 调用RetryListener的onOpen方法
// Give clients a chance to enhance the context...
boolean running = doOpenInterceptors(retryCallback, context);
private <T, E extends Throwable> boolean doOpenInterceptors(
RetryCallback<T, E> callback, RetryContext context) {
boolean result = true;
for (RetryListener listener : this.listeners) {
result = result && listener.open(context, callback);
}
return result;
}
在这里单行注释已经明明白白的写出了,我们可以利用RetryListener
的来给客户端一个机会增强RetryContext的机会,如果理解拦截器的思想的话,这点应该很容易理解。
- 获取和启动BackOffContext
// Get or Start the backoff context... BackOffContext backOffContext = null; Object resource = context.getAttribute("backOffContext"); if (resource instanceof BackOffContext) { backOffContext = (BackOffContext) resource; } if (backOffContext == null) { backOffContext = backOffPolicy.start(context); if (backOffContext != null) { context.setAttribute("backOffContext", backOffContext); } }
先从RetryContext中获取backOffContext,如果没有会调用backOffPolicy的start方法。该方法会返回BackOffContext对象不等于null的情况下把此对象放在RetryContext当中 - 执行RetryCallback的代码回调
/* * We allow the whole loop to be skipped if the policy or context already * forbid the first try. This is used in the case of external retry to allow a * recovery in handleRetryExhausted without the callback processing (which * would throw an exception). */ while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try { if (this.logger.isDebugEnabled()) { this.logger.debug("Retry: count=" + context.getRetryCount()); } // Reset the last exception, so if we are successful // the close interceptors will not think we failed... lastException = null; return retryCallback.doWithRetry(context); } catch (Throwable e) { lastException = e; try { registerThrowable(retryPolicy, state, context, e); } catch (Exception ex) { throw new TerminatedRetryException("Could not register throwable", ex); } finally { doOnErrorInterceptors(retryCallback, context, e); } if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try { backOffPolicy.backOff(backOffContext); } catch (BackOffInterruptedException ex) { lastException = e; // back off was prevented by another thread - fail the retry if (this.logger.isDebugEnabled()) { this.logger .debug("Abort retry because interrupted: count=" + context.getRetryCount()); } throw ex; } } if (this.logger.isDebugEnabled()) { this.logger.debug( "Checking for rethrow: count=" + context.getRetryCount()); } if (shouldRethrow(retryPolicy, context, state)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount()); } throw RetryTemplate.<E>wrapIfNecessary(e); } } /* * A stateful attempt that can retry may rethrow the exception before now, * but if we get this far in a stateful retry there's a reason for it, * like a circuit breaker or a rollback classifier. */ if (state != null && context.hasAttribute(GLOBAL_STATE)) { break; } }
此处代码是重试代码的执行核心:
- 首先是用一个循环执行RetryCallback的回调内容,此处循环条件有两个:
- 根据
RetryPolicy
接口里的canRetry方法的返回值,比如说TimeoutPolicy
里控制retry的条件是(System.currentTimeMillis() - start) <= timeout
- 还有一个是RetryContext中的isExhaustedOnly()方法,这个方法是一个“筋疲力尽”的标志如果为true那么也会停止重试的循环
- 当出现异常时
- 首先会调用RetryPolicy里的
registerThrowable
的方法。然后当RetryState不为null时会把RetryContext对象放入RetryContextCache
中保存。 - 依次循环调用RetryListener里的onError方法
- 再次判断重试条件,如果满足的情况下,会调用BackoffPolicy的backoff方法。BackOffPolicy的接口主要用于定义在执行重试时出现异常情况的处理逻辑,举个例子说明:
FixedBackOffPolicy
重写的backoff方法里可以控制线程休眠让其间隔多长时间进行重试: sleeper.sleep(backOffPeriod)
。 - 调用
RetryState
的rollbackFor方法的方法来判断当前的异常需不需要回滚,而在该接口的唯一实现类DefaultRetryState里会用Classifier<? super Throwable, Boolean> rollbackClassifier来将Throwable类型转换成Boolean类型,可以在这里定义处理逻辑。如果该方法的返回值为true,将会重新抛出异常
3. SpringRetry的注解式配置
使用注解的方式是SpringRetry中最常见的方式,其实它底层还是aop与前面所说的基本执行流程。对于注解的方式使用起来还是比较简单的。首先我们打上@EnableRetry
的注解
public class SpringApplicationProvider {
//....
}
(maxAttempts = 2, recover = "recover")
public void send() {
System.out.println("send....");
System.out.println(1 / 0);
}
public void recover(ArithmeticException exception) {
System.out.println(2 / 1);
}
可以在需要重试的方法上打上@Retryable
,这个注解有几个常见的属性:
- maxAttepts 这个定义了重试的最大次数,默认值是3
- recover : 用于定义@Recover修饰的方法名,旨在指定重试失败后用于执行的方法。
@Recover
修饰的方法参数必须是Throwable
的子类,同时返回值必须和@Retryable
修饰的返回值相同 - stateful:表示重试是有状态的,默认值为false,如果为true则有如下影响: 1.会重新抛出异常 2.执行重试期间,注册当前的重试上下文至缓存
- backoff : 这个属性用于收集
Backoff
的元数据,那么根据文档描述,可以得到以下信息:
- 没有明确设置的情况下,默认值为 1000 毫秒的固定延迟
只有delay()设置:设置固定的延迟时间进行重试
当设置delay()和maxDelay() ,延时的值在两个值之间均匀分布
使用delay() 、 maxDelay()和multiplier()后退指数增长到最大值
如果设置了random()标志,则从 [1, multiplier-1] 中的均匀分布中为每个延迟选择乘数 - 实际上这个几个属性是控制创建BackOff接口实现类的:
- 如果multiplier大于0,则创建
ExponentialBackOffPolicy
对象 - 如果max > min,则创建
UniformRandomBackOffPolicy
对象 - 否则创建
FixedBackOffPolicy
对象