源码解析-Spring Retry

610 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

Spring Retry:Spring提供的一种重试机制的解决方案,包括:组成部分、重试策略、核心逻辑、源码解析、使用方法

Retry

Spring提供的一种重试机制的解决方案 github.com/spring-proj…

组成部分

RetryOperations:定义基本操作集,以执行具有可配置重试行为的操作 RetryTemplate:RetryOperations的实现,简化操作执行的模板类 RetryCallback:重试回调 RecoveryCallback:重试耗尽时回调 RetryContext:回调上下文,如果需要,它可以作为一个属性包来存储迭代期间的数据 RetryPolicy:重试策略 RetryListener:对整个Retry过程进行监听(open、onError、close) RetryContextCache:为RetryContext提供存储策略 RetryState:有状态重试,跨重试的多个调用标识状态,要标识状态

重试策略

SimpleRetryPolicy:默认将对所有异常进行尝试,最多尝试3次 AlwaysRetryPolicy:一直重试,直到成功为止 NeverRetryPolicy:从不重试 TimeoutRetryPolicy:指定时间范围内进行重试,直到超时为止,默认的超时时间是1000毫秒。 ExceptionClassifierRetryPolicy:基于异常来判断是否需要进行重试 CircuitBreakerRetryPolicy:包含了断路器功能的RetryPolicy CompositeRetryPolicy:组合多个RetryPolicy BackOffPolicy:定义在两次尝试之间需要间隔的时间 FixedBackOffPolicy:将在两次重试之间进行一次固定的时间间隔,默认1秒 ExponentialBackOffPolicy:使每一次尝试的间隔时间指数性增长(参数:初始间隔时间、倍数、最大间隔时间) ExponentialRandomBackOffPolicy:在ExponentialBackOffPolicy的基础上随机增长10% UniformRandomBackOffPolicy:每次都随机的产生一个间隔时间

核心逻辑

  1. RetryLoadBalancerInterceptor.intercept:HTTP请求的负载均衡重试拦截器
  2. this.lbRetryFactory.createRetryPolicy:创建重试策略;当前实例的重试次数、切换实例的重试次数、状态码重试等
  3. createRetryTemplate:创建重试模板;定义重试策略、回退策略、监听器、重试上下文等
  4. RetryTemplate.doExecute:失败重试核心逻辑,回调业务逻辑
  5. retryCallback.doWithRetry:选择服务实例,创建HTTP请求,执行请求,指定错误码重试
  6. registerThrowable:记录重试失败的状态信息
  7. canRetry(retryPolicy, context):判定重试
  8. backOffPolicy.backOff:执行最后尝试失败逻辑

源码

org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor.java

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
		final ClientHttpRequestExecution execution) throws IOException {
	final URI originalUri = request.getURI();
	final String serviceName = originalUri.getHost();
	// 重试策略
	final LoadBalancedRetryPolicy retryPolicy = this.lbRetryFactory.createRetryPolicy(serviceName, this.loadBalancer);
	RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
	// 调用RetryTemplate.doExecute
	return template.execute(context -> {
	  // 业务和重试逻辑
		// 选择服务实例
		ServiceInstance serviceInstance = null;
		if (context instanceof LoadBalancedRetryContext) {
			LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
			serviceInstance = lbContext.getServiceInstance();
		}
		if (serviceInstance == null) {
			serviceInstance = this.loadBalancer.choose(serviceName);
		}
		// 拦截器执行HTTP请求处理
		ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer
				.execute(serviceName, serviceInstance,
						this.requestFactory.createRequest(request, body, execution));
		int statusCode = response.getRawStatusCode();
		// 指定HTTP状态码重试
		if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
			byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
			response.close();
			throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
		}
		return response;
	}, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
		// 最后尝试失败后处理逻辑
		@Override
		protected ClientHttpResponse createResponse(ClientHttpResponse response,
				URI uri) {
			return response;
		}
	});
}

org.springframework.retry.support.RetryTemplate.java

// 如果执行失败,根据重试策略执行retryCallback,否则执行recoveryCallback
protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException {
  // 重试策略
	RetryPolicy retryPolicy = this.retryPolicy;
	// 回退策略
	BackOffPolicy backOffPolicy = this.backOffPolicy;
	// 重试策略上下文,count:重试次数
	RetryContext context = open(retryPolicy, state);
	// 确保context对于clients需要的时候全局可用
	RetrySynchronizationManager.register(context);
	Throwable lastException = null;
  // 重试耗尽
	boolean exhausted = false;
	try {
		/*
		 * 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 {
				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()) {
				  // 回调最后尝试失败逻辑
					backOffPolicy.backOff(backOffContext);
				}
			}
			if (state != null && context.hasAttribute(GLOBAL_STATE)) {
				break;
			}
		}
		exhausted = true;
		// 最后尝试失败后要采取的措施。如果有状态,请清理缓存;如果有恢复回调,请执行该回调并返回其结果;否则抛出异常。
		return handleRetryExhausted(recoveryCallback, context, state);
	} catch (Throwable e) {
		throw RetryTemplate.<E>wrapIfNecessary(e);
	} finally {
		close(retryPolicy, context, state, lastException == null || exhausted);
		doCloseInterceptors(retryCallback, context, lastException);
		RetrySynchronizationManager.clear();
	}
}

使用

  1. 引入依赖
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.5.RELEASE</version>
</dependency>
  1. 配置
@Bean
public RetryTemplate simpleRetryTemplate() {
    RetryTemplate retryTemplate = new RetryTemplate();
    retryTemplate.setRetryPolicy(new SimpleRetryPolicy());
    ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
    backOffPolicy.setInitialInterval(3000);
    backOffPolicy.setMultiplier(2);
    backOffPolicy.setMaxInterval(15000);
    retryTemplate.setBackOffPolicy(backOffPolicy);
    return retryTemplate;
}
  1. 声明式
@Configuration
@EnableRetry
public class Application {
    @Bean
    public Service service() {
        return new Service();
    }
    @Bean public RetryListener retryListener() {
        return new RetryListener() {...}
    }
}

@Service
class Service {
    @Retryable(maxAttempts=12, backoff=@Backoff(delay=100, maxDelay=500))
    public service() {
        // ... do something
    }
    @Retryable(exceptionExpression="message.contains('this can be retried')")
    public void service1() {
      ...
    }
}