经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
JAVA重试机制多种方式深入浅出
来源:cnblogs  作者:Yubaba丶  时间:2023/3/14 8:47:23  对本文有异议

重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。

重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。

为了方便说明,先假设我们想要进行重试的方法如下:

  1. @Slf4j
  2. @Component
  3. public class HelloService {
  4. private static AtomicLong helloTimes = new AtomicLong();
  5. public String hello(){
  6. long times = helloTimes.incrementAndGet();
  7. if (times % 4 != 0){
  8. log.warn("发生异常,time:{}", LocalTime.now() );
  9. throw new HelloRetryException("发生Hello异常");
  10. }
  11. return "hello";
  12. }
  13. }

调用处:

  1. @Slf4j
  2. @Service
  3. public class HelloRetryService implements IHelloService{
  4. @Autowired
  5. private HelloService helloService;
  6. public String hello(){
  7. return helloService.hello();
  8. }
  9. }

也就是说,这个接口每调4次才会成功一次。

手动重试

先来用最简单的方法,直接在调用的时候进重试:

  1. // 手动重试
  2. public String hello(){
  3. int maxRetryTimes = 4;
  4. String s = "";
  5. for (int retry = 1; retry <= maxRetryTimes; retry++) {
  6. try {
  7. s = helloService.hello();
  8. log.info("helloService返回:{}", s);
  9. return s;
  10. } catch (HelloRetryException e) {
  11. log.info("helloService.hello() 调用失败,准备重试");
  12. }
  13. }
  14. throw new HelloRetryException("重试次数耗尽");
  15. }
  1. 发生异常,time10:17:21.079413300
  2. helloService.hello() 调用失败,准备重试
  3. 发生异常,time10:17:21.085861800
  4. helloService.hello() 调用失败,准备重试
  5. 发生异常,time10:17:21.085861800
  6. helloService.hello() 调用失败,准备重试
  7. helloService返回:hello
  8. service.helloRetry():hello

程序在极短的时间内进行了4次重试,然后成功返回。

这样虽然看起来可以解决问题,但实践上,由于没有重试间隔,很可能当时依赖的服务尚未从网络异常中恢复过来,所以极有可能接下来的几次调用都是失败的。

而且,这样需要对代码进行大量的侵入式修改,显然,不优雅。

代理模式

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。

