java优雅重试机制spring-retry

java优雅重试机制spring-retry

文章目录

  • java优雅重试机制spring-retry
    • 1.何为重试机制?
      • 1.1 优雅的重试机制要具备几点
      • 1.2 优雅重试共性和原理:
      • 1.3优雅重试适用场景:
  • 2.重试机制有哪些实现和实现思路
    • 2.1不优雅的实现
    • 2.2 稍微优雅一点的实现
    • 2.3 其它实现思路
    • 2.3.1 自定义重试公共实现
    • 2.3.2 使用谷歌的guava-retrying类库
    • 2.3.3 手动重试
    • 2.4优雅的方式
  • 3.spring-retry的简单demo
    • 3.1引入pom依赖
    • 3.2启用重试
    • 3.3调用方法上添加`@Retryable`
    • 3.4自定义监听器
    • 3.5 注解及参数介绍
      • 3.5.1@EnableRetry是否开启重试机制
      • 3.5.2 @Retryable 标注此注解的方法在发生异常时会进行重试
      • 3.5.3 @Backoff 重试等待策略
      • 3.5.4 @Recover回退处理
      • 3.5.5 使用 spring-retry的缺陷
  • 4.spring-retry的原理和源码分析
    • 4.1 结构:
    • 结构
    • 4.2常用类图结构
    • 补偿/回退 策略 ![补偿/回退 策略](https://img-blog.csdnimg.cn/fbd9f2a798b649dfab4df3461fcdaad9.png)
    • 重试策略 补偿/回退 策略 ![在这里插入图片描述](https://img-blog.csdnimg.cn/1be4bf97b8d446b992d113806aa6191f.png)
    • 上下文实现类: ![上下文实现类](https://img-blog.csdnimg.cn/d2d19979b3b14fb0819c1db72a14e851.png)
    • 重试上下文缓存(强引用 与 软引用 缓存方式) ![重试上下文缓存](https://img-blog.csdnimg.cn/e37e0902b83a4530a0b3b681503ec903.png)
    • 重试监听者(实现不同阶段的通知功能) ![重试监听者](https://img-blog.csdnimg.cn/b142ab27ec52474397be68e9b4875002.png)
    • 4.3 RetryPolicy提供了如下策略实现
    • 4.4 BackOffPolicy 提供了如下策略实现:
    • 4.5源码分析
  • 5.总结

1.何为重试机制?

​  业务系统和业务系统之间、接口与接口之间的接口调用会有一些不稳定的因素存在,比如网络波动导致接口调用网络超时,接口调用出现异常等,导致业务请求依赖的接口失败而获取不到数据或者是让系统异常的情况,也没有重试和补偿的机制,这就让系统的稳定性(鲁棒性)大大折扣,那重试机制的使用场景是接口没有要求幂等性和强一致性、发送消息失败、争抢锁失败等,接口要求只能调用一次而且必须成功,这个几乎是很难做到的,所以接口的调用需要考虑是否考虑幂等性、数据一致性、分区容错性和出错时的补偿机制或重试机制,最终一致性还是强一致性,最终一致性就使用MQ等中间件产品,强一致性就使用分布式事务等中间件产品,今天分享的是Java的优雅重试机制spring-retry。

如果一次操作失败,可以进行多次重试,提高调用成功的可能性

1.1 优雅的重试机制要具备几点

  • 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
  • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
  • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

1.2 优雅重试共性和原理:

  • 正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。
  • 约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。
  • 都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。
  • Spring-tryer和guava-tryer工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。

1.3优雅重试适用场景:

  • 功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。
  • 对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。
  • 对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。

2.重试机制有哪些实现和实现思路

2.1不优雅的实现

private int maxTry = 3;

    @Test
    public void test1() {
        for (int i = 1; i <= maxTry; i++) {
            try {
                Boolean result = this.operate1();
                if (result) {
                    i = 4;
                    log.info(result.toString());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

public Boolean operate1() {
        log.info("operate1开始");
        String result = HttpUtil.get("http://www.baidu.com", 5000);
        log.info(result);
        if (StringUtils.isNotEmpty(result)) {
            return Boolean.TRUE;
        }
        log.info("operate1结束");
        return Boolean.FALSE;
}

2.2 稍微优雅一点的实现

package com.example.javaretrydemo.service;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author zlf
 * @description:
 * @time: 2022/10/2
 */
public class ThreadPoolService {

    private static final int DEFAULT_CORE_SIZE = 10;
    private static final int MAX_QUEUE_SIZE = 20;
    private static final int QUEUE_INIT_MAX_SIZE = 500;
    private volatile static ThreadPoolExecutor executor;

    private ThreadPoolService() {
    }

    // 获取单例的线程池对象
    public static ThreadPoolExecutor getInstance() {
        if (executor == null) {
            synchronized (ThreadPoolService.class) {
                if (executor == null) {
                    executor = new ThreadPoolExecutor(DEFAULT_CORE_SIZE,// 核心线程数
                            MAX_QUEUE_SIZE, // 最大线程数
                            Integer.MAX_VALUE, // 闲置线程存活时间
                            TimeUnit.MILLISECONDS,// 时间单位
                            new LinkedBlockingDeque<Runnable>(QUEUE_INIT_MAX_SIZE),// 线程队列
                            Executors.defaultThreadFactory()// 线程工厂
                    );
                }
            }
        }
        return executor;
    }

    public void execute(Runnable runnable) {
        if (runnable == null) {
            return;
        }
        executor.execute(runnable);
    }

    // 从线程队列中移除对象
    public void cancel(Runnable runnable) {
        if (executor != null) {
            executor.getQueue().remove(runnable);
        }
    }

}


   @Test
    public void test2() {
        ThreadPoolExecutor executor = ThreadPoolService.getInstance();
        AtomicInteger num = new AtomicInteger(1);
        CompletableFuture.runAsync(() -> {
            while (num.get() <= 3) {
                //睡眠30秒
                try {
                    Thread.sleep(10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Boolean result = this.operate1();
                    if (result) {
                        num.set(4);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    num.getAndIncrement();
                }
            }
        }, executor);
    }

public Boolean operate1() {
        log.info("operate1开始");
        String result = HttpUtil.get("http://www.baidu.com", 5000);
        log.info(result);
        if (StringUtils.isNotEmpty(result)) {
            return Boolean.TRUE;
        }
        log.info("operate1结束");
        return Boolean.FALSE;
}

2.3 其它实现思路

2.3.1 自定义重试公共实现

  使用JDK动态代理和CGLib动态代理(对目标类进行增强处理)加上spring的AOP(统一的切入调用拦截)加上反射机制(让公共逻辑更加的通用),通过AOP对方法调用进行拦截然后通过动态代理来生成代理对象给目标类的目标方法的调用的前后进行增强处理同时在加上try/catch处理调用目标方法,这种就可以写成一个公共的实现思路模型。

2.3.2 使用谷歌的guava-retrying类库

  使用该类库虽然灵活但是代码入侵性很强,需要写额外的代码,所以不推荐使用,有兴趣的可以去研究下。

2.3.3 手动重试

   调用出错,没有重试机制,直接获取到日志调用参数然后使用curl重新调用接口,人工手动处理

2.4优雅的方式

使用spring-retry

@Retryable(value = SocketTimeoutException.class, maxAttempts = 10, backoff = @Backoff(value = 3000L), listeners = {"myRetryListener"})
 public Boolean operate2() {
        log.info("operate3开始");
        String result = HttpUtil.get("http://www.baidu.com", 5000);
        log.info(result);
        if (StringUtils.isNotEmpty(result)) {
            return Boolean.TRUE;
        }
        log.info("operate3结束");
        return Boolean.FALSE;
}
// 将该类中使用了@Retryable的方法的类注入到其它类中然后调用该方法

3.spring-retry的简单demo

spring-retry官网: https://github.com/spring-projects/spring-retry

3.1引入pom依赖

        
            org.springframework.retry
            spring-retry
        
        
            org.springframework
            spring-aspects
            5.2.22.RELEASE
        

3.2启用重试

​ 在主启动类上加入@EnableRetry注解

@EnableRetry
@SpringBootApplication
public class JavaRetryDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(JavaRetryDemoApplication.class, args);
    }

}

3.3调用方法上添加@Retryable

@Retryable(value = SocketTimeoutException.class, maxAttempts = 10, backoff = @Backoff(value = 3000L), listeners = {"myRetryListener"})
public Boolean operate2() {
        log.info("operate3开始");
        String result = HttpUtil.get("http://www.baidu.com", 5000);
        log.info(result);
        if (StringUtils.isNotEmpty(result)) {
            return Boolean.TRUE;
        }
        log.info("operate3结束");
        return Boolean.FALSE;
 }

  注意: 该demo使用的是注解式,使用注解方式简洁,没有代码入侵,没有使用编程式,也可以使用编程式,编写一个RetryTemplate模板,然后通过代码定义重试和回退的策略,使用注解方式,需要注意加了@Retryable注解的类需要是一个public修饰的,方法中不要加try/catch,否则方法调用异常后,异常没有抛出而是被你在方法中处理了,重试就无效了,方法使用public修饰是为了动态代理能调用到目标类的公共目标方法,就像是spring的事务失效的机制一样,制与方法上的修饰是否可以不是public,可以去自行验证,我只是类比了spring中的事务失效的情况的猜测,写成public更加稳妥一点,不然又出现一些莫名其妙的问题,@Retryable的方法不能在本类被调用,不然重试机制不会生效。也就是要标记为@Service,然后在其它类使用@Autowired注入或者@Bean去实例才能生效。

3.4自定义监听器

package com.example.javaretrydemo.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.stereotype.Component;

/**
 * @author zlf
 * @description:
 * @time: 2022/10/2
 */
@Component
@Slf4j
public class MyRetryListener extends RetryListenerSupport {

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("监听到重试过程关闭了");
        log.info("=======================================================================");
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("监听到重试过程错误了");
    }

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info("=======================================================================");
        log.info("监听到重试过程开启了");
        return true;
    }

}

3.5 注解及参数介绍

3.5.1@EnableRetry是否开启重试机制

​ 能否重试。

​ 当proxyTargetClass属性为true时,使用CGLIB代理。默认使用标准JAVA注解。

​ 在spring Boot中此参数写在程序入口即可。

3.5.2 @Retryable 标注此注解的方法在发生异常时会进行重试

​ value:指定处理的异常类

​ include:指定处理的异常类和value一样,默认为空,当exclude也为空时,默认所有异常

​ exclude:指定异常不处理,默认空,当include也为空时,默认所有异常

​ maxAttempts:最大重试次数。默认3次

​ backoff: 重试等待策略。默认使用@Backoff注解

3.5.3 @Backoff 重试等待策略

​ 不设置参数时,默认使用FixedBackOffPolicy(指定等待时间),重试等待1000ms

​ 设置delay,使用FixedBackOffPolicy(指定等待时间),重试等待填写的时间

​ 设置delay和maxDealy时,重试等待在这两个值之间均态分布

​ 设置delay、maxDealy、multiplier,使用 ExponentialBackOffPolicy(指数级重试间隔的实现 ),multiplier即指定延迟倍数,比如delay=5000l,multiplier=2,则第一次重试为5秒,第二次为10秒,第三次为20秒……

3.5.4 @Recover回退处理

​ 用于@Retryable重试失败后处理方法,此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理,要触发@Recover方法,那么在@Retryable方法上不能有返回值,只能是void才能生效

3.5.5 使用 spring-retry的缺陷

spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:

  • 一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体, 但 sping-retry框架必须强制转换为Throwable子类。
  • 另一个是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。

  Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,当抛出相关异常后执行重试, 如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。只读操作可以重试,幂等写操作可以重试,但是非幂等写操作不能重试,重试可能导致脏写,或产生重复数据。

   @Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。


4.spring-retry的原理和源码分析

4.1 结构:

java优雅重试机制spring-retry_第1张图片

4.2常用类图结构

补偿/回退 策略
java优雅重试机制spring-retry_第2张图片

重试策略
补偿/回退 策略
java优雅重试机制spring-retry_第3张图片

上下文实现类:
java优雅重试机制spring-retry_第4张图片

重试上下文缓存(强引用 与 软引用 缓存方式)
java优雅重试机制spring-retry_第5张图片

重试监听者(实现不同阶段的通知功能)
java优雅重试机制spring-retry_第6张图片

4.3 RetryPolicy提供了如下策略实现

  • NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试;

  • AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环;

  • SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略;

  • TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试;

  • CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

    • delegate:是真正判断是否重试的策略,当重试失败时,则执行熔断策略;应该配置基于次数的SimpleRetryPolicy或者基于超时的TimeoutRetryPolicy策略,且策略都是全局模式,而非局部模式,所以要注意次数或超时的配置合理性。
    • openTimeout:openWindow,配置熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路);
    • resetTimeout:timeout,配置重置熔断器重新闭合的超时时间
  • CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行。


4.4 BackOffPolicy 提供了如下策略实现:

  • NoBackOffPolicy:无退避算法策略,即当重试时是立即重试;
  • FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper(指定等待策略,默认是Thread.sleep,即线程休眠)、backOffPeriod(休眠时间,默认1秒);
  • UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod、maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒;
  • ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier。initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier;
  • ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。

4.5源码分析

  代码入口:是启动类的这个@EnableRetry注解,自动装配了一个RetryConfiguration.class的重试配置类
java优雅重试机制spring-retry_第7张图片

@SuppressWarnings("serial")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Component
public class RetryConfiguration extends AbstractPointcutAdvisor
		implements IntroductionAdvisor, BeanFactoryAware, InitializingBean {

	private Advice advice;

	private Pointcut pointcut;

	private RetryContextCache retryContextCache;

	private List<RetryListener> retryListeners;

	private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator;

	private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier;

	private Sleeper sleeper;

	private BeanFactory beanFactory;
    //执行顺序是:@PostConstruct > afterPropertiesSet > initMethod
	@Override
	public void afterPropertiesSet() throws Exception {
        //初始化上下文缓存对象
		this.retryContextCache = findBean(RetryContextCache.class);
		this.methodArgumentsKeyGenerator = findBean(MethodArgumentsKeyGenerator.class);
		this.newMethodArgumentsIdentifier = findBean(NewMethodArgumentsIdentifier.class);
        //找到监听器的对象集合
		this.retryListeners = findBeans(RetryListener.class);
		this.sleeper = findBean(Sleeper.class);
		Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
		retryableAnnotationTypes.add(Retryable.class);
        // 构建切点
		this.pointcut = buildPointcut(retryableAnnotationTypes);
        // 构建通知
		this.advice = buildAdvice();
		if (this.advice instanceof BeanFactoryAware) {
			((BeanFactoryAware) this.advice).setBeanFactory(this.beanFactory);
		}
	}
    .................
	protected Advice buildAdvice() {
        //注解解析重试操作拦截器
		AnnotationAwareRetryOperationsInterceptor interceptor = new AnnotationAwareRetryOperationsInterceptor();
		if (this.retryContextCache != null) {
			interceptor.setRetryContextCache(this.retryContextCache);
		}
		if (this.retryListeners != null) {
			interceptor.setListeners(this.retryListeners);
		}
		if (this.methodArgumentsKeyGenerator != null) {
			interceptor.setKeyGenerator(this.methodArgumentsKeyGenerator);
		}
		if (this.newMethodArgumentsIdentifier != null) {
			interceptor.setNewItemIdentifier(this.newMethodArgumentsIdentifier);
		}
		if (this.sleeper != null) {
			interceptor.setSleeper(this.sleeper);
		}
		return interceptor;
	}

	/**
	 * Calculate a pointcut for the given retry annotation types, if any.
	 * @param retryAnnotationTypes the retry annotation types to introspect
	 * @return the applicable Pointcut object, or {@code null} if none
	 */
	protected Pointcut buildPointcut(Set<Class<? extends Annotation>> retryAnnotationTypes) {
		ComposablePointcut result = null;
        //注解类或方法解析生成切点
		for (Class<? extends Annotation> retryAnnotationType : retryAnnotationTypes) {
			Pointcut filter = new AnnotationClassOrMethodPointcut(retryAnnotationType);
			if (result == null) {
				result = new ComposablePointcut(filter);
			}
			else {
				result.union(filter);
			}
		}
		return result;
	}

	private final class AnnotationClassOrMethodPointcut extends StaticMethodMatcherPointcut {

		private final MethodMatcher methodResolver;

		AnnotationClassOrMethodPointcut(Class<? extends Annotation> annotationType) {
			this.methodResolver = new AnnotationMethodMatcher(annotationType);
			setClassFilter(new AnnotationClassOrMethodFilter(annotationType));
		}
       
		@Override
		public boolean matches(Method method, Class<?> targetClass) {
			return getClassFilter().matches(targetClass) || this.methodResolver.matches(method, targetClass);
		}

		@Override
		public boolean equals(Object other) {
			if (this == other) {
				return true;
			}
			if (!(other instanceof AnnotationClassOrMethodPointcut)) {
				return false;
			}
			AnnotationClassOrMethodPointcut otherAdvisor = (AnnotationClassOrMethodPointcut) other;
			return ObjectUtils.nullSafeEquals(this.methodResolver, otherAdvisor.methodResolver);
		}

	}

    //注解方法解析过滤
	private final class AnnotationClassOrMethodFilter extends AnnotationClassFilter {

		private final AnnotationMethodsResolver methodResolver;

		AnnotationClassOrMethodFilter(Class<? extends Annotation> annotationType) {
			super(annotationType, true);
			this.methodResolver = new AnnotationMethodsResolver(annotationType);
		}

		@Override
		public boolean matches(Class<?> clazz) {
			return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz);
		}

	}

    //注解方法解析
	private static class AnnotationMethodsResolver {

		private Class<? extends Annotation> annotationType;

		public AnnotationMethodsResolver(Class<? extends Annotation> annotationType) {
			this.annotationType = annotationType;
		}

		public boolean hasAnnotatedMethods(Class<?> clazz) {
			final AtomicBoolean found = new AtomicBoolean(false);
			ReflectionUtils.doWithMethods(clazz, new MethodCallback() {
				@Override
				public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
					if (found.get()) {
						return;
					}
					Annotation annotation = AnnotationUtils.findAnnotation(method,
							AnnotationMethodsResolver.this.annotationType);
					if (annotation != null) {
						found.set(true);
					}
				}
			});
			return found.get();
		}

	}

}

  AnnotationAwareRetryOperationsInterceptor( //注解解析重试操作拦截器)类是方法调用的关键类

关键代码如下:

   // 基于AOP和springBoot2.x默认使用CGLib的动态代理对目标方法进行拦截,这个invoke方法就是会在方法调用的时候会被触发到
    @Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
        //生成一个代理对象MethodInterceptor
		MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
		if (delegate != null) {
			return delegate.invoke(invocation);
		}
		else {
			return invocation.proceed();
		}
	}

	private MethodInterceptor getDelegate(Object target, Method method) {
        // 目标方法对应的代理对象你的的一个缓存
		ConcurrentMap<Method, MethodInterceptor> cachedMethods = this.delegates.get(target);
		if (cachedMethods == null) {
			cachedMethods = new ConcurrentHashMap<Method, MethodInterceptor>();
		}
        //缓存中有直接使用
		MethodInterceptor delegate = cachedMethods.get(method);
        //缓存中没有则生成后加入缓存
		if (delegate == null) {
			MethodInterceptor interceptor = NULL_INTERCEPTOR;
			Retryable retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class);
			if (retryable == null) {
				retryable = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Retryable.class);
			}
			if (retryable == null) {
				retryable = findAnnotationOnTarget(target, method, Retryable.class);
			}
			if (retryable != null) {
                // 从spring容器中获取代理对象
				if (StringUtils.hasText(retryable.interceptor())) {
					interceptor = this.beanFactory.getBean(retryable.interceptor(), MethodInterceptor.class);
				}
                // 有状态重试的代理对象
				else if (retryable.stateful()) {
					interceptor = getStatefulInterceptor(target, method, retryable);
				}
				else {
                      // 无状态重试的代理对象(默认)
					interceptor = getStatelessInterceptor(target, method, retryable);
				}
			}
			cachedMethods.putIfAbsent(method, interceptor);
			delegate = cachedMethods.get(method);
		}
		this.delegates.putIfAbsent(target, cachedMethods);
		return delegate == NULL_INTERCEPTOR ? null : delegate;
	}

   // 无状态代理对象
   private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable retryable) {
	   // 创建重试模板(模板方法模式)
        RetryTemplate template = createTemplate(retryable.listeners());
        // 设置重试策略
		template.setRetryPolicy(getRetryPolicy(retryable));
        // 设置补偿策略
		template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
		return RetryInterceptorBuilder
              // 构建无状态代理对象
              .stateless().
              retryOperations(template).label(retryable.label())
			   // 匹配回退策略
              .recoverer(getRecoverer(target, method)).build();
	}
  // 有状态代理对象
	private MethodInterceptor getStatefulInterceptor(Object target, Method method, Retryable retryable) {
		RetryTemplate template = createTemplate(retryable.listeners());
		template.setRetryContextCache(this.retryContextCache);

		CircuitBreaker circuit = AnnotatedElementUtils.findMergedAnnotation(method, CircuitBreaker.class);
		if (circuit == null) {
			circuit = findAnnotationOnTarget(target, method, CircuitBreaker.class);
		}
		if (circuit != null) {
			RetryPolicy policy = getRetryPolicy(circuit);
			CircuitBreakerRetryPolicy breaker = new CircuitBreakerRetryPolicy(policy);
			breaker.setOpenTimeout(getOpenTimeout(circuit));
			breaker.setResetTimeout(getResetTimeout(circuit));
			template.setRetryPolicy(breaker);
			template.setBackOffPolicy(new NoBackOffPolicy());
			String label = circuit.label();
			if (!StringUtils.hasText(label)) {
				label = method.toGenericString();
			}
			return RetryInterceptorBuilder.circuitBreaker().keyGenerator(new FixedKeyGenerator("circuit"))
					.retryOperations(template).recoverer(getRecoverer(target, method)).label(label).build();
		}
		RetryPolicy policy = getRetryPolicy(retryable);
		template.setRetryPolicy(policy);
		template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
		String label = retryable.label();
		return RetryInterceptorBuilder.stateful().keyGenerator(this.methodArgumentsKeyGenerator)		.newMethodArgumentsIdentifier(this.newMethodArgumentsIdentifier).retryOperations(template).label(label)
				.recoverer(getRecoverer(target, method)).build();
	}

java优雅重试机制spring-retry_第8张图片

  RetryOperationsInterceptor类中实际操作的对象是retryTemplate,关键代码如下:

public class RetryOperationsInterceptor implements MethodInterceptor {

	private RetryOperations retryOperations = new RetryTemplate();

	private MethodInvocationRecoverer<?> recoverer;

	private String label;

	public void setLabel(String label) {
		this.label = label;
	}

	public void setRetryOperations(RetryOperations retryTemplate) {
		Assert.notNull(retryTemplate, "'retryOperations' cannot be null.");
		this.retryOperations = retryTemplate;
	}
    // 代理对象会执行到xxxxInterceptorBuilder中构建的RetryOperationsInterceptor的invoke方法如下
   @Override
	public Object invoke(final MethodInvocation invocation) throws Throwable {

		String name;
		if (StringUtils.hasText(this.label)) {
			name = this.label;
		}
		else {
			name = invocation.getMethod().toGenericString();
		}
		final String label = name;
        
		RetryCallback<Object, Throwable> retryCallback = new MethodInvocationRetryCallback<Object, Throwable>(
				invocation, label) {

			@Override
			public Object doWithRetry(RetryContext context) throws Exception {

				context.setAttribute(RetryContext.NAME, this.label);

				/*
				 * If we don't copy the invocation carefully it won't keep a reference to
				 * the other interceptors in the chain. We don't have a choice here but to
				 * specialise to ReflectiveMethodInvocation (but how often would another
				 * implementation come along?).
				 */
				if (this.invocation instanceof ProxyMethodInvocation) {
					context.setAttribute("___proxy___", ((ProxyMethodInvocation) this.invocation).getProxy());
					try {
                        // 执行目标类的目标方法
						return ((ProxyMethodInvocation) this.invocation).invocableClone().proceed();
					}
					catch (Exception e) {
						throw e;
					}
					catch (Error e) {
						throw e;
					}
					catch (Throwable e) {
						throw new IllegalStateException(e);
					}
				}
				else {
					throw new IllegalStateException(
							"MethodInvocation of the wrong type detected - this should not happen with Spring AOP, "
									+ "so please raise an issue if you see this exception");
				}
			}

		};

		if (this.recoverer != null) {
			ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(),
					this.recoverer);
			try {
				Object recovered = this.retryOperations.execute(retryCallback, recoveryCallback);
				return recovered;
			}
			finally {
				RetryContext context = RetrySynchronizationManager.getContext();
				if (context != null) {
					context.removeAttribute("__proxy__");
				}
			}
		}
        
		return this.retryOperations.execute(retryCallback);

	}
}

  上面的invoke方法最终会调用RetryTemplate类中的几个execute()的重载方法如下:
