java retry 的一步步实现机制。java
产品经理:实现一个按条件,查询用户信息的服务。github
小明:好的。没问题。web
public interface UserService {
/** * 根据条件查询用户信息 * @param condition 条件 * @return User 信息 */
User queryUser(QueryUserCondition condition);
}
复制代码
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 的外部服务,可是有时候不稳定。spring
项目经理:若是调用失败了,你能够调用的时候重试几回。你去看下重试相关的东西编程
对于重试是有场景限制的,不是什么场景都适合重试,好比参数校验不合法、写操做等(要考虑写是否幂等)都不适合重试。设计模式
远程调用超时、网络忽然中断能够重试。在微服务治理框架中,一般都有本身的重试与超时配置,好比dubbo能够设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。安全
好比外部 RPC 调用,或者数据入库等操做,若是一次操做失败,能够进行屡次重试,提升调用成功的可能性。bash
小明:我手头还有其余任务,这个也挺简单的。5 分钟时间搞定他。
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;
}
}
复制代码
项目经理:你的代码我看了,功能虽然实现了,可是尽可能写的易于维护一点。
小明:好的。(心想,是说要写点注释什么的?)
为其余对象提供一种代理以控制对这个对象的访问。
在某些状况下,一个对象不适合或者不能直接引用另外一个对象,而代理对象能够在客户端和目标对象之间起到中介做用。
其特征是代理与委托类有一样的接口。
小明想到之前看过的代理模式,心想用这种方式,原来的代码改动量较少,之后想改起来也方便些。
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;
}
}
复制代码
项目经理:小明啊,这里还有个方法也是一样的问题。你也给加上重试吧。
小明:好的。
小明心想,我在写一个代理,可是转念冷静了下来,若是还有个服务也要重试怎么办呢?
public interface RoleService {
/** * 查询 * @param user 用户信息 * @return 是否拥有权限 */
boolean hasPrivilege(User user);
}
复制代码
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);
}
复制代码
项目经理:小明,你动态代理的方式是挺会偷懒的,但是咱们有的类没有接口。这个问题你要解决一下。
小明:好的。(谁?写服务居然不定义接口)
public class ResourceServiceImpl {
/** * 校验资源信息 * @param user 入参 * @return 是否校验经过 */
public boolean checkResource(User user) {
OutService outService = new AlwaysFailOutServiceImpl();
outService.remoteCall();
return true;
}
}
复制代码
小明看了下网上的资料,解决的办法仍是有的。
CGLIB 是一个功能强大、高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
javassist (Java编程助手)使Java字节码操做变得简单。
它是Java中编辑字节码的类库;它容许Java程序在运行时定义新类,并在JVM加载类文件时修改类文件。
与其余相似的字节码编辑器不一样,Javassist提供了两个级别的API:源级和字节码级。
若是用户使用源代码级API,他们能够编辑类文件,而不须要了解Java字节码的规范。
整个API只使用Java语言的词汇表进行设计。您甚至能够以源文本的形式指定插入的字节码;Javassist动态编译它。
另外一方面,字节码级API容许用户直接编辑类文件做为其余编辑器。
ASM 是一个通用的Java字节码操做和分析框架。
它能够用来修改现有的类或动态地生成类,直接以二进制形式。
ASM提供了一些通用的字节码转换和分析算法,能够从这些算法中构建自定义复杂的转换和代码分析工具。
ASM提供与其余Java字节码框架相似的功能,但主要关注性能。
由于它的设计和实现都尽量地小和快,因此很是适合在动态系统中使用(固然也能够以静态的方式使用,例如在编译器中)。
小明看了下,就选择使用 CGLIB。
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);
}
复制代码
项目经理:小明啊,最近我在想一个问题。不一样的服务,重试的时候次数应该是不一样的。由于服务对稳定性的要求各不相同啊。
小明:好的。(心想,重试都搞了一周了,今天都周五了。)
下班以前,小明一直在想这个问题。恰好周末,花点时间写个重试小工具吧。
spring
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;
}
复制代码
@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);
}
}
}
复制代码
当前方法一共重试 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
...
复制代码
周一来到公司,项目经理又和小明谈了起来。
项目经理:重试次数是知足了,可是重试其实应该讲究策略。好比调用外部,第一次失败,能够等待 5S 在次调用,若是又失败了,能够等待 10S 再调用。。。
小明:了解。
但是今天周一,还有其余不少事情要作。
小明在想,没时间写这个呀。看看网上有没有现成的。
Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。
在分布式系统中,为了保证数据分布式事务的强一致性,你们在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时状况采起一下重试操做。 你们用的最多的重试方式就是MQ了,可是若是你的项目中没有引入MQ,那就不方便了。
还有一种方式,是开发者本身编写重试机制,可是大多不够优雅。
重试条件:遇到 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
注解在使用时没法指定方法,若是一个类中多个重试方法,就会很麻烦。
表示是否开始重试。
序号 | 属性 | 类型 | 默认值 | 说明 |
---|---|---|---|---|
1 | proxyTargetClass | boolean | false | 指示是否要建立基于子类的(CGLIB)代理,而不是建立标准的基于Java接口的代理。 |
标注此注解的方法在发生异常时会进行重试
序号 | 属性 | 类型 | 默认值 | 说明 |
---|---|---|---|---|
1 | interceptor | String | "" | 将 interceptor 的 bean 名称应用到 retryable() |
2 | value | Class[] | {} | 可重试的异常类型。 |
3 | label | String | "" | 统计报告的惟一标签。若是没有提供,调用者能够选择忽略它,或者提供默认值。 |
4 | maxAttempts | int | 3 | 尝试的最大次数(包括第一次失败),默认为3次。 |
5 | backoff | @Backoff | @Backoff() | 指定用于重试此操做的backoff属性。默认为空 |
序号 | 属性 | 类型 | 默认值 | 说明 |
---|---|---|---|---|
1 | delay | long | 0 | 若是不设置则默认使用 1000 milliseconds |
2 | maxDelay | long | 0 | 最大重试等待时间 |
3 | multiplier | long | 0 | 用于计算下一个延迟延迟的乘数(大于0生效) |
4 | random | boolean | false | 随机重试等待时间 |
用于恢复处理程序的方法调用的注释。一个合适的复苏handler有一个类型为可投掷(或可投掷的子类型)的第一个参数 和返回与@Retryable
方法相同的类型的值。 可抛出的第一个参数是可选的(可是没有它的方法只会被调用)。 从失败方法的参数列表按顺序填充后续的参数。
注解式只是让咱们使用更加便捷,可是若是要更高的灵活性。可使用各类提供的方法。
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
复制代码
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:随机指数退避策略,引入随机乘数能够实现随机乘数回退
小华:咱们系统也要用到重试
项目经理:小明前段时间用了 spring-retry,分享下应该还不错
小明:spring-retry 基本功能都有,可是必须是基于异常来进行控制。若是你要以返回值的某个状态来断定是否须要重试,可能只能经过本身判断返回值而后显式抛出异常了。
小华:咱们项目中想根据对象的属性来进行重试。你能够看下 guava-retry,我好久之前用过,感受还不错。
小明:好的。
guava-retrying 模块提供了一种通用方法, 可使用Guava谓词匹配加强的特定中止、重试和异常处理功能来重试任意Java代码。
guava retryer工具与spring-retry相似,都是经过定义重试者角色来包装正常逻辑重试,可是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,可以兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。
Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callable
的 call()
方法
遇到异常以后,重试 3 次中止
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
复制代码
重试次数: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
复制代码
RetryerBuilder 是一个 factory 建立者,能够定制设置重试源且能够支持多个重试源,能够配置重试次数或重试超时时间,以及能够配置等待时间间隔,建立重试者 Retryer 实例。
RetryerBuilder 的重试源支持 Exception 异常对象和自定义断言对象,经过retryIfException 和 retryIfResult 设置,同时支持多个且能兼容。
retryIfException,抛出 runtime 异常、checked 异常时都会重试,可是抛出 error 不会重试。
retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。
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 能够指定你的 Callable 方法在返回值的时候进行重试,如
// 返回false重试
.retryIfResult(Predicates.equalTo(false))
//以_error结尾才重试
.retryIfResult(Predicates.containsPattern("_error$"))
复制代码
当发生重试以后,假如咱们须要作一些额外的处理动做,好比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
提供三种:
设定一个最长容许的执行时间;好比设定最长执行10s,不管任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
不中止,用于须要一直轮训知道返回指望结果的状况;
设定最大重试次数,若是超出最大重试次数则中止重试,并返回重试异常;
WaitStrategy
固定等待时长策略;
随机等待时长策略(能够提供一个最小和最大时长,等待时长为其区间随机值)
递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增长而增长)
指数等待时长策略;
Fibonacci 等待时长策略;
异常时长等待策略;
复合时长等待策略;
正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是二者沟通的媒介。
约定重试间隔,差别性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。
都使用了命令设计模式,经过委托重试对象完成相应的逻辑操做,同时内部封装实现重试逻辑。
spring-retry 和 guava-retry 工具都是线程安全的重试,可以支持并发业务场景的重试逻辑正确性。
功能逻辑中存在不稳定依赖场景,须要使用重试获取预期结果或者尝试从新执行逻辑不当即结束。好比远程接口访问,数据加载访问,数据上传校验等等。
对于异常场景存在须要重试场景,同时但愿把正常逻辑和重试逻辑解耦。
对于须要基于数据媒介交互,但愿经过重试轮询检测执行逻辑场景也能够考虑重试方案。
项目经理:我以为 guava-retry 挺好的,就是不够方便。小明啊,你给封装个基于注解的吧。
小明:……