所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。你会发现又是代理。

  1. @Slf4j
  2. public class HelloRetryProxyService implements IHelloService{
  3. @Autowired
  4. private HelloRetryService helloRetryService;
  5. @Override
  6. public String hello() {
  7. int maxRetryTimes = 4;
  8. String s = "";
  9. for (int retry = 1; retry <= maxRetryTimes; retry++) {
  10. try {
  11. s = helloRetryService.hello();
  12. log.info("helloRetryService 返回:{}", s);
  13. return s;
  14. } catch (HelloRetryException e) {
  15. log.info("helloRetryService.hello() 调用失败,准备重试");
  16. }
  17. }
  18. throw new HelloRetryException("重试次数耗尽");
  19. }
  20. }

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了,分工明确。比如,现在想要在重试之间加上一个延迟,只需要做一点点修改即可:

  1. @Override
  2. public String hello() {
  3. int maxRetryTimes = 4;
  4. String s = "";
  5. for (int retry = 1; retry <= maxRetryTimes; retry++) {
  6. try {
  7. s = helloRetryService.hello();
  8. log.info("helloRetryService 返回:{}", s);
  9. return s;
  10. } catch (HelloRetryException e) {
  11. log.info("helloRetryService.hello() 调用失败,准备重试");
  12. }
  13. // 延时一秒
  14. try {
  15. Thread.sleep(1000);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. throw new HelloRetryException("重试次数耗尽");
  21. }

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。

如果每个类都写这么一长串类似的代码,显然,不优雅!

JDK动态代理

这时候,动态代理就闪亮登场了。只需要写一个代理处理类就ok了。

  1. @Slf4j
  2. public class RetryInvocationHandler implements InvocationHandler {
  3. private final Object subject;
  4. public RetryInvocationHandler(Object subject) {
  5. this.subject = subject;
  6. }
  7. @Override
  8. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  9. int times = 0;
  10. while (times < RetryConstant.MAX_TIMES) {
  11. try {
  12. return method.invoke(subject, args);
  13. } catch (Exception e) {
  14. times++;
  15. log.info("times:{},time:{}", times, LocalTime.now());
  16. if (times >= RetryConstant.MAX_TIMES) {
  17. throw new RuntimeException(e);
  18. }
  19. }
  20. // 延时一秒
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. return null;
  28. }
  29. /**
  30. * 获取动态代理
  31. *
  32. * @param realSubject 代理对象
  33. */
  34. public static Object getProxy(Object realSubject) {
  35. InvocationHandler handler = new RetryInvocationHandler(realSubject);
  36. return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
  37. realSubject.getClass().getInterfaces(), handler);
  38. }
  39. }

咱们测试一下:

  1. @Test
  2. public void helloDynamicProxy() {
  3. IHelloService realService = new HelloService();
  4. IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
  5. String hello = proxyService.hello();
  6. log.info("hello:{}", hello);
  7. }
  8. 输出结果如下:
  9. hello times:1
  10. 发生异常,time11:22:20.727586700
  11. times:1,time:11:22:20.728083
  12. hello times:2
  13. 发生异常,time11:22:21.728858700
  14. times:2,time:11:22:21.729343700
  15. hello times:3
  16. 发生异常,time11:22:22.729706600
  17. times:3,time:11:22:22.729706600
  18. hello times:4
  19. hello:hello

在重试了4次之后输出了Hello,符合预期。

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

不过不要高兴的太早,这里因为被代理的HelloService是一个简单的类,没有依赖其它类,所以直接创建是没有问题的,但如果被代理的类依赖了其它被Spring容器管理的类,则这种方式会抛出异常,因为没有把被依赖的实例注入到创建的代理实例中。

这种情况下,就比较复杂了,需要从Spring容器中获取已经装配好的,需要被代理的实例,然后为其创建代理类实例,并交给Spring容器来管理,这样就不用每次都重新创建新的代理类实例了。

话不多说,撸起袖子就是干。

新建一个工具类,用来获取代理实例:

  1. @Component
  2. public class RetryProxyHandler {
  3. @Autowired
  4. private ConfigurableApplicationContext context;
  5. public Object getProxy(Class clazz) {
  6. // 1. 从Bean中获取对象
  7. DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
  8. Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
  9. Set<Map.Entry<String, Object>> entries = beans.entrySet();
  10. if (entries.size() <= 0){
  11. throw new ProxyBeanNotFoundException();
  12. }
  13. // 如果有多个候选bean, 判断其中是否有代理bean
  14. Object bean = null;
  15. if (entries.size() > 1){
  16. for (Map.Entry<String, Object> entry : entries) {
  17. if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
  18. bean = entry.getValue();
  19. }
  20. };
  21. if (bean != null){
  22. return bean;
  23. }
  24. throw new ProxyBeanNotSingleException();
  25. }
  26. Object source = beans.entrySet().iterator().next().getValue();
  27. Object source = beans.entrySet().iterator().next().getValue();
  28. // 2. 判断该对象的代理对象是否存在
  29. String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
  30. Boolean exist = beanFactory.containsBean(proxyBeanName);
  31. if (exist) {
  32. bean = beanFactory.getBean(proxyBeanName);
  33. return bean;
  34. }
  35. // 3. 不存在则生成代理对象
  36. bean = RetryInvocationHandler.getProxy(source);
  37. // 4. 将bean注入spring容器
  38. beanFactory.registerSingleton(proxyBeanName, bean);
  39. return bean;
  40. }
  41. }

