Java實(shí)現(xiàn)幾種簡(jiǎn)單的重試機(jī)制

背景

當(dāng)業(yè)務(wù)執(zhí)行失敗之后,進(jìn)行重試是一個(gè)非常常見的場(chǎng)景,那么如何在業(yè)務(wù)代碼中優(yōu)雅的實(shí)現(xiàn)重試機(jī)制呢?

設(shè)計(jì)

我們的目標(biāo)是實(shí)現(xiàn)一個(gè)優(yōu)雅的重試機(jī)制,那么先來(lái)看下怎么樣才算是優(yōu)雅

  • 無(wú)侵入:這個(gè)好理解,不改動(dòng)當(dāng)前的業(yè)務(wù)邏輯,對(duì)于需要重試的地方,可以很簡(jiǎn)單的實(shí)現(xiàn)
  • 可配置:包括重試次數(shù),重試的間隔時(shí)間,是否使用異步方式等
  • 通用性:最好是無(wú)改動(dòng)(或者很小改動(dòng))的支持絕大部分的場(chǎng)景,拿過來(lái)直接可用

針對(duì)上面的幾點(diǎn),分別看下右什么好的解決方案

幾種解決思路

要想做到無(wú)侵入或者很小的改動(dòng),一般來(lái)將比較好的方式就是切面或者消息總線模式;可配置和通用性則比較清晰了,基本上開始做就表示這兩點(diǎn)都是基礎(chǔ)要求了,唯一的要求就是不要硬編碼,不要寫死,基本上就能達(dá)到這個(gè)基礎(chǔ)要求,當(dāng)然要優(yōu)秀的話,要做的事情并不少

切面方式

這個(gè)思路比較清晰,在需要添加重試的方法上添加一個(gè)用于重試的自定義注解,然后在切面中實(shí)現(xiàn)重試的邏輯,主要的配置參數(shù)則根據(jù)注解中的選項(xiàng)來(lái)初始化

優(yōu)點(diǎn):

  • 真正的無(wú)侵入

缺點(diǎn):

  • 某些方法無(wú)法被切面攔截的場(chǎng)景無(wú)法覆蓋(如spring-aop無(wú)法切私有方法,final方法)
  • 直接使用aspecj則有些小復(fù)雜;如果用spring-aop,則只能切被spring容器管理的bean

消息總線方式

這個(gè)也比較容易理解,在需要重試的方法中,發(fā)送一個(gè)消息,并將業(yè)務(wù)邏輯作為回調(diào)方法傳入;由一個(gè)訂閱了重試消息的consumer來(lái)執(zhí)行重試的業(yè)務(wù)邏輯

優(yōu)點(diǎn):

  • 重試機(jī)制不受任何限制,即在任何地方你都可以使用
  • 利用EventBus框架,可以非常容易把框架搭起來(lái)

缺點(diǎn):

  • 業(yè)務(wù)侵入,需要在重試的業(yè)務(wù)處,主動(dòng)發(fā)起一條重試消息
  • 調(diào)試?yán)斫鈴?fù)雜(消息總線方式的最大優(yōu)點(diǎn)和缺點(diǎn),就是過于靈活了,你可能都不知道什么地方處理這個(gè)消息,特別是新的童鞋來(lái)維護(hù)這段代碼時(shí))
  • 如果要獲取返回結(jié)果,不太好處理, 上下文參數(shù)不好處理

模板方式

把這個(gè)單獨(dú)撈出來(lái),主要是某些時(shí)候我就一兩個(gè)地方要用到重試,簡(jiǎn)單的實(shí)現(xiàn)下就好了,也沒有必用用到上面這么重的方式;而且我希望可以針對(duì)代碼快進(jìn)行重試

這個(gè)的設(shè)計(jì)還是非常簡(jiǎn)單的,基本上代碼都可以直接貼出來(lái),一目了然:

public abstract class RetryTemplate {

