a simple retry schedule (一个简单的重试机制实现)
Switch branches/tags
Nothing to show
Clone or download
yihui
Latest commit b78399a Aug 30, 2017
Permalink
Failed to load latest commit information.
src 几种重试实现 Aug 30, 2017
pom.xml 几种重试实现 Aug 30, 2017
readme.md 几种重试实现 Aug 30, 2017

readme.md

背景

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

设计

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

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

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

几种解决思路

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

切面方式

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

优点:

  • 真正的无侵入

缺点:

  • 某些方法无法被切面拦截的场景无法覆盖(如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
@Slf4j
public class RetryAspect {

    ExecutorService executorService = new ThreadPoolExecutor(3, 5,
            1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());


    @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

@Data
public class RetryEvent {

    /**
     * 重试间隔时间, ms为单位
     */
    private int sleep;


    /**
     * 重试次数
     */
    private int count;


    /**
     * 是否异步重试
     */
    private boolean asyn;


    /**
     * 回调方法
     */
    private Supplier<Object> callback;
}

消息处理类

@Component
public class RetryProcess {

    ExecutorService executorService = new ThreadPoolExecutor(3, 5,
            1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());

    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,分别采用注解和消息两种方式

@Service
public 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

<context:component-scan base-package="com.hui.quickretry"/>

<context:annotation-config/>
<aop:aspectj-autoproxy/>

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: 4
now num: 1
------------------
genBigNum 5
----5----
now num: 6
now num: 6
now num: 0
------------------

其他

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

相关代码:

https://github.com/liuyueyi/quick-retry

参考

Retry重试机制