當業務執行失敗以後,進行重試是一個很是常見的場景,那麼如何在業務代碼中優雅的實現重試機制呢?html
咱們的目標是實現一個優雅的重試機制,那麼先來看下怎麼樣纔算是優雅java
針對上面的幾點,分別看下右什麼好的解決方案git
要想作到無侵入或者很小的改動,通常來將比較好的方式就是切面或者消息總線模式;可配置和通用性則比較清晰了,基本上開始作就表示這兩點都是基礎要求了,惟一的要求就是不要硬編碼,不要寫死,基本上就能達到這個基礎要求,固然要優秀的話,要作的事情並很多github
這個思路比較清晰,在須要添加劇試的方法上添加一個用於重試的自定義註解,而後在切面中實現重試的邏輯,主要的配置參數則根據註解中的選項來初始化spring
優勢:框架
缺點:less
這個也比較容易理解,在須要重試的方法中,發送一個消息,並將業務邏輯做爲回調方法傳入;由一個訂閱了重試消息的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); }
優勢:
缺點:
上面的模板方式基本上就那樣了,接下來談到的實現,毫無疑問將是切面和消息總線的方式
實現依然是基於前面的模板方式作的,簡單來看就是添加一個切面,內部實現模版類便可
註解定義以下
@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(); } } }
依然是在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-retrying
和 spring-retry
其實是更好的選擇,設計與實現都很是優雅,實際的項目中徹底能夠直接使用
相關代碼:
https://github.com/liuyueyi/quick-retry
我的博客:一灰的我的博客
公衆號獲取更多: