java retry(重试) spring retry, guava retrying 详解
系列说明
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
[p]用于恢复处理程序的方法调用的注释。一个合适的复苏handler有一个类型为可投掷(或可投掷的子类型)的第一个参数[url=mailto:br/>和返回与`@Retryable`方法相同的类型的值。和返回与`@Retryable`方法相同的类型的值。- guava-retrying重试工具库: 什么时候重试
- spring-retry重试与熔断详解—《亿级流量》内容补充
- guava-retrying重试工具库: 隔多长时间重试
- java重试之Spring Retry
- Spring错误异常重试框架guava-retrying
- guava-retrying重试工具库: RetryListener
- Java异常错误重试方案研究(转)(spring-retry/guava-retryer)
- [java]微服务架构连载No3 Ribbon+Retry服务实现负载均衡和服务请求重试
- guava-retrying重试工具库: Retryer.call()使用注意事项
- guava-retrying,重试工具使用
- 使用guava-retry优雅的实现接口重试
- Guava-Retrying实现重试机制
- guava-retrying重试工具库: 什么时候终止
- guava-retrying实现业务逻辑重试
- guava-retrying重试工具库: 阻塞策略BlockStrategy
- guava-retrying重试工具库: AttemptTimeLimiter
- Java类和对象 详解(一)
- java synchronized详解
- (十二)Java工具类StringUtils中trim、trimToEmpty、trimToNull方法详解
- JAVA中intern()方法的详解