博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java实现几种简单的重试机制
阅读量:6626 次
发布时间:2019-06-25

本文共 7517 字,大约阅读时间需要 25 分钟。

hot3.png

背景

当业务执行失败之后,进行重试是一个非常常见的场景,那么如何在业务代码中优雅的实现重试机制呢?

设计

我们的目标是实现一个优雅的重试机制,那么先来看下怎么样才算是优雅

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

针对上面的几点,分别看下右什么好的解决方案

几种解决思路

要想做到无侵入或者很小的改动,一般来将比较好的方式就是切面或者消息总线模式;可配置和通用性则比较清晰了,基本上开始做就表示这两点都是基础要求了,唯一的要求就是不要硬编码,不要写死,基本上就能达到这个基础要求,当然要优秀的话,要做的事情并不少

切面方式

这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化

优点:

  • 真正的无侵入

缺点:

  • 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)
  • 直接使用aspecj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean

消息总线方式

这个也比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑

优点:

  • 重试机制不受任何限制,即在任何地方你都可以使用
  • 利用EventBus框架,可以非常容易把框架搭起来

缺点:

  • 业务侵入,需要在重试的业务处,主动发起一条重试消息
  • 调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新的童鞋来维护这段代码时)
  • 如果要获取返回结果,不太好处理, 上下文参数不好处理

模板方式

把这个单独捞出来,主要是某些时候我就一两个地方要用到重试,简单的实现下就好了,也没有必用用到上面这么重的方式;而且我希望可以针对代码快进行重试

这个的设计还是非常简单的,基本上代码都可以直接贴出来,一目了然:

public abstract class RetryTemplate {    private static final int DEFAULT_RETRY_TIME = 1;    private int retryTime = DEFAULT_RETRY_TIME;    // 重试的睡眠时间    private int sleepTime = 0;    public int getSleepTime() {        return sleepTime;    }    public RetryTemplate setSleepTime(int sleepTime) {        if(sleepTime < 0) {            throw new IllegalArgumentException("sleepTime should equal or bigger than 0");        }        this.sleepTime = sleepTime;        return this;    }    public int getRetryTime() {        return retryTime;    }    public RetryTemplate setRetryTime(int retryTime) {        if (retryTime <= 0) {            throw new IllegalArgumentException("retryTime should bigger than 0");        }        this.retryTime = retryTime;        return this;    }    /**     * 重试的业务执行代码     * 失败时请抛出一个异常     *     * todo 确定返回的封装类,根据返回结果的状态来判定是否需要重试     *     * @return     */    protected abstract Object doBiz() throws Exception;    public Object execute() throws InterruptedException {        for (int i = 0; i < retryTime; i++) {            try {                return doBiz();            } catch (Exception e) {                log.error("业务执行出现异常,e: {}", e);                Thread.sleep(sleepTime);            }        }        return null;    }    public Object submit(ExecutorService executorService) {        if (executorService == null) {            throw new IllegalArgumentException("please choose executorService!");        }        return executorService.submit((Callable) () -> execute());    }}

预留一个doBiz方法由业务方来实现,在其中书写需要重试的业务代码,然后执行即可

使用case也比较简单

public void retryDemo() throws InterruptedException {    Object ans = new RetryTemplate() {        @Override        protected Object doBiz() throws Exception {            int temp = (int) (Math.random() * 10);            System.out.println(temp);              if (temp > 3) {                throw new Exception("generate value bigger then 3! need retry");            }              return temp;        }    }.setRetryTime(10).setSleepTime(10).execute();    System.out.println(ans);}

优点:

  • 简单(依赖简单:引入一个类就可以了; 使用简单:实现抽象类,讲业务逻辑填充即可;)
  • 灵活(这个是真正的灵活了,你想怎么干都可以,完全由你控制)

缺点:

