欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java编程Retry重试机制实例详解

程序员文章站 2023-01-03 19:15:13
本文研究的主要是java编程retry重试机制实例详解,分享了相关代码示例,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下 1、业务场景 应用中需要实...

本文研究的主要是java编程retry重试机制实例详解,分享了相关代码示例,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下

1、业务场景

应用中需要实现一个功能: 需要将数据上传到远程存储服务,同时在返回处理成功情况下做其他操作。这个功能不复杂,分为两个步骤:第一步调用远程的rest服务逻辑包装给处理方法返回处理结果;第二步拿到第一步结果或者捕捉异常,如果出现错误或异常实现重试上传逻辑,否则继续逻辑操作。

2、常规解决方案演化

1)try-catch-redo简单重试模式:

包装正常上传逻辑基础上,通过判断返回结果或监听异常决策是否重试,同时为了解决立即重试的无效执行(假设异常是有外部执行不稳定导致的),休眠一定延迟时间重新执行功能逻辑。

public void commonretry(map<string, object> datamap) throws interruptedexception { 
    map<string, object> parammap = maps.newhashmap(); 
    parammap.put("tablename", "creativetable"); 
    parammap.put("ds", "20160220"); 
    parammap.put("datamap", datamap); 
    boolean result = false; 
    try { 
      result = uploadtoodps(parammap); 
      if (!result) { 
        thread.sleep(1000); 
        uploadtoodps(parammap); //一次重试 
      } 
    } catch (exception e) { 
      thread.sleep(1000); 
      uploadtoodps(parammap);//一次重试 
    } 
  } 

2)try-catch-redo-retry strategy策略重试模式:

上述方案还是有可能重试无效,解决这个问题尝试增加重试次数retrycount以及重试间隔周期interval,达到增加重试有效的可能性。

public void commonretry(map<string, object> datamap) throws interruptedexception { 
    map<string, object> parammap = maps.newhashmap(); 
    parammap.put("tablename", "creativetable"); 
    parammap.put("ds", "20160220"); 
    parammap.put("datamap", datamap); 
    boolean result = false; 
    try { 
      result = uploadtoodps(parammap); 
      if (!result) { 
        reuploadtoodps(parammap,1000l,10);//延迟多次重试 
      } 
    } catch (exception e) { 
      reuploadtoodps(parammap,1000l,10);//延迟多次重试 
    } 
  } 

方案一和方案二存在一个问题:正常逻辑和重试逻辑强耦合,重试逻辑非常依赖正常逻辑的执行结果,对正常逻辑预期结果被动重试触发,对于重试根源往往由于逻辑复杂被淹没,可能导致后续运维对于重试逻辑要解决什么问题产生不一致理解。重试正确性难保证而且不利于运维,原因是重试设计依赖正常逻辑异常或重试根源的臆测。

3、优雅重试方案尝试:

那有没有可以参考的方案实现正常逻辑和重试逻辑解耦,同时能够让重试逻辑有一个标准化的解决思路?答案是有:那就是基于代理设计模式的重试工具,我们尝试使用相应工具来重构上述场景。

1)应用命令设计模式解耦正常和重试逻辑:

命令设计模式具体定义不展开阐述,主要该方案看中命令模式能够通过执行对象完成接口操作逻辑,同时内部封装处理重试逻辑,不暴露实现细节,对于调用者来看就是执行了正常逻辑,达到解耦的目标,具体看下功能实现。(类图结构)

Java编程Retry重试机制实例详解

iretry约定了上传和重试接口,其实现类odpsretry封装odps上传逻辑,同时封装重试机制和重试策略。与此同时使用recover方法在结束执行做恢复操作。

而我们的调用者logicclient无需关注重试,通过重试者retryer实现约定接口功能,同时 retryer需要对重试逻辑做出响应和处理, retryer具体重试处理又交给真正的irtry接口的实现类odpsretry完成。通过采用命令模式,优雅实现正常逻辑和重试逻辑分离,同时通过构建重试者角色,实现正常逻辑和重试逻辑的分离,让重试有更好的扩展性。

2)spring-retry 规范正常和重试逻辑

spring-retry是一个开源工具包,目前可用的版本为1.1.2.release,该工具把重试操作模板定制化,可以设置重试策略和回退策略。同时重试执行实例保证线程安全,具体场景操作实例如下:

public void upload(final map<string, object> map) throws exception { 
    // 构建重试模板实例 
    retrytemplate retrytemplate = new retrytemplate(); 
    // 设置重试策略,主要设置重试次数 
    simpleretrypolicy policy = new simpleretrypolicy(3, collections.<class<? extends throwable>, boolean> singletonmap(exception.class, true)); 
    // 设置重试回退操作策略,主要设置重试间隔时间 
    fixedbackoffpolicy fixedbackoffpolicy = new fixedbackoffpolicy(); 
    fixedbackoffpolicy.setbackoffperiod(100); 
    retrytemplate.setretrypolicy(policy); 
    retrytemplate.setbackoffpolicy(fixedbackoffpolicy); 
    // 通过retrycallback 重试回调实例包装正常逻辑逻辑,第一次执行和重试执行执行的都是这段逻辑 
    final retrycallback<object, exception> retrycallback = new retrycallback<object, exception>() { 
      //retrycontext 重试操作上下文约定,统一spring-try包装  
      public object dowithretry(retrycontext context) throws exception { 
        system.out.println("do some thing"); 
        exception e = uploadtoodps(map); 
        system.out.println(context.getretrycount()); 
        throw e;//这个点特别注意,重试的根源通过exception返回 
      } 
    }; 
    // 通过recoverycallback 重试流程正常结束或者达到重试上限后的退出恢复操作实例 
    final recoverycallback<object> recoverycallback = new recoverycallback<object>() { 
      public object recover(retrycontext context) throws exception { 
        system.out.println("do recory operation"); 
        return null; 
      } 
    }; 
    try { 
      // 由retrytemplate 执行execute方法开始逻辑执行 
      retrytemplate.execute(retrycallback, recoverycallback); 
    } catch (exception e) { 
      e.printstacktrace(); 
    } 
  } 

简单剖析下案例代码,retrytemplate 承担了重试执行者的角色,它可以设置simpleretrypolicy(重试策略,设置重试上限,重试的根源实体),fixedbackoffpolicy(固定的回退策略,设置执行重试回退的时间间隔)。retrytemplate通过execute提交执行操作,需要准备retrycallback 和recoverycallback 两个类实例,前者对应的就是重试回调逻辑实例,包装正常的功能操作,recoverycallback实现的是整个执行操作结束的恢复操作实例。

retrytemplate的execute 是线程安全的,实现逻辑使用threadlocal保存每个执行实例的retrycontext执行上下文。

spring-retry工具虽能优雅实现重试,但是存在两个不友好设计:一个是 重试实体限定为throwable子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,但sping-retry框架必须强制转换为throwable子类。另一个就是重试根源的断言对象使用的是dowithretry的exception 异常实例,不符合正常内部断言的返回设计。

spring retry提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是throwable,如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

spring 对于retry的抽象

“抽象”是每个程序员必备的素质。对于资质平平的我来说,没有比模仿与理解优秀源码更好地进步途径了吧。为此,我将其核心逻辑重写了一遍...下面就看看spring retry对于“重试”的抽象。

Java编程Retry重试机制实例详解

spring retry相关接口.jpg

  • retrycallback: 封装你需要重试的业务逻辑(上文中的dosth)
  • recovercallback:封装在多次重试都失败后你需要执行的业务逻辑(上文中的dosthwhenstillfail)
  • retrycontext: 重试语境下的上下文,可用于在多次retry或者retry 和recover之间传递参数或状态(在多次dosth或者dosth与dosthwhenstillfail之间传递参数)
  • retryoperations : 定义了“重试”的基本框架(模板),要求传入retrycallback,可选传入recoverycallback;
  • retrylistener:典型的“监听者”,在重试的不同阶段通知“监听者”(例如dosth,wait等阶段时通知)
  • retrypolicy : 重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试(上文中的somecondition)
  • backoffpolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),当然这段时间可以是0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait();
  • retrytemplate :retryoperations的具体实现,组合了retrylistener[],backoffpolicy,retrypolicy。

3)guava-retryer 分离正常和重试逻辑

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。guava retryer也是线程安全的,入口调用逻辑采用的是java.util.concurrent.callable的call方法,示例代码如下:

public void uploadodps(final map<string, object> map) { 
    // retryerbuilder 构建重试实例 retryer,可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔 
    retryer<boolean> retryer = retryerbuilder.<boolean> newbuilder() 
        .retryifexception().//设置异常重试源 
        retryifresult(new predicate<boolean>() {//设置自定义段元重试源, 
      @override 
      public boolean apply(boolean state) {//特别注意:这个apply返回true说明需要重试,与操作逻辑的语义要区分 
        return true; 
      } 
    }) 
    .withstopstrategy(stopstrategies.stopafterattempt(5))//设置重试5次,同样可以设置重试超时时间 
    .withwaitstrategy(waitstrategies.fixedwait(100l, timeunit.milliseconds)).build();//设置每次重试间隔 
 
    try { 
      //重试入口采用call方法,用的是java.util.concurrent.callable<v>的call方法,所以执行是线程安全的 
      boolean result = retryer.call(new callable<boolean>() {  
        @override 
        public boolean call() throws exception { 
          try { 
            //特别注意:返回false说明无需重试,返回true说明需要继续重试 
            return uploadtoodps(map); 
          } catch (exception e) { 
            throw new exception(e); 
          } 
        } 
      }); 
 
    } catch (executionexception e) { 
    } catch (retryexception ex) { 
    } 
  } 

示例代码原理分析:

retryerbuilder是一个factory创建者,可以定制设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔,创建重试者retryer实例。

retryerbuilder的重试源支持exception异常对象 和自定义断言对象,通过retryifexception 和retryifresult设置,同时支持多个且能兼容。

retryerbuilder的等待时间和重试限制配置采用不同的策略类实现,同时对于等待时间特征可以支持无间隔和固定间隔方式。

retryer 是重试者实例,通过call方法执行操作逻辑,同时封装重试源操作。

优雅重试共性和原理

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

优雅重试适用场景

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

总结

以上就是本文关于java编程retry重试机制实例详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!