使用的是JDK动态代理:

  1. @Slf4j
  2. public class RetryInvocationHandler implements InvocationHandler {
  3. private final Object subject;
  4. public RetryInvocationHandler(Object subject) {
  5. this.subject = subject;
  6. }
  7. @Override
  8. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  9. int times = 0;
  10. while (times < RetryConstant.MAX_TIMES) {
  11. try {
  12. return method.invoke(subject, args);
  13. } catch (Exception e) {
  14. times++;
  15. log.info("retry times:{},time:{}", times, LocalTime.now());
  16. if (times >= RetryConstant.MAX_TIMES) {
  17. throw new RuntimeException(e);
  18. }
  19. }
  20. // 延时一秒
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. return null;
  28. }
  29. /**
  30. * 获取动态代理
  31. *
  32. * @param realSubject 代理对象
  33. */
  34. public static Object getProxy(Object realSubject) {
  35. InvocationHandler handler = new RetryInvocationHandler(realSubject);
  36. return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
  37. realSubject.getClass().getInterfaces(), handler);
  38. }
  39. }

至此,主要代码就完成了,修改一下HelloService类,增加一个依赖:

  1. @Slf4j
  2. @Component
  3. public class HelloService implements IHelloService{
  4. private static AtomicLong helloTimes = new AtomicLong();
  5. @Autowired
  6. private NameService nameService;
  7. public String hello(){
  8. long times = helloTimes.incrementAndGet();
  9. log.info("hello times:{}", times);
  10. if (times % 4 != 0){
  11. log.warn("发生异常,time:{}", LocalTime.now() );
  12. throw new HelloRetryException("发生Hello异常");
  13. }
  14. return "hello " + nameService.getName();
  15. }
  16. }

NameService其实很简单,创建的目的仅在于测试依赖注入的Bean能否正常运行。

  1. @Service
  2. public class NameService {
  3. public String getName(){
  4. return "Frank";
  5. }
  6. }

测试一下:

  1. @Test
  2. public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
  3. IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
  4. String hello = proxy.hello();
  5. log.info("hello:{}", hello);
  6. }
  7. 结果:
  8. hello times:1
  9. 发生异常,time14:40:27.540672200
  10. retry times:1,time:14:40:27.541167400
  11. hello times:2
  12. 发生异常,time14:40:28.541584600
  13. retry times:2,time:14:40:28.542033500
  14. hello times:3
  15. 发生异常,time14:40:29.542161500
  16. retry times:3,time:14:40:29.542161500
  17. hello times:4
  18. hello:hello Frank

完美,这样就不用担心依赖注入的问题了,因为从Spring容器中拿到的Bean对象都是已经注入配置好的。当然,这里仅考虑了单例Bean的情况,可以考虑的更加完善一点,判断一下容器中Bean的类型是Singleton还是Prototype,如果是Singleton则像上面这样进行操作,如果是Prototype则每次都新建代理类对象。

另外,这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了。

Spring AOP

想要无侵入式的修改原有逻辑?想要一个注解就实现重试?用Spring AOP不就能完美实现吗?使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些额外的逻辑。

先创建一个注解:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Retryable {
  5. int retryTimes() default 3;
  6. int retryInterval() default 1;
  7. }

有两个参数,retryTimes 代表最大重试次数,retryInterval代表重试间隔。

  1. @Retryable(retryTimes = 4, retryInterval = 2)
  2. public String hello(){
  3. long times = helloTimes.incrementAndGet();
  4. log.info("hello times:{}", times);
  5. if (times % 4 != 0){
  6. log.warn("发生异常,time:{}", LocalTime.now() );
  7. throw new HelloRetryException("发生Hello异常");
  8. }
  9. return "hello " + nameService.getName();
  10. }

接着,进行最后一步,编写AOP切面:

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. public class RetryAspect {
  5. @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
  6. private void retryMethodCall(){}
  7. @Around("retryMethodCall()")
  8. public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
  9. // 获取重试次数和重试间隔
  10. Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);
  11. int maxRetryTimes = retry.retryTimes();
  12. int retryInterval = retry.retryInterval();
  13. Throwable error = new RuntimeException();
  14. for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
  15. try {
  16. Object result = joinPoint.proceed();
  17. return result;
  18. } catch (Throwable throwable) {
  19. error = throwable;
  20. log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
  21. }
  22. Thread.sleep(retryInterval * 1000);
  23. }
  24. throw new RetryExhaustedException("重试次数耗尽", error);
  25. }
  26. }

