Java實現幾種簡單的重試機制

背景

當業務執行失敗以後,進行重試是一個很是常見的場景,那麼如何在業務代碼中優雅的實現重試機制呢?html

設計

咱們的目標是實現一個優雅的重試機制,那麼先來看下怎麼樣纔算是優雅java

  • 無侵入:這個好理解,不改動當前的業務邏輯,對於須要重試的地方,能夠很簡單的實現
  • 可配置:包括重試次數,重試的間隔時間,是否使用異步方式等
  • 通用性:最好是無改動(或者很小改動)的支持絕大部分的場景,拿過來直接可用

針對上面的幾點,分別看下右什麼好的解決方案git

幾種解決思路

要想作到無侵入或者很小的改動,通常來將比較好的方式就是切面或者消息總線模式;可配置和通用性則比較清晰了,基本上開始作就表示這兩點都是基礎要求了,惟一的要求就是不要硬編碼,不要寫死,基本上就能達到這個基礎要求,固然要優秀的話,要作的事情並很多github

切面方式

這個思路比較清晰,在須要添加劇試的方法上添加一個用於重試的自定義註解,而後在切面中實現重試的邏輯,主要的配置參數則根據註解中的選項來初始化spring

優勢:框架

  • 真正的無侵入

缺點:less

  • 某些方法沒法被切面攔截的場景沒法覆蓋(如spring-aop沒法切私有方法,final方法)
  • 直接使用aspecj則有些小複雜;若是用spring-aop,則只能切被spring容器管理的bean

消息總線方式

這個也比較容易理解,在須要重試的方法中,發送一個消息,並將業務邏輯做爲回調方法傳入;由一個訂閱了重試消息的consumer來執行重試的業務邏輯dom

優勢:異步

  • 重試機制不受任何限制,即在任何地方你均可以使用
  • 利用EventBus框架,能夠很是容易把框架搭起來

缺點:ide

  • 業務侵入,須要在重試的業務處,主動發起一條重試消息
  • 調試理解複雜(消息總線方式的最大優勢和缺點,就是過於靈活了,你可能都不知道什麼地方處理這個消息,特別是新的童鞋來維護這段代碼時)
  • 若是要獲取返回結果,不太好處理, 上下文參數很差處理

模板方式

把這個單獨撈出來,主要是某些時候我就一兩個地方要用到重試,簡單的實現下就行了,也沒有必用用到上面這麼重的方式;並且我但願能夠針對代碼快進行重試

這個的設計仍是很是簡單的,基本上代碼均可以直接貼出來,一目瞭然:

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

我的博客:一灰的我的博客

公衆號獲取更多:

我的信息

參考

相關文章
相關標籤/搜索