sisyphus 综合了 spring-retry 和 gauva-retrying 的劣势,应用起来也非常灵活。
明天,让咱们一起看一下西西弗斯背地的故事。
情景导入
简略的需要
产品经理:实现一个按条件,查问用户信息的服务。
小明:好的。没问题。
代码
- 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
注解在应用时无奈指定办法,如果一个类中多个重试办法,就会很麻烦。
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
总结
优雅重试共性和原理
失常和重试优雅解耦,重试断言条件实例或逻辑异样实例是两者沟通的媒介。
约定重试距离,差异性重试策略,设置重试超时工夫,进一步保障重试有效性以及重试流程稳定性。
都应用了命令设计模式,通过委托重试对象实现相应的逻辑操作,同时外部封装实现重试逻辑。
spring-retry 和 guava-retry 工具都是线程平安的重试,可能反对并发业务场景的重试逻辑正确性。
优雅重试实用场景
性能逻辑中存在不稳固依赖场景,须要应用重试获取预期后果或者尝试从新执行逻辑不立刻完结。比方近程接口拜访,数据加载拜访,数据上传校验等等。
对于异样场景存在须要重试场景,同时心愿把失常逻辑和重试逻辑解耦。
对于须要基于数据媒介交互,心愿通过重试轮询检测执行逻辑场景也能够思考重试计划。
谈话
项目经理:我感觉 guava-retry 挺好的,就是不够不便。小明啊,你给封装个基于注解的吧。
小明:……
更好的实现
于是小明含泪写下了 sisyphus.
java 重试框架——sisyphus
心愿本文对你有所帮忙,如果喜爱,欢送点赞珍藏转发一波。
我是老马,期待与你的下次重逢。