开始测试:

  1. @Autowired
  2. private HelloService helloService;
  3. @Test
  4. public void helloAOP(){
  5. String hello = helloService.hello();
  6. log.info("hello:{}", hello);
  7. }
  8. 打印结果:
  9. hello times:1
  10. 发生异常,time16:49:30.224649800
  11. 调用发生异常,开始重试,retryTimes:1
  12. hello times:2
  13. 发生异常,time16:49:32.225230800
  14. 调用发生异常,开始重试,retryTimes:2
  15. hello times:3
  16. 发生异常,time16:49:34.225968900
  17. 调用发生异常,开始重试,retryTimes:3
  18. hello times:4
  19. hello:hello Frank

这样就相当优雅了,一个注解就能搞定重试,简直不要更棒。

Spring 的重试注解

实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。

那让我们先来看看这个轮子究竟好不好使。

先引入重试所需的jar包:

  1. <dependency>
  2. <groupId>org.springframework.retry</groupId>
  3. <artifactId>spring-retry</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.aspectj</groupId>
  7. <artifactId>aspectjweaver</artifactId>
  8. </dependency>

然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?竟然抄袭我的注解【手动狗头】)

  1. @Retryable
  2. public String hello(){
  3. long times = helloTimes.incrementAndGet();
  4. log.info("hello times:{}", times);
  5. if (times % 4 != 0){
  6. log.warn("发生异常,time:{}", LocalTime.now() );
  7. throw new HelloRetryException("发生Hello异常");
  8. }
  9. return "hello " + nameService.getName();
  10. }

默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。

但Spring里的重试机制还支持很多很有用的特性,比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。

  1. @Retryable{value = {HelloRetryException.class}}
  2. public String hello(){
  3. ...
  4. }

也可以使用include和exclude来指定包含或者排除哪些异常进行重试。

可以用maxAttemps指定最大重试次数,默认为3次。

可以用interceptor设置重试拦截器的bean名称。

可以通过label设置该重试的唯一标志,用于统计输出。

可以使用exceptionExpression来添加异常表达式,在抛出异常后执行,以判断后续是否进行重试。

此外,Spring中的重试机制还支持使用backoff来设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数。

举个例子:

  1. @Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
  2. backoff = @Backoff(delay = 1000, multiplier = 2))
  3. public String hello(){
  4. ...
  5. }

该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。

重试机制还支持使用@Recover 注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。

这里值得注意的是,想要@Recover 注解生效的话,需要跟被@Retryable 标记的方法在同一个类中,且被@Retryable 标记的方法不能有返回值,否则不会生效。

并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。

  1. @Recover
  2. public boolean recover(Exception e) {
  3. log.error("达到最大重试次数",e);
  4. return false;
  5. }

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

  1. @Test
  2. public void normalSpringRetry() {
  3. // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
  4. Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
  5. exceptionMap.put(HelloRetryException.class, true);
  6. // 构建重试模板实例
  7. RetryTemplate retryTemplate = new RetryTemplate();
  8. // 设置重试回退操作策略,主要设置重试间隔时间
  9. FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
  10. long fixedPeriodTime = 1000L;
  11. backOffPolicy.setBackOffPeriod(fixedPeriodTime);
  12. // 设置重试策略,主要设置重试次数
  13. int maxRetryTimes = 3;
  14. SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
  15. retryTemplate.setRetryPolicy(retryPolicy);
  16. retryTemplate.setBackOffPolicy(backOffPolicy);
  17. Boolean execute = retryTemplate.execute(
  18. //RetryCallback
  19. retryContext -> {
  20. String hello = helloService.hello();
  21. log.info("调用的结果:{}", hello);
  22. return true;
  23. },
  24. // RecoverCallBack
  25. retryContext -> {
  26. //RecoveryCallback
  27. log.info("已达到最大重试次数");
  28. return false;
  29. }
  30. );
  31. }

此时唯一的好处是可以设置多种重试策略:

  1. NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  2. AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  3. SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
  4. TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  5. ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  6. CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeoutresetTimeoutdelegate
  7. CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
  8. 悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

可以看出,Spring中的重试机制还是相当完善的,比上面自己写的AOP切面功能更加强大。

这里还需要再提醒的一点是,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效。

但也还是存在一定的不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验。

原文链接:https://www.cnblogs.com/lmyupupblogs/p/17212423.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号