java优雅重试机制spring-retry_第9张图片

最后会调用到doExecute()方法,这个方法就是spring-retry的核心方法:

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;

		// Allow the retry policy to initialise itself...
		RetryContext context = open(retryPolicy, state);
		if (this.logger.isTraceEnabled()) {
			this.logger.trace("RetryContext retrieved: " + context);
		}

		// Make sure the context is available globally for clients who need
		// it...
        //重试同步上下文管理器,里面有一个private static final ThreadLocal context = new ThreadLocal();属性,使用ThreadLocal对各种重试策略的上下文做一个多线程隔了
		RetrySynchronizationManager.register(context);

		Throwable lastException = null;

		boolean exhausted = false;
		try {

			// Give clients a chance to enhance the context...
			boolean running = doOpenInterceptors(retryCallback, context);

			if (!running) {
				throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
			}

			// 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);
				}
			}

			/*
			 * 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).
			 */
            // 执行对上下文的应策略的canRetry方法判断是否可以是否满足重试条件
			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;
                    // 调用目标类的目标方法 目标方法执行成功则返回,执行失败则进入catch中捕获异常,进入重试调用
					return retryCallback.doWithRetry(context);
				}
                // 
				catch (Throwable e) {

					lastException = e;

					try {
                        // 在上下文中注册一个异常,然后上面的while条件会一直的去执行canRetry是否可以重试,然后哦一直调用目标类的目标方法成功返回,然后不成功直到不满足重试条件则调用退出,不可能一直的while(true)死循环的调用,肯定要有一个出口的
						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;
				}
			}

			if (state == null && this.logger.isDebugEnabled()) {
				this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount());
			}

			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();
		
					}

				}

				/*
				 * 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;
				}
			}

			if (state == null && this.logger.isDebugEnabled()) {
				this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount());
			}

			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();
		}

	}

5.总结

  由于之前做了一个停车缴费的项目,平台应用要和终端设备通过网络调用终端的先关接口,由于终端设备的网络是免费的软件搭建的网络(没有使用专线,使用专线就需要额外的费用,所以不划算)一直会出现掉线导致调用终端接口因为网络掉线而出现SocketTimeoutException异常,多以就尝试使用异步重试,多试几次的方式,后面就使用了这个spring-retry的类库做了一个优雅的重试,但是最终还是没有解决这种网络问题导致重试n多次都还是不行,但是这种重试机制在网络基本可靠一些其它适合spring-retry的场景还是很ok的,后面又花了点时间看了下spring-retry的源码对这个类库有了很好的掌握和了解,看源码其实是一种比较好的学习探索的方式,看大佬们是如何写出优雅漂亮、令人赏心悦目的代码的,从中可以学习大佬的设计思想和各种设计模式的运用;最后是使用了Tio的一个半开源的websocket类库写了一个clien的终端你程序跟平台应用建立websocket心跳连接来保持在线的,同时平台端可以给指定的client端发送websocket消息,client端收到消息后就可以在本地调用终端软件的接口来完成和终端的交互了,到此就是使用spring-retry的心得和总结。

你可能感兴趣的:(java,spring,开发语言)