背景
當業(yè)務執(zhí)行失敗之后,進行重試是一個非常常見的場景,那么如何在業(yè)務代碼中優(yōu)雅的實現(xiàn)重試機制呢?
設計
我們的目標是實現(xiàn)一個優(yōu)雅的重試機制,那么先來看下怎么樣才算是優(yōu)雅
- 無侵入:這個好理解,不改動當前的業(yè)務邏輯,對于需要重試的地方,可以很簡單的實現(xiàn)
- 可配置:包括重試次數(shù),重試的間隔時間,是否使用異步方式等
- 通用性:最好是無改動(或者很小改動)的支持絕大部分的場景,拿過來直接可用
針對上面的幾點,分別看下右什么好的解決方案
幾種解決思路
要想做到無侵入或者很小的改動,一般來將比較好的方式就是切面或者消息總線模式;可配置和通用性則比較清晰了,基本上開始做就表示這兩點都是基礎要求了,唯一的要求就是不要硬編碼,不要寫死,基本上就能達到這個基礎要求,當然要優(yōu)秀的話,要做的事情并不少
切面方式
這個思路比較清晰,在需要添加重試的方法上添加一個用于重試的自定義注解,然后在切面中實現(xiàn)重試的邏輯,主要的配置參數(shù)則根據(jù)注解中的選項來初始化
優(yōu)點:
- 真正的無侵入
缺點:
- 某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
- 直接使用aspecj則有些小復雜;如果用spring-aop,則只能切被spring容器管理的bean
消息總線方式
這個也比較容易理解,在需要重試的方法中,發(fā)送一個消息,并將業(yè)務邏輯作為回調方法傳入;由一個訂閱了重試消息的consumer來執(zhí)行重試的業(yè)務邏輯
優(yōu)點:
- 重試機制不受任何限制,即在任何地方你都可以使用
- 利用
EventBus框架,可以非常容易把框架搭起來
缺點:
- 業(yè)務侵入,需要在重試的業(yè)務處,主動發(fā)起一條重試消息
- 調試理解復雜(消息總線方式的最大優(yōu)點和缺點,就是過于靈活了,你可能都不知道什么地方處理這個消息,特別是新的童鞋來維護這段代碼時)
- 如果要獲取返回結果,不太好處理, 上下文參數(shù)不好處理
模板方式
把這個單獨撈出來,主要是某些時候我就一兩個地方要用到重試,簡單的實現(xiàn)下就好了,也沒有必用用到上面這么重的方式;而且我希望可以針對代碼快進行重試
這個的設計還是非常簡單的,基本上代碼都可以直接貼出來,一目了然:
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;
}
/**
* 重試的業(yè)務執(zhí)行代碼
* 失敗時請拋出一個異常
*
* todo 確定返回的封裝類,根據(jù)返回結果的狀態(tà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è)務執(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());
}
}
預留一個doBiz方法由業(yè)務方來實現(xiàn),在其中書寫需要重試的業(yè)務代碼,然后執(zhí)行即可
使用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);
}
優(yōu)點:
- 簡單(依賴簡單:引入一個類就可以了; 使用簡單:實現(xiàn)抽象類,講業(yè)務邏輯填充即可;)
- 靈活(這個是真正的靈活了,你想怎么干都可以,完全由你控制)
缺點:
- 強侵入
- 代碼臃腫
實現(xiàn)
上面的模板方式基本上就那樣了,接下來談到的實現(xiàn),毫無疑問將是切面和消息總線的方式
1. 切面方式
實現(xiàn)依然是基于前面的模板方式做的,簡單來看就是添加一個切面,內(nèi)部實現(xiàn)模版類即可
注解定義如下
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryDot {
/**
* 重試次數(shù)
* @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的基礎上進行開發(fā),結果寫到一半,發(fā)現(xiàn)這種方式局限性還蠻大,基本上不太適合實際使用,下面依然給出實現(xiàn)邏輯
定義的重試事件RetryEvent
@Data
public class RetryEvent {
/**
* 重試間隔時間, ms為單位
*/
private int sleep;
/**
* 重試次數(shù)
*/
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();
}
}
}
問題比較明顯,返回值以及輸入?yún)?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);
}
}
因為使用了切面,在spring的基礎上進行開發(fā)的,所以需要加上對應的配置信息 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 實際上是更好的選擇,設計與實現(xiàn)都非常優(yōu)雅,實際的項目中完全可以直接使用
相關代碼:
https://github.com/liuyueyi/quick-retry