  • 强侵入
  • 代码臃肿

实现

上面的模板方式基本上就那样了,接下来谈到的实现,毫无疑问将是切面和消息总线的方式

1. 切面方式

实现依然是基于前面的模板方式做的,简单来看就是添加一个切面,内部实现模版类即可

注解定义如下

@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface RetryDot {    /**     * 重试次数     * @return     */    int count() default 0;    /**     * 重试的间隔时间     * @return     */    int sleep() default 0;    /**     * 是否支持异步重试方式     * @return     */    boolean asyn() default false;}

切面逻辑如下

@Aspect@Component@Slf4jpublic class RetryAspect {    ExecutorService executorService = new ThreadPoolExecutor(3, 5,            1, TimeUnit.MINUTES,            new LinkedBlockingQueue
()); @Around(value = "@annotation(retryDot)") public Object execute(ProceedingJoinPoint joinPoint, RetryDot retryDot) throws Exception { RetryTemplate retryTemplate = new RetryTemplate() { @Override protected Object doBiz() throws Throwable { return joinPoint.proceed(); } }; retryTemplate.setRetryCount(retryDot.count()) .setSleepTime(retryDot.sleep()); if (retryDot.asyn()) { return retryTemplate.submit(executorService); } else { return retryTemplate.execute(); } }}

2. 消息方式

依然是在EventBus的基础上进行开发,结果写到一半,发现这种方式局限性还蛮大,基本上不太适合实际使用,下面依然给出实现逻辑

定义的重试事件RetryEvent

@Datapublic class RetryEvent {    /**     * 重试间隔时间, ms为单位     */    private int sleep;    /**     * 重试次数     */    private int count;    /**     * 是否异步重试     */    private boolean asyn;    /**     * 回调方法     */    private Supplier callback;}

消息处理类

@Componentpublic class RetryProcess {    ExecutorService executorService = new ThreadPoolExecutor(3, 5,            1, TimeUnit.MINUTES,            new LinkedBlockingQueue
()); private static EventBus eventBus = new EventBus("retry"); public static void post(RetryEvent event) { eventBus.post(event); } public static void register(Object handler) { eventBus.register(handler); } public static void unregister(Object handler) { eventBus.unregister(handler); } @PostConstruct public void init() { register(this); } @Subscribe public void process(RetryEvent event) throws InterruptedException { RetryTemplate retryTemplate = new RetryTemplate() { @Override protected Object doBiz() throws Throwable { return event.getCallback().get(); } }; retryTemplate.setSleepTime(event.getSleep()) .setRetryCount(event.getCount()); if(event.isAsyn()) { retryTemplate.submit(executorService); } else { retryTemplate.execute(); } }}

问题比较明显,返回值以及输入参数的传入,比较不好处理

测试

测试下上面两种使用方式, 定义一个实例Service,分别采用注解和消息两种方式

@Servicepublic class RetryDemoService {    private int genNum() {        return (int) (Math.random() * 10);    }    @RetryDot(count = 5, sleep = 10)    public int genBigNum() throws Exception {        int a = genNum();        System.out.println("genBigNum " + a);        if (a < 3) {            throw new Exception("num less than 3");        }        return a;    }    public void genSmallNum() throws Exception {        RetryEvent retryEvent = new RetryEvent();        retryEvent.setSleep(10);        retryEvent.setCount(5);        retryEvent.setAsyn(false);        retryEvent.setCallback(() -> {            int a = genNum();            System.out.println("now num: " + a);            if (a > 3) {                throw new RuntimeException("num bigger than 3");            }            return a;        });        RetryProcess.post(retryEvent);    }}

因为使用了切面,在spring的基础上进行开发的,所以需要加上对应的配置信息 aop.xml

Test代码

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath:aop.xml"})public class AspectRetryTest {    @Autowired    private RetryDemoService retryDemoService;    @Test    public void testRetry() throws Exception {        for (int i = 0; i < 3; i++) {            int ans = retryDemoService.genBigNum();            System.out.println("----" + ans + "----");            retryDemoService.genSmallNum();            System.out.println("------------------");        }    }}

输出

genBigNum 9----9----now num: 1------------------genBigNum 9----9----now num: 4now num: 1------------------genBigNum 5----5----now num: 6now num: 6now num: 0------------------

其他

guava-retryingspring-retry 实际上是更好的选择,设计与实现都非常优雅,实际的项目中完全可以直接使用

相关代码:

个人博客:

公众号获取更多:

个人信息

参考

转载于:https://my.oschina.net/u/566591/blog/1526551

你可能感兴趣的文章
javascript中数据属性与访问器属性
查看>>
对.NET跨平台的随想
查看>>
CSS实现各种方向三角形
查看>>
python字符串内容替换的方法(转载)
查看>>
ng-view 路由 简单应用
查看>>
Nginx Rewrite规则初探(转)
查看>>
Spring学习笔记1——IOC: 尽量使用注解以及java代码(转)
查看>>
黑魔法NSURLProtocol 可拦截网络加载
查看>>
Webtop中新建文档,无法选择Type和Format
查看>>
Integration Services创建ETL包
查看>>
IE浏览器开发中遇到的问题
查看>>
【C#学习笔记】载入图片并居中
查看>>
php实现按utf8编码对字符串进行分割
查看>>
Ftp的断点下载实现
查看>>
[转载] ubuntu Authentication failure
查看>>
Ring0 - 链表
查看>>
修改数组之----splice
查看>>
a版本冲刺第五天
查看>>
Arduino示例教程超声波测距实验
查看>>
Redis操作hash
查看>>