JAVA重试机制多种方式深入浅出
重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。
重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。
为了方便说明,先假设我们想要进行重试的方法如下:
@Slf4j
@Component
public class HelloService {
private static AtomicLong helloTimes = new AtomicLong();
public String hello(){
long times = helloTimes.incrementAndGet();
if (times % 4 != 0){
log.warn("发生异常,time:{}", LocalTime.now() );
throw new HelloRetryException("发生Hello异常");
}
return "hello";
}
}
调用处:
@Slf4j
@Service
public class HelloRetryService implements IHelloService{
@Autowired
private HelloService helloService;
public String hello(){
return helloService.hello();
}
}
也就是说,这个接口每调4次才会成功一次。
手动重试
先来用最简单的方法,直接在调用的时候进重试:
// 手动重试
public String hello(){
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloService.hello();
log.info("helloService返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloService.hello() 调用失败,准备重试");
}
}
throw new HelloRetryException("重试次数耗尽");
}
发生异常,time:10:17:21.079413300
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
helloService返回:hello
service.helloRetry():hello
程序在极短的时间内进行了4次重试,然后成功返回。
这样虽然看起来可以解决问题,但实践上,由于没有重试间隔,很可能当时依赖的服务尚未从网络异常中恢复过来,所以极有可能接下来的几次调用都是失败的。
而且,这样需要对代码进行大量的侵入式修改,显然,不优雅。
代理模式
上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。
所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?
其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。你会发现又是代理。
@Slf4j
public class HelloRetryProxyService implements IHelloService{
@Autowired
private HelloRetryService helloRetryService;
@Override
public String hello() {
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloRetryService.hello();
log.info("helloRetryService 返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloRetryService.hello() 调用失败,准备重试");
}
}
throw new HelloRetryException("重试次数耗尽");
}
}
这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了,分工明确。比如,现在想要在重试之间加上一个延迟,只需要做一点点修改即可:
@Override
public String hello() {
int maxRetryTimes = 4;
String s = "";
for (int retry = 1; retry <= maxRetryTimes; retry++) {
try {
s = helloRetryService.hello();
log.info("helloRetryService 返回:{}", s);
return s;
} catch (HelloRetryException e) {
log.info("helloRetryService.hello() 调用失败,准备重试");
}
// 延时一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
throw new HelloRetryException("重试次数耗尽");
}
代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。
如果每个类都写这么一长串类似的代码,显然,不优雅!
JDK动态代理
这时候,动态代理就闪亮登场了。只需要写一个代理处理类就ok了。
@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(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 {
return method.invoke(subject, args);
} catch (Exception e) {
times++;
log.info("times:{},time:{}", times, LocalTime.now());
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
// 延时一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 获取动态代理
*
* @param realSubject 代理对象
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}
咱们测试一下:
@Test
public void helloDynamicProxy() {
IHelloService realService = new HelloService();
IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
String hello = proxyService.hello();
log.info("hello:{}", hello);
}
输出结果如下:
hello times:1
发生异常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
发生异常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
发生异常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello
在重试了4次之后输出了Hello,符合预期。
动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。
不过不要高兴的太早,这里因为被代理的HelloService是一个简单的类,没有依赖其它类,所以直接创建是没有问题的,但如果被代理的类依赖了其它被Spring容器管理的类,则这种方式会抛出异常,因为没有把被依赖的实例注入到创建的代理实例中。
这种情况下,就比较复杂了,需要从Spring容器中获取已经装配好的,需要被代理的实例,然后为其创建代理类实例,并交给Spring容器来管理,这样就不用每次都重新创建新的代理类实例了。
话不多说,撸起袖子就是干。
新建一个工具类,用来获取代理实例:
@Component
public class RetryProxyHandler {
@Autowired
private ConfigurableApplicationContext context;
public Object getProxy(Class clazz) {
// 1. 从Bean中获取对象
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
Set<Map.Entry<String, Object>> entries = beans.entrySet();
if (entries.size() <= 0){
throw new ProxyBeanNotFoundException();
}
// 如果有多个候选bean, 判断其中是否有代理bean
Object bean = null;
if (entries.size() > 1){
for (Map.Entry<String, Object> entry : entries) {
if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
bean = entry.getValue();
}
};
if (bean != null){
return bean;
}
throw new ProxyBeanNotSingleException();
}
Object source = beans.entrySet().iterator().next().getValue();
Object source = beans.entrySet().iterator().next().getValue();
// 2. 判断该对象的代理对象是否存在
String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
Boolean exist = beanFactory.containsBean(proxyBeanName);
if (exist) {
bean = beanFactory.getBean(proxyBeanName);
return bean;
}
// 3. 不存在则生成代理对象
bean = RetryInvocationHandler.getProxy(source);
// 4. 将bean注入spring容器
beanFactory.registerSingleton(proxyBeanName, bean);
return bean;
}
}
使用的是JDK动态代理:
@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(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 {
return method.invoke(subject, args);
} catch (Exception e) {
times++;
log.info("retry times:{},time:{}", times, LocalTime.now());
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
// 延时一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 获取动态代理
*
* @param realSubject 代理对象
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}
至此,主要代码就完成了,修改一下HelloService类,增加一个依赖:
@Slf4j
@Component
public class HelloService implements IHelloService{
private static AtomicLong helloTimes = new AtomicLong();
@Autowired
private NameService nameService;
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("发生异常,time:{}", LocalTime.now() );
throw new HelloRetryException("发生Hello异常");
}
return "hello " + nameService.getName();
}
}
NameService其实很简单,创建的目的仅在于测试依赖注入的Bean能否正常运行。
@Service
public class NameService {
public String getName(){
return "Frank";
}
}
测试一下:
@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
String hello = proxy.hello();
log.info("hello:{}", hello);
}
结果:
hello times:1
发生异常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
发生异常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
发生异常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank
完美,这样就不用担心依赖注入的问题了,因为从Spring容器中拿到的Bean对象都是已经注入配置好的。当然,这里仅考虑了单例Bean的情况,可以考虑的更加完善一点,判断一下容器中Bean的类型是Singleton还是Prototype,如果是Singleton则像上面这样进行操作,如果是Prototype则每次都新建代理类对象。
另外,这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了。
Spring AOP
想要无侵入式的修改原有逻辑?想要一个注解就实现重试?用Spring AOP不就能完美实现吗?使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些额外的逻辑。
先创建一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
int retryTimes() default 3;
int retryInterval() default 1;
}
有两个参数,retryTimes 代表最大重试次数,retryInterval代表重试间隔。
@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("发生异常,time:{}", LocalTime.now() );
throw new HelloRetryException("发生Hello异常");
}
return "hello " + nameService.getName();
}
接着,进行最后一步,编写AOP切面:
@Slf4j
@Aspect
@Component
public class RetryAspect {
@Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
private void retryMethodCall(){}
@Around("retryMethodCall()")
public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
// 获取重试次数和重试间隔
Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);
int maxRetryTimes = retry.retryTimes();
int retryInterval = retry.retryInterval();
Throwable error = new RuntimeException();
for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
try {
Object result = joinPoint.proceed();
return result;
} catch (Throwable throwable) {
error = throwable;
log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
}
Thread.sleep(retryInterval * 1000);
}
throw new RetryExhaustedException("重试次数耗尽", error);
}
}
开始测试:
@Autowired
private HelloService helloService;
@Test
public void helloAOP(){
String hello = helloService.hello();
log.info("hello:{}", hello);
}
打印结果:
hello times:1
发生异常,time:16:49:30.224649800
调用发生异常,开始重试,retryTimes:1
hello times:2
发生异常,time:16:49:32.225230800
调用发生异常,开始重试,retryTimes:2
hello times:3
发生异常,time:16:49:34.225968900
调用发生异常,开始重试,retryTimes:3
hello times:4
hello:hello Frank
这样就相当优雅了,一个注解就能搞定重试,简直不要更棒。
Spring 的重试注解
实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。
那让我们先来看看这个轮子究竟好不好使。
先引入重试所需的jar包:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?竟然抄袭我的注解【手动狗头】)
@Retryable
public String hello(){
long times = helloTimes.incrementAndGet();
log.info("hello times:{}", times);
if (times % 4 != 0){
log.warn("发生异常,time:{}", LocalTime.now() );
throw new HelloRetryException("发生Hello异常");
}
return "hello " + nameService.getName();
}
默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。
但Spring里的重试机制还支持很多很有用的特性,比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。
@Retryable{value = {HelloRetryException.class}}
public String hello(){
...
}
也可以使用include和exclude来指定包含或者排除哪些异常进行重试。
可以用maxAttemps指定最大重试次数,默认为3次。
可以用interceptor设置重试拦截器的bean名称。
可以通过label设置该重试的唯一标志,用于统计输出。
可以使用exceptionExpression来添加异常表达式,在抛出异常后执行,以判断后续是否进行重试。
此外,Spring中的重试机制还支持使用backoff来设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数。
举个例子:
@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2))
public String hello(){
...
}
该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。
重试机制还支持使用@Recover 注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。
这里值得注意的是,想要@Recover 注解生效的话,需要跟被@Retryable 标记的方法在同一个类中,且被@Retryable 标记的方法不能有返回值,否则不会生效。
并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。
@Recover
public boolean recover(Exception e) {
log.error("达到最大重试次数",e);
return false;
}
除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:
@Test
public void normalSpringRetry() {
// 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
exceptionMap.put(HelloRetryException.class, true);
// 构建重试模板实例
RetryTemplate retryTemplate = new RetryTemplate();
// 设置重试回退操作策略,主要设置重试间隔时间
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
long fixedPeriodTime = 1000L;
backOffPolicy.setBackOffPeriod(fixedPeriodTime);
// 设置重试策略,主要设置重试次数
int maxRetryTimes = 3;
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);
Boolean execute = retryTemplate.execute(
//RetryCallback
retryContext -> {
String hello = helloService.hello();
log.info("调用的结果:{}", hello);
return true;
},
// RecoverCallBack
retryContext -> {
//RecoveryCallback
log.info("已达到最大重试次数");
return false;
}
);
}
此时唯一的好处是可以设置多种重试策略:
NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
可以看出,Spring中的重试机制还是相当完善的,比上面自己写的AOP切面功能更加强大。
这里还需要再提醒的一点是,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效。
但也还是存在一定的不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验。
JAVA重试机制多种方式深入浅出的更多相关文章
- Java重试机制
重试作用: 对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法.写操作等(要考虑写是否幂等)都不适合重试. 远程调用超时.网络突然中断可以重试.在微服务治理框架中,通常都有自己的重试与 ...
- Java list 分页(多种方式)
方式一:public static void fenye(List list,int pagesize){ int totalcount=list.size(); int pagecou ...
- Rocket重试机制,消息模式,刷盘方式
一.Consumer 批量消费(推模式) 可以通过 consumer.setConsumeMessageBatchMaxSize(10);//每次拉取10条 这里需要分为2种情况 Consumer端先 ...
- Java之Retry重试机制详解
应用中需要实现一个功能: 需要将数据上传到远程存储服务,同时在返回处理成功情况下做其他操作.这个功能不复杂,分为两个步骤:第一步调用远程的Rest服务上传数据后对返回的结果进行处理:第二步拿到第一步结 ...
- Java中测试异常的多种方式
使用JUnit来测试Java代码中的异常有很多种方式,你知道几种? 给定这样一个class. Person.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...
- java 获取classpath下文件多种方式
java 获取classpath下文件多种方式 一:properties下配置 在resources下定义server.properties register.jks.path=classpath\: ...
- java获得路径的多种方式
本文讲解java语言中获得运行时路径的多种方式,包括java项目.java web项目.jar.weblogic等多种场景. 一.this.getClass().getClassLoader().ge ...
- Java反射机制(创建Class对象的三种方式)
1:SUN提供的反射机制的类: java.lang.Class<T> java.lang.reflect.Constructor<T> java.lang.reflect.Fi ...
- Java多种方式读文件,追加文件内容,等对文件的各种操作
一.多种方式读文件内容. 1.按字节读取文件内容 2.按字符读取文件内容 3.按行读取文件内容 4.随机读取文件内容 import java.io.BufferedReader; import jav ...
- java中多种方式读文件
转自:http://www.jb51.net/article/16396.htm java中多种方式读文件 一.多种方式读文件内容. 1.按字节读取文件内容 2.按字符读取文件内容 3.按行读取文件内 ...
随机推荐
- CF1557总结
CF1557总结 Codeforces Round #737 (Div. 2) 先看了 A .意思是要把序列分成两个子序列,使得两序列各自平均值的和最小,输出最小值,要求 \(O(n)\) .想半天然 ...
- JS中split、slice、splice区别
splite 定义:该方法是切割字符串的一种方法,该方法主要用于把一个字符串分割成字符串数组并且返回新生成的数组.用于字符串对象 语法:str.split(separator,howmany) 返 ...
- mac中Mysql各种版本下载
https://downloads.mysql.com/archives/community/ 安装后,5.8以下修改密码,参考 https://www.cnblogs.com/xuyin/p/121 ...
- PYTHON常用五大库
python常用五大库 Numpy Numpy 是python科学计算的基础包,本书大部分内容都基于numpy以及构建于其上的库.其功能有: 快速高效的多维数组对象ndarray 用于对数组执行元素级 ...
- ActiveMQ的基本使用
优点:消息中间件主要解决了各个模块异步调动问题,解除了模块之间的耦合,提高了运行的性能.一.点对点方式生产者1.创建连接工厂ActiveMQConnectionFactory 基于TCP协议Activ ...
- Shell脚本结构化-控制流
脚本结构化命令 上一章给出的那些 shell 脚本里,shell 按照命令在脚本中出现的顺序依次进行处理.对顺序操作来说,这已经足够了,因为在这种操作环境下,你想要的就是所有的命令按照正确的顺序执行. ...
- mysql授权、导入等基本操作
1.授权: mysqladmin -uroot password rootpwd mysql -uroot -prootpwd mysql -e "INSERT INTO user (Hos ...
- 认识flutter
flutter是谷歌的移动的ui框架,可以快速的在ios和安卓上构建高质量的原生用户界面.最主要的是完全免费开源.开发快,最重要的是使用flutter开发的开发工作者也越来越多了,生态圈也越来越好了. ...
- EF调用sql语句
1.连接数据库 2.在Dal进行调用sql语句 public List<UserInfo> PageShow(out int totalcount,out int totalpage, s ...
- jetson TX2 + opencv3.4 + python3 + 双目 +人脸检测
淘宝看到一款很便宜的双目,150元,就买了.想着用它学习一下opencv,好换个工作.当然,也想着能否用它做一些好玩的,比如三维重建之类高大上的东西.先用便宜的入个门,等以后眼界高了再看是不是买那些更 ...