摘要:支持重試版本思考小明我手頭還有其他任務,這個也挺簡單的。與其他類似的字節(jié)碼編輯器不同,提供了兩個級別的源級和字節(jié)碼級。另一方面,字節(jié)碼級允許用戶直接編輯類文件作為其他編輯器。提供與其他字節(jié)碼框架類似的功能,但主要關注性能。
系列說明
java retry 的一步步實現(xiàn)機制。
java-retry 源碼地址情景導入 簡單的需求
產(chǎn)品經(jīng)理:實現(xiàn)一個按條件,查詢用戶信息的服務。
小明:好的。沒問題。
代碼UserService.java
public interface UserService { /** * 根據(jù)條件查詢用戶信息 * @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(); } }談話
項目經(jīng)理:這個服務有時候會失敗,你看下。
小明:OutService 在是一個 RPC 的外部服務,但是有時候不穩(wěn)定。
項目經(jīng)理:如果調用失敗了,你可以調用的時候重試幾次。你去看下重試相關的東西
重試 重試作用對于重試是有場景限制的,不是什么場景都適合重試,比如參數(shù)校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試。
遠程調用超時、網(wǎng)絡突然中斷可以重試。在微服務治理框架中,通常都有自己的重試與超時配置,比如dubbo可以設置retries=1,timeout=500調用失敗只重試1次,超過500ms調用仍未返回則調用失敗。
比如外部 RPC 調用,或者數(shù)據(jù)入庫等操作,如果一次操作失敗,可以進行多次重試,提高調用成功的可能性。
V1.0 支持重試版本 思考小明:我手頭還有其他任務,這個也挺簡單的。5 分鐘時間搞定他。
實現(xiàn)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 代理模式版本 易于維護
項目經(jīng)理:你的代碼我看了,功能雖然實現(xiàn)了,但是盡量寫的易于維護一點。
小明:好的。(心想,是說要寫點注釋什么的?)
代理模式為其他對象提供一種代理以控制對這個對象的訪問。
在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介作用。
其特征是代理與委托類有同樣的接口。
實現(xiàn)小明想到以前看過的代理模式,心想用這種方式,原來的代碼改動量較少,以后想改起來也方便些。
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 動態(tài)代理模式 方便拓展
項目經(jīng)理:小明啊,這里還有個方法也是同樣的問題。你也給加上重試吧。
小明:好的。
小明心想,我在寫一個代理,但是轉念冷靜了下來,如果還有個服務也要重試怎么辦呢?
RoleService.java
public interface RoleService { /** * 查詢 * @param user 用戶信息 * @return 是否擁有權限 */ boolean hasPrivilege(User user); }代碼實現(xiàn)
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 { // 當代理對象調用真實對象的方法時,其會自動的跳轉到代理對象關聯(lián)的handler對象的invoke方法來進行調用 return method.invoke(subject, args); } catch (Exception e) { times++; if (times >= RetryConstant.MAX_TIMES) { throw new RuntimeException(e); } } } return null; } /** * 獲取動態(tài)代理 * * @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 動態(tài)代理模式增強 對話
項目經(jīng)理:小明,你動態(tài)代理的方式是挺會偷懶的,可是我們有的類沒有接口。這個問題你要解決一下。
小明:好的。(誰?寫服務竟然不定義接口)
ResourceServiceImpl.java
public class ResourceServiceImpl { /** * 校驗資源信息 * @param user 入?yún)? * @return 是否校驗通過 */ public boolean checkResource(User user) { OutService outService = new AlwaysFailOutServiceImpl(); outService.remoteCall(); return true; } }字節(jié)碼技術
小明看了下網(wǎng)上的資料,解決的辦法還是有的。
CGLIB
CGLIB 是一個功能強大、高性能和高質量的代碼生成庫,用于擴展JAVA類并在運行時實現(xiàn)接口。
javassist
javassist (Java編程助手)使Java字節(jié)碼操作變得簡單。
它是Java中編輯字節(jié)碼的類庫;它允許Java程序在運行時定義新類,并在JVM加載類文件時修改類文件。
與其他類似的字節(jié)碼編輯器不同,Javassist提供了兩個級別的API:源級和字節(jié)碼級。
如果用戶使用源代碼級API,他們可以編輯類文件,而不需要了解Java字節(jié)碼的規(guī)范。
整個API只使用Java語言的詞匯表進行設計。您甚至可以以源文本的形式指定插入的字節(jié)碼;Javassist動態(tài)編譯它。
另一方面,字節(jié)碼級API允許用戶直接編輯類文件作為其他編輯器。
ASM
ASM 是一個通用的Java字節(jié)碼操作和分析框架。
它可以用來修改現(xiàn)有的類或動態(tài)地生成類,直接以二進制形式。
ASM提供了一些通用的字節(jié)碼轉換和分析算法,可以從這些算法中構建自定義復雜的轉換和代碼分析工具。
ASM提供與其他Java字節(jié)碼框架類似的功能,但主要關注性能。
因為它的設計和實現(xiàn)都盡可能地小和快,所以非常適合在動態(tài)系統(tǒng)中使用(當然也可以以靜態(tài)的方式使用,例如在編譯器中)。
實現(xiàn)小明看了下,就選擇使用 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); //通過字節(jié)碼技術創(chuàng)建目標對象類的子類實例作為代理 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 實現(xiàn) 對話
項目經(jīng)理:小明啊,最近我在想一個問題。不同的服務,重試的時候次數(shù)應該是不同的。因為服務對穩(wěn)定性的要求各不相同啊。
小明:好的。(心想,重試都搞了一周了,今天都周五了。)
下班之前,小明一直在想這個問題。剛好周末,花點時間寫個重試小工具吧。
設計思路技術支持
spring
java 注解
注解定義
注解可在方法上使用,定義需要重試的次數(shù)
注解解析
攔截指定需要重試的方法,解析對應的重試次數(shù),然后進行對應次數(shù)的重試。
實現(xiàn)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. 最大次數(shù)判斷 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++; // 超過最大重試次數(shù) 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 版本 對話
周一來到公司,項目經(jīng)理又和小明談了起來。
項目經(jīng)理:重試次數(shù)是滿足了,但是重試其實應該講究策略。比如調用外部,第一次失敗,可以等待 5S 在次調用,如果又失敗了,可以等待 10S 再調用。。。
小明:了解。
思考可是今天周一,還有其他很多事情要做。
小明在想,沒時間寫這個呀。看看網(wǎng)上有沒有現(xiàn)成的。
spring-retrySpring Retry 為 Spring 應用程序提供了聲明性重試支持。 它用于Spring批處理、Spring集成、Apache Hadoop(等等)的Spring。
在分布式系統(tǒng)中,為了保證數(shù)據(jù)分布式事務的強一致性,大家在調用RPC接口或者發(fā)送MQ時,針對可能會出現(xiàn)網(wǎng)絡抖動請求超時情況采取一下重試操作。 大家用的最多的重試方式就是MQ了,但是如果你的項目中沒有引入MQ,那就不方便了。
還有一種方式,是開發(fā)者自己編寫重試機制,但是大多不夠優(yōu)雅。
注解式使用RemoteService.java
重試條件:遇到 RuntimeException
重試次數(shù):3
重試策略:重試的時候等待 5S, 后面時間依次變?yōu)樵瓉淼?2 倍數(shù)。
熔斷機制:全部重試失敗,則調用 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 工具雖能優(yōu)雅實現(xiàn)重試,但是存在兩個不友好設計:
一個是重試實體限定為 Throwable 子類,說明重試針對的是可捕捉的功能異常為設計前提的,但是我們希望依賴某個數(shù)據(jù)對象實體作為重試實體,
但 sping-retry框架必須強制轉換為Throwable子類。
另一個就是重試根源的斷言對象使用的是 doWithRetry 的 Exception 異常實例,不符合正常內(nèi)部斷言的返回設計。
Spring Retry 提倡以注解的方式對方法進行重試,重試邏輯是同步執(zhí)行的,重試的“失敗”針對的是Throwable,
如果你要以返回值的某個狀態(tài)來判定是否需要重試,可能只能通過自己判斷返回值然后顯式拋出異常了。
@Recover 注解在使用時無法指定方法,如果一個類中多個重試方法,就會很麻煩。
注解介紹 @EnableRetry表示是否開始重試。
序號 | 屬性 | 類型 | 默認值 | 說明 |
---|---|---|---|---|
1 | proxyTargetClass | boolean | false | 指示是否要創(chuàng)建基于子類的(CGLIB)代理,而不是創(chuàng)建標準的基于Java接口的代理。 |
標注此注解的方法在發(fā)生異常時會進行重試
序號 | 屬性 | 類型 | 默認值 | 說明 |
---|---|---|---|---|
1 | interceptor | String | "" | 將 interceptor 的 bean 名稱應用到 retryable() |
2 | value | Class[] | {} | 可重試的異常類型。 |
3 | label | String | "" | 統(tǒng)計報告的唯一標簽。如果沒有提供,調用者可以選擇忽略它,或者提供默認值。 |
4 | maxAttempts | int | 3 | 嘗試的最大次數(shù)(包括第一次失敗),默認為3次。 |
5 | backoff | @Backoff | @Backoff() | 指定用于重試此操作的backoff屬性。默認為空 |
序號 | 屬性 | 類型 | 默認值 | 說明 | |
---|---|---|---|---|---|
1 | delay | long | 0 | 如果不設置則默認使用 1000 milliseconds | 重試等待 |
2 | maxDelay | long | 0 | 最大重試等待時間 | |
3 | multiplier | long | 0 | 用于計算下一個延遲延遲的乘數(shù)(大于0生效) | |
4 | random | boolean | false | 隨機重試等待時間 |
用于恢復處理程序的方法調用的注釋。一個合適的復蘇handler有一個類型為可投擲(或可投擲的子類型)的第一個參數(shù)
和返回與@Retryable方法相同的類型的值。
可拋出的第一個參數(shù)是可選的(但是沒有它的方法只會被調用)。
從失敗方法的參數(shù)列表按順序填充后續(xù)的參數(shù)。
注解式只是讓我們使用更加便捷,但是如果要更高的靈活性??梢允褂酶鞣N提供的方法。
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() { @Override public String doWithRetry(RetryContext arg0) { throw new NullPointerException(); } } , new RecoveryCallback () { @Override public String recover(RetryContext context) { return "recovery callback"; } } ); LOGGER.info("result: {}", result); } }
執(zhí)行日志
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 callbackspring-retry 結構 概覽
RetryCallback: 封裝你需要重試的業(yè)務邏輯(上文中的doSth)
RecoverCallback:封裝在多次重試都失敗后你需要執(zhí)行的業(yè)務邏輯(上文中的doSthWhenStillFail)
RetryContext: 重試語境下的上下文,可用于在多次Retry或者Retry 和Recover之間傳遞參數(shù)或狀態(tài)(在多次doSth或者doSth與doSthWhenStillFail之間傳遞參數(shù))
RetryOperations : 定義了“重試”的基本框架(模板),要求傳入RetryCallback,可選傳入RecoveryCallback;
RetryListener:典型的“監(jiān)聽者”,在重試的不同階段通知“監(jiān)聽者”(例如doSth,wait等階段時通知)
RetryPolicy : 重試的策略或條件,可以簡單的進行多次重試,可以是指定超時時間進行重試(上文中的someCondition)
BackOffPolicy: 重試的回退策略,在業(yè)務邏輯執(zhí)行發(fā)生異常時。如果需要重試,我們可能需要等一段時間(可能服務器過于繁忙,如果一直不間隔重試可能拖垮服務器),
當然這段時間可以是 0,也可以是固定的,可以是隨機的(參見tcp的擁塞控制算法中的回退策略)?;赝瞬呗栽谏衔闹畜w現(xiàn)為wait();
RetryTemplate: RetryOperations的具體實現(xiàn),組合了RetryListener[],BackOffPolicy,RetryPolicy。
重試策略NeverRetryPolicy:只允許調用RetryCallback一次,不允許重試
AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導致死循環(huán)
SimpleRetryPolicy:固定次數(shù)重試策略,默認重試最大次數(shù)為3次,RetryTemplate默認使用的策略
TimeoutRetryPolicy:超時時間重試策略,默認超時時間為1秒,在指定的超時時間內(nèi)允許重試
ExceptionClassifierRetryPolicy:設置不同異常的重試策略,類似組合重試策略,區(qū)別在于這里只區(qū)分不同異常的重試
CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設置3個參數(shù)openTimeout、resetTimeout和delegate
CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許重試即可以,
悲觀組合重試策略是指只要有一個策略不允許重試即可以,但不管哪種組合方式,組合中的每一個策略都會執(zhí)行
重試回退策略重試回退策略,指的是每次重試是立即重試還是等待一段時間后重試。
默認情況下是立即重試,如果需要配置等待一段時間后重試則需要指定回退策略BackoffRetryPolicy。
NoBackOffPolicy:無退避算法策略,每次重試時立即重試
FixedBackOffPolicy:固定時間的退避策略,需設置參數(shù)sleeper和backOffPeriod,sleeper指定等待策略,默認是Thread.sleep,即線程休眠,backOffPeriod指定休眠時間,默認1秒
UniformRandomBackOffPolicy:隨機時間退避策略,需設置sleeper、minBackOffPeriod和maxBackOffPeriod,該策略在[minBackOffPeriod,maxBackOffPeriod之間取一個隨機休眠時間,minBackOffPeriod默認500毫秒,maxBackOffPeriod默認1500毫秒
ExponentialBackOffPolicy:指數(shù)退避策略,需設置參數(shù)sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠時間,默認100毫秒,maxInterval指定最大休眠時間,默認30秒,multiplier指定乘數(shù),即下一次休眠時間為當前休眠時間*multiplier
ExponentialRandomBackOffPolicy:隨機指數(shù)退避策略,引入隨機乘數(shù)可以實現(xiàn)隨機乘數(shù)回退
guava-retrying 談話小華:我們系統(tǒng)也要用到重試
項目經(jīng)理:小明前段時間用了 spring-retry,分享下應該還不錯
小明:spring-retry 基本功能都有,但是必須是基于異常來進行控制。如果你要以返回值的某個狀態(tài)來判定是否需要重試,可能只能通過自己判斷返回值然后顯式拋出異常了。
小華:我們項目中想根據(jù)對象的屬性來進行重試。你可以看下 guava-retry,我很久以前用過,感覺還不錯。
小明:好的。
guava-retryingguava-retrying 模塊提供了一種通用方法, 可以使用Guava謂詞匹配增強的特定停止、重試和異常處理功能來重試任意Java代碼。
優(yōu)勢
guava retryer工具與spring-retry類似,都是通過定義重試者角色來包裝正常邏輯重試,但是Guava retryer有更優(yōu)的策略定義,在支持重試次數(shù)和重試頻度控制基礎上,能夠兼容支持多個異?;蛘咦远x實體對象的重試源定義,讓重試功能有更多的靈活性。
Guava Retryer也是線程安全的,入口調用邏輯采用的是 java.util.concurrent.Callable 的 call() 方法
代碼例子 入門案例遇到異常之后,重試 3 次停止
HelloDemo.java
public static void main(String[] args) { Callablecallable = new Callable () { @Override public Boolean call() throws Exception { // do something useful here LOGGER.info("call..."); throw new RuntimeException(); } }; Retryer retryer = RetryerBuilder. 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
重試次數(shù):3
重試策略:固定等待 3S
Retryerretryer = RetryerBuilder. 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 moreguava-retrying 簡介 RetryerBuilder
RetryerBuilder 是一個 factory 創(chuàng)建者,可以定制設置重試源且可以支持多個重試源,可以配置重試次數(shù)或重試超時時間,以及可以配置等待時間間隔,創(chuàng)建重試者 Retryer 實例。
RetryerBuilder 的重試源支持 Exception 異常對象和自定義斷言對象,通過retryIfException 和 retryIfResult 設置,同時支持多個且能兼容。
retryIfException
retryIfException,拋出 runtime 異常、checked 異常時都會重試,但是拋出 error 不會重試。
retryIfRuntimeException
retryIfRuntimeException 只會在拋 runtime 異常的時候才重試,checked 異常和error 都不重試。
retryIfExceptionOfType
retryIfExceptionOfType 允許我們只在發(fā)生特定異常的時候才重試,比如NullPointerException 和 IllegalStateException 都屬于 runtime 異常,也包括自定義的error。
如:
retryIfExceptionOfType(Error.class)// 只在拋出error重試
當然我們還可以在只有出現(xiàn)指定的異常的時候才重試,如:
.retryIfExceptionOfType(IllegalStateException.class) .retryIfExceptionOfType(NullPointerException.class)
或者通過Predicate實現(xiàn)
.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
當發(fā)生重試之后,假如我們需要做一些額外的處理動作,比如log一下異常,那么可以使用RetryListener。
每次重試之后,guava-retrying 會自動回調我們注冊的監(jiān)聽。
可以注冊多個RetryListener,會按照注冊順序依次調用。
.withRetryListener(new RetryListener { @Override public主要接口void onRetry(Attempt attempt) { logger.error("第【{}】次調用失敗" , attempt.getAttemptNumber()); } } )
序號 | 接口 | 描述 | 備注 |
---|---|---|---|
1 | Attempt | 一次執(zhí)行任務 | |
2 | AttemptTimeLimiter | 單次任務執(zhí)行時間限制 | 如果單次任務執(zhí)行超時,則終止執(zhí)行當前任務 |
3 | BlockStrategies | 任務阻塞策略 | 通俗的講就是當前任務執(zhí)行完,下次任務還沒開始這段時間做什么),默認策略為:BlockStrategies.THREAD_SLEEP_STRATEGY |
4 | RetryException | 重試異常 | |
5 | RetryListener | 自定義重試監(jiān)聽器 | 可以用于異步記錄錯誤日志 |
6 | StopStrategy | 停止重試策略 | |
7 | WaitStrategy | 等待時長策略 | (控制時間間隔),返回結果為下次執(zhí)行時長 |
8 | Attempt | 一次執(zhí)行任務 | |
9 | Attempt | 一次執(zhí)行任務 |
StopStrategy
提供三種:
StopAfterDelayStrategy
設定一個最長允許的執(zhí)行時間;比如設定最長執(zhí)行10s,無論任務執(zhí)行次數(shù),只要重試的時候超出了最長時間,則任務終止,并返回重試異常RetryException;
NeverStopStrategy
不停止,用于需要一直輪訓知道返回期望結果的情況;
StopAfterAttemptStrategy
設定最大重試次數(shù),如果超出最大重試次數(shù)則停止重試,并返回重試異常;
WaitStrategy
FixedWaitStrategy
固定等待時長策略;
RandomWaitStrategy
隨機等待時長策略(可以提供一個最小和最大時長,等待時長為其區(qū)間隨機值)
IncrementingWaitStrategy
遞增等待時長策略(提供一個初始值和步長,等待時間隨重試次數(shù)增加而增加)
ExponentialWaitStrategy
指數(shù)等待時長策略;
FibonacciWaitStrategy
Fibonacci 等待時長策略;
ExceptionWaitStrategy
異常時長等待策略;
CompositeWaitStrategy
復合時長等待策略;
總結 優(yōu)雅重試共性和原理正常和重試優(yōu)雅解耦,重試斷言條件實例或邏輯異常實例是兩者溝通的媒介。
約定重試間隔,差異性重試策略,設置重試超時時間,進一步保證重試有效性以及重試流程穩(wěn)定性。
都使用了命令設計模式,通過委托重試對象完成相應的邏輯操作,同時內(nèi)部封裝實現(xiàn)重試邏輯。
spring-retry 和 guava-retry 工具都是線程安全的重試,能夠支持并發(fā)業(yè)務場景的重試邏輯正確性。
優(yōu)雅重試適用場景功能邏輯中存在不穩(wěn)定依賴場景,需要使用重試獲取預期結果或者嘗試重新執(zhí)行邏輯不立即結束。比如遠程接口訪問,數(shù)據(jù)加載訪問,數(shù)據(jù)上傳校驗等等。
對于異常場景存在需要重試場景,同時希望把正常邏輯和重試邏輯解耦。
對于需要基于數(shù)據(jù)媒介交互,希望通過重試輪詢檢測執(zhí)行邏輯場景也可以考慮重試方案。
談話項目經(jīng)理:我覺得 guava-retry 挺好的,就是不夠方便。小明啊,你給封裝個基于注解的吧。
小明:……
更好的實現(xiàn)java 重試框架——sisyphus
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.hztianpu.com/yun/76667.html
摘要:特性支持過程式編程基于字節(jié)碼的代理重試基于注解的重試,允許自定義注解無縫接入接口與注解的統(tǒng)一解決與中的不足之處設計目的綜合了和的優(yōu)勢?;谧止?jié)碼實現(xiàn)的代理重試,可以不依賴。提供基于代碼模式字節(jié)碼增強實現(xiàn)的方式。 Sisyphus 支持過程式編程和注解編程的 java 重試框架。 特性 支持 fluent 過程式編程 基于字節(jié)碼的代理重試 基于注解的重試,允許自定義注解 無縫接入 sp...
摘要:包含一些狀態(tài)來決定是重試還是中止,但是這個狀態(tài)位于堆棧上,不需要將它存儲在全局的任何位置,因此我們將此稱為無狀態(tài)重試。將拋出原始異常,除非在有狀態(tài)的情況下,當沒有可用的恢復,在這種情況下,它將拋出。 spring-retry 該項目為Spring應用程序提供聲明式重試支持,它用于Spring Batch、Spring Integration、Apache Hadoop的Spring(以...
摘要:的類圖如下主要根據(jù)創(chuàng)建擴展了,創(chuàng)建攔截的,這里會設置攔截器,這是集成的核心,當發(fā)起請求調用的時候,會先經(jīng)過攔截器,然后才真正發(fā)起請求。和是配合使用的,最大重試次數(shù)是針對每一個的,如果設置,這樣觸發(fā)最大重試次數(shù)就是次。 上一篇文章我們分析了ribbon的核心原理,接下來我們來看看springcloud是如何集成ribbon的,不同的springcloud的組件(feign,zuul,Re...
摘要:和二級緩存影響狀態(tài)更新,縮短這兩個定時任務周期可減少滯后時間,例如配置更新周期更新周期服務提供者保證服務正常下線。服務提供者延遲下線。 引言 Eureka是Netflix開源的、用于實現(xiàn)服務注冊和發(fā)現(xiàn)的服務。Spring Cloud Eureka基于Eureka進行二次封裝,增加了更人性化的UI,使用更為方便。但是由于Eureka本身存在較多緩存,服務狀態(tài)更新滯后,最常見的狀況是:服務...
摘要:重試會增加的響應時間。提供了輔助方法來為包含遠程調用的函數(shù)式接口或表達式創(chuàng)建裝飾器。如果我們想創(chuàng)建一個裝飾器并在代碼庫的不同位置重用它,我們將使用。 在本文中,我們將從快速介紹 Resilience4j 開始,然后深入探討其 Retry 模塊。我們將了解何時、如何使用它,以及它提供的功能。在此過程中,我們還將學...
閱讀 4214·2021-10-08 10:04
閱讀 3172·2021-08-11 11:20
閱讀 2931·2021-07-25 21:37
閱讀 2761·2019-08-30 12:44
閱讀 2412·2019-08-30 11:12
閱讀 1385·2019-08-26 13:45
閱讀 2441·2019-08-26 11:53
閱讀 3138·2019-08-26 11:32