系列說明
java retry 的一步步實現機制。
情景匯入
簡單的需求
產品經理:實現一個按條件,查詢使用者資訊的服務。
小明:好的。沒問題。
程式碼
- UserService.java
public interface UserService {
/**
* 根據條件查詢使用者資訊
* @param condition 條件
* @return User 資訊
*/
User queryUser(QueryUserCondition condition);
}
- UserServiceImpl.java
public class UserServiceImpl implements UserService {
private OutService outService;
public UserServiceImpl(OutService outService) {
this.outService = outService;
}
@Override
public User queryUser(QueryUserCondition condition) {
outService.remoteCall();
return new User();
}
}
談話
專案經理:這個服務有時候會失敗,你看下。
小明:OutService
在是一個 RPC 的外部服務,但是有時候不穩定。
專案經理:如果呼叫失敗了,你可以呼叫的時候重試幾次。你去看下重試相關的東西
重試
重試作用
對於重試是有場景限制的,不是什麼場景都適合重試,比如引數校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試。
遠端呼叫超時、網路突然中斷可以重試。在微服務治理框架中,通常都有自己的重試與超時配置,比如dubbo可以設定retries=1,timeout=500呼叫失敗只重試1次,超過500ms呼叫仍未返回則呼叫失敗。
比如外部 RPC 呼叫,或者資料入庫等操作,如果一次操作失敗,可以進行多次重試,提高呼叫成功的可能性。
V1.0 支援重試版本
思考
小明:我手頭還有其他任務,這個也挺簡單的。5 分鐘時間搞定他。
實現
- UserServiceRetryImpl.java
public class UserServiceRetryImpl implements UserService {
@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
OutService outService = new AlwaysFailOutServiceImpl();
while (times < RetryConstant.MAX_TIMES) {
try {
outService.remoteCall();
return new User();
} catch (Exception e) {
times++;
if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
}
V1.1 代理模式版本
易於維護
專案經理:你的程式碼我看了,功能雖然實現了,但是儘量寫的易於維護一點。
小明:好的。(心想,是說要寫點註釋什麼的?)
代理模式
為其他物件提供一種代理以控制對這個物件的訪問。
在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介作用。
其特徵是代理與委託類有同樣的介面。
實現
小明想到以前看過的代理模式,心想用這種方式,原來的程式碼改動量較少,以後想改起來也方便些。
- UserServiceProxyImpl.java
public class UserServiceProxyImpl implements UserService {
private UserService userService = new UserServiceImpl();
@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
return userService.queryUser(condition);
} catch (Exception e) {
times++;
if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
}
V1.2 動態代理模式
方便擴充
專案經理:小明啊,這裡還有個方法也是同樣的問題。你也給加上重試吧。
小明:好的。
小明心想,我在寫一個代理,但是轉念冷靜了下來,如果還有個服務也要重試怎麼辦呢?
- RoleService.java
public interface RoleService {
/**
* 查詢
* @param user 使用者資訊
* @return 是否擁有許可權
*/
boolean hasPrivilege(User user);
}
程式碼實現
- DynamicProxy.java
public class DynamicProxy implements InvocationHandler {
private final Object subject;
public DynamicProxy(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
// 當代理物件呼叫真實物件的方法時,其會自動的跳轉到代理物件關聯的handler物件的invoke方法來進行呼叫
return method.invoke(subject, args);
} catch (Exception e) {
times++;
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
/**
* 獲取動態代理
*
* @param realSubject 代理物件
*/
public static Object getProxy(Object realSubject) {
// 我們要代理哪個真實物件,就將該物件傳進去,最後是通過該真實物件來呼叫其方法的
InvocationHandler handler = new DynamicProxy(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}
- 測試程式碼
@Test
public void failUserServiceTest() {
UserService realService = new UserServiceImpl();
UserService proxyService = (UserService) DynamicProxy.getProxy(realService);
User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}
@Test
public void roleServiceTest() {
RoleService realService = new RoleServiceImpl();
RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);
boolean hasPrivilege = proxyService.hasPrivilege(new User());
LOGGER.info("roleServiceTest: " + hasPrivilege);
}
V1.3 動態代理模式增強
對話
專案經理:小明,你動態代理的方式是挺會偷懶的,可是我們有的類沒有介面。這個問題你要解決一下。
小明:好的。(誰?寫服務竟然不定義介面)
- ResourceServiceImpl.java
public class ResourceServiceImpl {
/**
* 校驗資源資訊
* @param user 入參
* @return 是否校驗通過
*/
public boolean checkResource(User user) {
OutService outService = new AlwaysFailOutServiceImpl();
outService.remoteCall();
return true;
}
}
位元組碼技術
小明看了下網上的資料,解決的辦法還是有的。
- CGLIB
CGLIB 是一個功能強大、高效能和高質量的程式碼生成庫,用於擴充套件JAVA類並在執行時實現介面。
- javassist
javassist (Java程式設計助手)使Java位元組碼操作變得簡單。
它是Java中編輯位元組碼的類庫;它允許Java程式在執行時定義新類,並在JVM載入類檔案時修改類檔案。
與其他類似的位元組碼編輯器不同,Javassist提供了兩個級別的API:源級和位元組碼級。
如果使用者使用原始碼級API,他們可以編輯類檔案,而不需要了解Java位元組碼的規範。
整個API只使用Java語言的詞彙表進行設計。您甚至可以以源文字的形式指定插入的位元組碼;Javassist動態編譯它。
另一方面,位元組碼級API允許使用者直接編輯類檔案作為其他編輯器。
- ASM
ASM 是一個通用的Java位元組碼操作和分析框架。
它可以用來修改現有的類或動態地生成類,直接以二進位制形式。
ASM提供了一些通用的位元組碼轉換和分析演算法,可以從這些演算法中構建自定義複雜的轉換和程式碼分析工具。
ASM提供與其他Java位元組碼框架類似的功能,但主要關注效能。
因為它的設計和實現都儘可能地小和快,所以非常適合在動態系統中使用(當然也可以以靜態的方式使用,例如在編譯器中)。
實現
小明看了下,就選擇使用 CGLIB。
- CglibProxy.java
public class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
//通過代理子類呼叫父類的方法
return methodProxy.invokeSuper(o, objects);
} catch (Exception e) {
times++;
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
/**
* 獲取代理類
* @param clazz 類資訊
* @return 代理類結果
*/
public Object getProxy(Class clazz){
Enhancer enhancer = new Enhancer();
//目標物件類
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通過位元組碼技術建立目標物件類的子類例項作為代理
return enhancer.create();
}
}
- 測試
@Test
public void failUserServiceTest() {
UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);
User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}
@Test
public void resourceServiceTest() {
ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
boolean result = proxyService.checkResource(new User());
LOGGER.info("resourceServiceTest: " + result);
}
V2.0 AOP 實現
對話
專案經理:小明啊,最近我在想一個問題。不同的服務,重試的時候次數應該是不同的。因為服務對穩定性的要求各不相同啊。
小明:好的。(心想,重試都搞了一週了,今天都週五了。)
下班之前,小明一直在想這個問題。剛好週末,花點時間寫個重試小工具吧。
設計思路
- 技術支援
spring
java 註解
- 註解定義
註解可在方法上使用,定義需要重試的次數
- 註解解析
攔截指定需要重試的方法,解析對應的重試次數,然後進行對應次數的重試。
實現
- Retryable.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
/**
* Exception type that are retryable.
* @return exception type to retry
*/
Class<? extends Throwable> value() default RuntimeException.class;
/**
* 包含第一次失敗
* @return the maximum number of attempts (including the first failure), defaults to 3
*/
int maxAttempts() default 3;
}
- RetryAspect.java
@Aspect
@Component
public class RetryAspect {
@Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
"@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
public void myPointcut() {
}
@Around("myPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Method method = getCurrentMethod(point);
Retryable retryable = method.getAnnotation(Retryable.class);
//1. 最大次數判斷
int maxAttempts = retryable.maxAttempts();
if (maxAttempts <= 1) {
return point.proceed();
}
//2. 異常處理
int times = 0;
final Class<? extends Throwable> exceptionClass = retryable.value();
while (times < maxAttempts) {
try {
return point.proceed();
} catch (Throwable e) {
times++;
// 超過最大重試次數 or 不屬於當前處理異常
if (times >= maxAttempts ||
!e.getClass().isAssignableFrom(exceptionClass)) {
throw new Throwable(e);
}
}
}
return null;
}
private Method getCurrentMethod(ProceedingJoinPoint point) {
try {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
方法的使用
- fiveTimes()
當前方法一共重試 5 次。
重試條件:服務丟擲 AopRuntimeExption
@Override
@Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
public void fiveTimes() {
LOGGER.info("fiveTimes called!");
throw new AopRuntimeExption();
}
- 測試日誌
2018-08-08 15:49:33.814 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
java.lang.reflect.UndeclaredThrowableException
...
V3.0 spring-retry 版本
對話
週一來到公司,專案經理又和小明談了起來。
專案經理:重試次數是滿足了,但是重試其實應該講究策略。比如呼叫外部,第一次失敗,可以等待 5S 在次呼叫,如果又失敗了,可以等待 10S 再呼叫。。。
小明:瞭解。
思考
可是今天週一,還有其他很多事情要做。
小明在想,沒時間寫這個呀。看看網上有沒有現成的。
spring-retry
Spring Retry 為 Spring 應用程式提供了宣告性重試支援。 它用於Spring批處理、Spring整合、Apache Hadoop(等等)的Spring。
在分散式系統中,為了保證資料分散式事務的強一致性,大家在呼叫RPC介面或者傳送MQ時,針對可能會出現網路抖動請求超時情況採取一下重試操作。 大家用的最多的重試方式就是MQ了,但是如果你的專案中沒有引入MQ,那就不方便了。
還有一種方式,是開發者自己編寫重試機制,但是大多不夠優雅。
註解式使用
- RemoteService.java
重試條件:遇到 RuntimeException
重試次數:3
重試策略:重試的時候等待 5S, 後面時間依次變為原來的 2 倍數。
熔斷機制:全部重試失敗,則呼叫 recover()
方法。
@Service
public class RemoteService {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);
/**
* 呼叫方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC呼叫異常");
}
/**
* recover 機制
* @param e 異常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
}
}
- 測試
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {
@Autowired
private RemoteService remoteService;
@Test
public void test() {
remoteService.call();
}
}
- 日誌
2018-08-08 16:03:26.409 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:31.414 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.416 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.418 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things....
2018-08-08 16:03:41.425 WARN 1433 --- [ main] c.g.h.r.spring.service.RemoteService : We meet ex:
java.lang.RuntimeException: RPC呼叫異常
at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...
三次呼叫的時間點:
2018-08-08 16:03:26.409
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416
缺陷
spring-retry 工具雖能優雅實現重試,但是存在兩個不友好設計:
一個是重試實體限定為 Throwable
子類,說明重試針對的是可捕捉的功能異常為設計前提的,但是我們希望依賴某個資料物件實體作為重試實體,
但 sping-retry框架必須強制轉換為Throwable子類。
另一個就是重試根源的斷言物件使用的是 doWithRetry 的 Exception 異常例項,不符合正常內部斷言的返回設計。
Spring Retry 提倡以註解的方式對方法進行重試,重試邏輯是同步執行的,重試的“失敗”針對的是Throwable,
如果你要以返回值的某個狀態來判定是否需要重試,可能只能通過自己判斷返回值然後顯式丟擲異常了。
@Recover
註解在使用時無法指定方法,如果一個類中多個重試方法,就會很麻煩。
註解介紹
@EnableRetry
表示是否開始重試。
序號 | 屬性 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
1 | proxyTargetClass | boolean | false | 指示是否要建立基於子類的(CGLIB)代理,而不是建立標準的基於Java介面的代理。 |
@Retryable
標註此註解的方法在發生異常時會進行重試
序號 | 屬性 | 型別 | 預設值 | 說明 |
---|---|---|---|---|
1 | interceptor | String | “” | 將 interceptor 的 bean 名稱應用到 retryable() |
2 | value | Class[] | {} | 可重試的異常型別。 |
3 | label | String | “” | 統計報告的唯一標籤。如果沒有提供,呼叫者可以選擇忽略它,或者提供預設值。 |
4 | maxAttempts | int | 3 | 嘗試的最大次數(包括第一次失敗),預設為3次。 |
5 | backoff | @Backoff | @Backoff() | 指定用於重試此操作的backoff屬性。預設為空 |
@Backoff
序號 | 屬性 | 型別 | 預設值 | 說明 | |
---|---|---|---|---|---|
1 | delay | long | 0 | 如果不設定則預設使用 1000 milliseconds | 重試等待 |
2 | maxDelay | long | 0 | 最大重試等待時間 | |
3 | multiplier | long | 0 | 用於計算下一個延遲延遲的乘數(大於0生效) | |
4 | random | boolean | false | 隨機重試等待時間 |
@Recover
用於恢復處理程式的方法呼叫的註釋。一個合適的復甦handler有一個型別為可投擲(或可投擲的子型別)的第一個引數
和返回與@Retryable
方法相同的型別的值。
可丟擲的第一個引數是可選的(但是沒有它的方法只會被呼叫)。
從失敗方法的引數列表按順序填充後續的引數。
方法式使用
註解式只是讓我們使用更加便捷,但是如果要更高的靈活性。可以使用各種提供的方法。
- SimpleDemo.java
public class SimpleDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) throws Exception {
RetryTemplate template = new RetryTemplate();
// 策略
SimpleRetryPolicy policy = new SimpleRetryPolicy();
policy.setMaxAttempts(2);
template.setRetryPolicy(policy);
String result = template.execute(
new RetryCallback<String, Exception>() {
@Override
public String doWithRetry(RetryContext arg0) {
throw new NullPointerException();
}
}
,
new RecoveryCallback<String>() {
@Override
public String recover(RetryContext context) {
return "recovery callback";
}
}
);
LOGGER.info("result: {}", result);
}
}
- 執行日誌
16:30:52.578 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=2
16:30:52.592 [main] INFO com.github.houbb.retry.spring.commonway.SimpleDemo - result: recovery callback
spring-retry 結構
概覽
- RetryCallback: 封裝你需要重試的業務邏輯(上文中的doSth)
- RecoverCallback:封裝在多次重試都失敗後你需要執行的業務邏輯(上文中的doSthWhenStillFail)
- RetryContext: 重試語境下的上下文,可用於在多次Retry或者Retry 和Recover之間傳遞引數或狀態(在多次doSth或者doSth與doSthWhenStillFail之間傳遞引數)
- RetryOperations : 定義了“重試”的基本框架(模板),要求傳入RetryCallback,可選傳入RecoveryCallback;
- RetryListener:典型的“監聽者”,在重試的不同階段通知“監聽者”(例如doSth,wait等階段時通知)
- RetryPolicy : 重試的策略或條件,可以簡單的進行多次重試,可以是指定超時時間進行重試(上文中的someCondition)
- BackOffPolicy: 重試的回退策略,在業務邏輯執行發生異常時。如果需要重試,我們可能需要等一段時間(可能伺服器過於繁忙,如果一直不間隔重試可能拖垮伺服器),
當然這段時間可以是 0,也可以是固定的,可以是隨機的(參見tcp的擁塞控制演算法中的回退策略)。回退策略在上文中體現為wait();
- RetryTemplate: RetryOperations的具體實現,組合了RetryListener[],BackOffPolicy,RetryPolicy。
重試策略
- NeverRetryPolicy:只允許呼叫RetryCallback一次,不允許重試
- AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死迴圈
- SimpleRetryPolicy:固定次數重試策略,預設重試最大次數為3次,RetryTemplate預設使用的策略
- TimeoutRetryPolicy:超時時間重試策略,預設超時時間為1秒,在指定的超時時間內允許重試
- ExceptionClassifierRetryPolicy:設定不同異常的重試策略,類似組合重試策略,區別在於這裡只區分不同異常的重試
- CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設定3個引數openTimeout、resetTimeout和delegate
- CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許重試即可以,
悲觀組合重試策略是指只要有一個策略不允許重試即可以,但不管哪種組合方式,組合中的每一個策略都會執行
重試回退策略
重試回退策略,指的是每次重試是立即重試還是等待一段時間後重試。
預設情況下是立即重試,如果需要配置等待一段時間後重試則需要指定回退策略BackoffRetryPolicy。
- NoBackOffPolicy:無退避演算法策略,每次重試時立即重試
- FixedBackOffPolicy:固定時間的退避策略,需設定引數sleeper和backOffPeriod,sleeper指定等待策略,預設是Thread.sleep,即執行緒休眠,backOffPeriod指定休眠時間,預設1秒
- UniformRandomBackOffPolicy:隨機時間退避策略,需設定sleeper、minBackOffPeriod和maxBackOffPeriod,該策略在[minBackOffPeriod,maxBackOffPeriod之間取一個隨機休眠時間,minBackOffPeriod預設500毫秒,maxBackOffPeriod預設1500毫秒
- ExponentialBackOffPolicy:指數退避策略,需設定引數sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠時間,預設100毫秒,maxInterval指定最大休眠時間,預設30秒,multiplier指定乘數,即下一次休眠時間為當前休眠時間*multiplier
- ExponentialRandomBackOffPolicy:隨機指數退避策略,引入隨機乘數可以實現隨機乘數回退
guava-retrying
談話
小華:我們系統也要用到重試
專案經理:小明前段時間用了 spring-retry,分享下應該還不錯
小明:spring-retry 基本功能都有,但是必須是基於異常來進行控制。如果你要以返回值的某個狀態來判定是否需要重試,可能只能通過自己判斷返回值然後顯式丟擲異常了。
小華:我們專案中想根據物件的屬性來進行重試。你可以看下 guava-retry,我很久以前用過,感覺還不錯。
小明:好的。
guava-retrying
guava-retrying 模組提供了一種通用方法, 可以使用Guava謂詞匹配增強的特定停止、重試和異常處理功能來重試任意Java程式碼。
- 優勢
guava retryer工具與spring-retry類似,都是通過定義重試者角色來包裝正常邏輯重試,但是Guava retryer有更優的策略定義,在支援重試次數和重試頻度控制基礎上,能夠相容支援多個異常或者自定義實體物件的重試源定義,讓重試功能有更多的靈活性。
Guava Retryer也是執行緒安全的,入口呼叫邏輯採用的是 java.util.concurrent.Callable
的 call()
方法
程式碼例子
入門案例
遇到異常之後,重試 3 次停止
- HelloDemo.java
public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}
- 日誌
2018-08-08 17:21:12.442 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more
重試策略
- ExponentialBackoff.java
重試次數:3
重試策略:固定等待 3S
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
- 日誌
2018-08-08 17:20:41.653 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
2018-08-08 17:20:44.659 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
2018-08-08 17:20:47.664 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.ExponentialBackoff.main(ExponentialBackoff.java:56)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:44)
at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:39)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more
guava-retrying 簡介
RetryerBuilder
RetryerBuilder 是一個 factory 建立者,可以定製設定重試源且可以支援多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔,建立重試者 Retryer 例項。
RetryerBuilder 的重試源支援 Exception 異常物件和自定義斷言物件,通過retryIfException 和 retryIfResult 設定,同時支援多個且能相容。
- retryIfException
retryIfException,丟擲 runtime 異常、checked 異常時都會重試,但是丟擲 error 不會重試。
- retryIfRuntimeException
retryIfRuntimeException 只會在拋 runtime 異常的時候才重試,checked 異常和error 都不重試。
- retryIfExceptionOfType
retryIfExceptionOfType 允許我們只在發生特定異常的時候才重試,比如NullPointerException 和 IllegalStateException 都屬於 runtime 異常,也包括自定義的error。
如:
retryIfExceptionOfType(Error.class)// 只在丟擲error重試
當然我們還可以在只有出現指定的異常的時候才重試,如:
.retryIfExceptionOfType(IllegalStateException.class)
.retryIfExceptionOfType(NullPointerException.class)
或者通過Predicate實現
.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),
Predicates.instanceOf(IllegalStateException.class)))
- retryIfResult
retryIfResult 可以指定你的 Callable 方法在返回值的時候進行重試,如
// 返回false重試
.retryIfResult(Predicates.equalTo(false))
//以_error結尾才重試
.retryIfResult(Predicates.containsPattern("_error$"))
- RetryListener
當發生重試之後,假如我們需要做一些額外的處理動作,比如log一下異常,那麼可以使用RetryListener。
每次重試之後,guava-retrying 會自動回撥我們註冊的監聽。
可以註冊多個RetryListener,會按照註冊順序依次呼叫。
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次呼叫失敗" , attempt.getAttemptNumber());
}
}
)
主要介面
序號 | 介面 | 描述 | 備註 |
---|---|---|---|
1 | Attempt | 一次執行任務 | |
2 | AttemptTimeLimiter | 單次任務執行時間限制 | 如果單次任務執行超時,則終止執行當前任務 |
3 | BlockStrategies | 任務阻塞策略 | 通俗的講就是當前任務執行完,下次任務還沒開始這段時間做什麼),預設策略為:BlockStrategies.THREAD_SLEEP_STRATEGY
|
4 | RetryException | 重試異常 | |
5 | RetryListener | 自定義重試監聽器 | 可以用於非同步記錄錯誤日誌 |
6 | StopStrategy | 停止重試策略 | |
7 | WaitStrategy | 等待時長策略 | (控制時間間隔),返回結果為下次執行時長 |
8 | Attempt | 一次執行任務 | |
9 | Attempt | 一次執行任務 |
StopStrategy
提供三種:
- StopAfterDelayStrategy
設定一個最長允許的執行時間;比如設定最長執行10s,無論任務執行次數,只要重試的時候超出了最長時間,則任務終止,並返回重試異常RetryException;
- NeverStopStrategy
不停止,用於需要一直輪訓知道返回期望結果的情況;
- StopAfterAttemptStrategy
設定最大重試次數,如果超出最大重試次數則停止重試,並返回重試異常;
WaitStrategy
- FixedWaitStrategy
固定等待時長策略;
- RandomWaitStrategy
隨機等待時長策略(可以提供一個最小和最大時長,等待時長為其區間隨機值)
- IncrementingWaitStrategy
遞增等待時長策略(提供一個初始值和步長,等待時間隨重試次數增加而增加)
- ExponentialWaitStrategy
指數等待時長策略;
- FibonacciWaitStrategy
Fibonacci 等待時長策略;
- ExceptionWaitStrategy
異常時長等待策略;
- CompositeWaitStrategy
複合時長等待策略;
總結
優雅重試共性和原理
正常和重試優雅解耦,重試斷言條件例項或邏輯異常例項是兩者溝通的媒介。
約定重試間隔,差異性重試策略,設定重試超時時間,進一步保證重試有效性以及重試流程穩定性。
都使用了命令設計模式,通過委託重試物件完成相應的邏輯操作,同時內部封裝實現重試邏輯。
spring-retry 和 guava-retry 工具都是執行緒安全的重試,能夠支援併發業務場景的重試邏輯正確性。
優雅重試適用場景
功能邏輯中存在不穩定依賴場景,需要使用重試獲取預期結果或者嘗試重新執行邏輯不立即結束。比如遠端介面訪問,資料載入訪問,資料上傳校驗等等。
對於異常場景存在需要重試場景,同時希望把正常邏輯和重試邏輯解耦。
對於需要基於資料媒介互動,希望通過重試輪詢檢測執行邏輯場景也可以考慮重試方案。
談話
專案經理:我覺得 guava-retry 挺好的,就是不夠方便。小明啊,你給封裝個基於註解的吧。
小明:……