    private static final int DEFAULT_RETRY_TIME = 1;

    private int retryTime = DEFAULT_RETRY_TIME;

    // 重試的睡眠時(shí)間
    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;
    }

    /**
     * 重試的業(yè)務(wù)執(zhí)行代碼
     * 失敗時(shí)請(qǐng)拋出一個(gè)異常
     *
     * todo 確定返回的封裝類,根據(jù)返回結(jié)果的狀態(tài)來(lái)判定是否需要重試
     *
     * @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("業(yè)務(wù)執(zhí)行出現(xiàn)異常,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());
    }

}

預(yù)留一個(gè)doBiz方法由業(yè)務(wù)方來(lái)實(shí)現(xiàn),在其中書寫需要重試的業(yè)務(wù)代碼,然后執(zhí)行即可

使用case也比較簡(jiǎn)單

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);
}

優(yōu)點(diǎn):

  • 簡(jiǎn)單(依賴簡(jiǎn)單:引入一個(gè)類就可以了; 使用簡(jiǎn)單:實(shí)現(xiàn)抽象類,講業(yè)務(wù)邏輯填充即可;)
  • 靈活(這個(gè)是真正的靈活了,你想怎么干都可以,完全由你控制)

缺點(diǎn):

  • 強(qiáng)侵入
  • 代碼臃腫

實(shí)現(xiàn)

上面的模板方式基本上就那樣了,接下來(lái)談到的實(shí)現(xiàn),毫無(wú)疑問將是切面和消息總線的方式

1. 切面方式

實(shí)現(xiàn)依然是基于前面的模板方式做的,簡(jiǎn)單來(lái)看就是添加一個(gè)切面,內(nèi)部實(shí)現(xiàn)模版類即可

注解定義如下

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryDot {
    /**
     * 重試次數(shù)
     * @return
     */
    int count() default 0;


    /**
     * 重試的間隔時(shí)間
     * @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的基礎(chǔ)上進(jìn)行開發(fā),結(jié)果寫到一半,發(fā)現(xiàn)這種方式局限性還蠻大,基本上不太適合實(shí)際使用,下面依然給出實(shí)現(xiàn)邏輯

定義的重試事件RetryEvent

@Data
public class RetryEvent {

    /**
     * 重試間隔時(shí)間, ms為單位
     */
    private int sleep;


    /**
     * 重試次數(shù)
     */
    private int count;


    /**
     * 是否異步重試
     */
    private boolean asyn;


    /**
     * 回調(diào)方法
     */
    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();
        }
    }
}

問題比較明顯,返回值以及輸入?yún)?shù)的傳入,比較不好處理

測(cè)試

測(cè)試下上面兩種使用方式, 定義一個(gè)實(shí)例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);
    }
}

因?yàn)槭褂昧饲忻妫趕pring的基礎(chǔ)上進(jìn)行開發(fā)的,所以需要加上對(duì)應(yīng)的配置信息 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 實(shí)際上是更好的選擇,設(shè)計(jì)與實(shí)現(xiàn)都非常優(yōu)雅,實(shí)際的項(xiàng)目中完全可以直接使用

相關(guān)代碼:

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

參考

Retry重試機(jī)制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,533評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,872評(píng)論 25 709
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,261評(píng)論 6 342
  • 孩子不僅是父母血脈的延續(xù),更是父母承載希望的載體,每一個(gè)孩子的離去,都會(huì)讓其背后的家庭承受難以言述的傷痛。 失去父...
    風(fēng)想留步閱讀 5,157評(píng)論 7 10
  • day1 下定決心瘦身就好好堅(jiān)持下去,節(jié)食健身,一樣不落。 自己現(xiàn)在這么宅這么自卑這么不敢相信自己怕都是因?yàn)樽约号?..
    五里墩頭的瓦尼兔閱讀 369評(píng)論 1 2

友情鏈接更多精彩內(nèi)容