点击关注公众号,实用技术文章及时了解

背景介绍

在实际的项目应用场景中,经常会需要遇到远程服务接口的调用,时不时会出现一些接口调用超时,或者函数执行失败需要重试的情况,例如下边的这种场景:

某些不太稳定的接口,需要依赖于第三方的远程调用,例如数据加载,数据上传相关的类型。

方案整理

基于try catch机制

这种方式来做重试处理的话,会比较简单粗暴。

public void test(){try{//执行远程调用方法doRef();}catch(Exception e){//重新执行远程调用方法doRef();}
}

当出现了异常的时候,立即执行远程调用,此时可能忽略了几个问题:

  1. 如果重试出现了问题,是否还能继续重试

  2. 第一次远程调用出现了异常,此时可能第三方服务此时负载已达到瓶颈,或许需要间隔一段时间再发送远程调用的成功率会高些。

  3. 多次重试都失败之后如何通知调用方自己。

使用Spring的Retry组件

Spring的Retry组件提供了非常丰富的功能用于请求重试。接入这款组件的方式也很简单, 首先需要引入相关的依赖配置:

<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后是在启动类上加入一个@EnableRetry注解

@SpringBootApplication
@EnableRetry
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class);}
}

最后是在需要被执行的函数头部加入这一@Retryable注解:

@RestController
@RequestMapping(value = "/retry")
public class RetryController {@GetMapping(value = "/test")@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))public int retryServiceOne(int code) throws Exception {System.out.println("retryServiceOne 被调用,时间" + LocalTime.now());System.out.println("执行当前线程为:" + Thread.currentThread().getName());if(code==0){throw new Exception("业务执行异常!");}System.out.println("retryServiceOne 执行成功!");return 200;}
}

测试结果:

请求url:http://localhost:8080/retry/test?code=0

控制台会输出相关的调用信息:

从输出记录来看,确实是spring封装好的retry组件帮我们在出现了异常的情况下会重复调用该方法多次,并且每次调用都会有对应的时间间隔。

好的,看到了这里,目前大概了解了Spring的这款重试组件该如何去使用,那么我们再来深入思考一下,如果需要通过我们手写去实现一款重试组件需要考虑哪些因素呢?下边我和大家分享下自己的一些设计思路,可能有些部分设计得并不是特别完善。

手写一款重试组件

首先我们需要定义一个retry注解:

@Documented
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {int maxAttempts() default 3;int delay() default 3000;Class<? extends Throwable>[] value() default {};Class<? extends RetryStrategy> strategy() default FastRetryStrategy.class;Class<? extends RetryListener> listener() default AbstractRetryListener.class;
}

这款注解里面主要属性有:

  • 最大重试次数

  • 每次重试的间隔时间

  • 关注异常(仅当抛出了相应异常的条件下才会重试)

  • 重试策略(默认是快速重试)

  • 重试监听器

为了减少代码的耦合性,所以这里我将重试接口的拦截和处理都归到了aop层面去处理,因此需要引入一个对应的依赖配置:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

重试部分的Aop模块代码如下所示:

@Aspect
@Component
public class RetryAop {@Resourceprivate ApplicationContext applicationContext;@Pointcut("@annotation(org.idea.qiyu.framework.retry.jdk.config.Retry)")public void pointCut() {}@Around(value = "pointCut()")public Object doBiz(ProceedingJoinPoint point) {MethodSignature methodSignature = (MethodSignature) point.getSignature();Method method = methodSignature.getMethod();Retry retry = method.getDeclaredAnnotation(Retry.class);RetryStrategy retryStrategy = applicationContext.getBean(retry.strategy());RetryTask retryTask = new RetryTaskImpl(point);retryStrategy.initArgs(retry, retryTask);try {Object result = point.proceed();return result;} catch (Throwable throwable) {retryStrategy.retryTask();}return null;}private class RetryTaskImpl implements RetryTask {private ProceedingJoinPoint proceedingJoinPoint;private Object result;private volatile Boolean asyncRetryState = null;public RetryTaskImpl(ProceedingJoinPoint proceedingJoinPoint) {this.proceedingJoinPoint = proceedingJoinPoint;}public ProceedingJoinPoint getProceedingJoinPoint() {return proceedingJoinPoint;}public void setProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {this.proceedingJoinPoint = proceedingJoinPoint;}public Object getResult() {return result;}public void setResult(Object result) {this.result = result;}public Boolean getAsyncRetryState() {return asyncRetryState;}public void setAsyncRetryState(Boolean asyncRetryState) {this.asyncRetryState = asyncRetryState;}@Overridepublic Object getRetryResult() {return result;}@Overridepublic Boolean getRetryStatus() {return asyncRetryState;}@Overridepublic void setRetrySuccess() {this.setAsyncRetryState(true);}@Overridepublic void doTask() throws Throwable {this.result = proceedingJoinPoint.proceed();}}
}

这里解释一下,这个模块主要是拦截带有 @Retry 注解的方法,然后将需要执行的部分放入到一个RetryTask类型的对象当中,内部的doTask函数会触发真正的方法调用。

RetryTask接口的代码如下:

public interface RetryTask {Object getRetryResult();Boolean getRetryStatus();void setRetrySuccess();void doTask() throws Throwable;
}

首次函数执行的过程中,会有一个try catch的捕获,如果出现了异常情况就会进入了retryTask函数内部:

在进入retryTask函数当中,则相当于进入了具体的重试策略函数执行逻辑中。

从代码截图可以看出,重试策略是从Spring容器中加载出来的,这是需要提前注入到Spring容器。

重试策略接口:

public interface RetryStrategy {/*** 初始化一些参数配置** @param retry* @param retryTask*/void initArgs(Retry retry,RetryTask retryTask);/*** 重试策略*/void retryTask();}

默认的重试策略为快速重试策略,相关代码为:

public class FastRetryStrategy implements RetryStrategy, ApplicationContextAware {private Retry retry;private RetryTask retryTask;private ApplicationContext applicationContext;private ExecutorService retryThreadPool;public FastRetryStrategy() {}public ExecutorService getRetryThreadPool() {return retryThreadPool;}public void setRetryThreadPool(ExecutorService retryThreadPool) {this.retryThreadPool = retryThreadPool;}@Overridepublic void initArgs(Retry retry, RetryTask retryTask) {this.retry = retry;this.retryTask = retryTask;}@Overridepublic void retryTask() {if (!FastRetryStrategy.class.equals(retry.strategy())) {System.err.println("error retry strategy");return;}//安全类型bean查找String[] beanNames = applicationContext.getBeanNamesForType(retry.listener());RetryListener retryListener = null;if (beanNames != null && beanNames.length > 0) {retryListener = applicationContext.getBean(retry.listener());}Class<? extends Throwable>[] exceptionClasses = retry.value();RetryListener finalRetryListener = retryListener;//如果没有支持异步功能,那么在进行重试的时候就会一直占用着服务器的业务线程,导致服务器线程负载暴增retryThreadPool.submit(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= retry.maxAttempts(); i++) {int finalI = i;try {retryTask.doTask();retryTask.setRetrySuccess();return;} catch (Throwable e) {for (Class<? extends Throwable> clazz : exceptionClasses) {if (e.getClass().equals(clazz) || e.getClass().isInstance(clazz)) {if (finalRetryListener != null) {finalRetryListener.notifyObserver();}System.err.println("[FastRetryStrategy] retry again,attempt's time is " + finalI + ",tims is " + System.currentTimeMillis());try {Thread.sleep(retry.delay());} catch (InterruptedException ex) {ex.printStackTrace();}continue;}}}}}});}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;ExecutorService executorService = (ExecutorService) applicationContext.getBean("retryThreadPool");this.setRetryThreadPool(executorService);}
}

重试的过程中专门采用了一个单独的线程池来执行相应逻辑,这样可以避免一直消耗着服务器的业务线程,导致业务线程被长时间占用影响整体吞吐率。

另外,当重试出现异常的时候,还可以通过回调对应的监听器组件做一些记录:例如日志记录,操作记录写入等等操作。

public interface RetryListener {/*** 通知观察者*/void notifyObserver();
}

默认抽象类

public abstract class AbstractRetryListener implements RetryListener {@Overridepublic abstract void notifyObserver();
}

自定义的一个监听器对象:

public class DefaultRetryListener implements RetryListener {@Overridepublic void notifyObserver() {System.out.println("this is a DefaultRetryListener");}
}

好了,此时基本的配置都差不多了,如果需要使用的话,则需要进行一些bean的初始化配置:

@Configuration
public class RetryConfig {@Beanpublic FastRetryStrategy fastRetryStrategy(){return new FastRetryStrategy();}@Beanpublic RetryListener defaultRetryListener(){return new DefaultRetryListener();}@Beanpublic ExecutorService retryThreadPool(){ExecutorService executorService = new ThreadPoolExecutor(2,4,0L, TimeUnit.SECONDS,new LinkedBlockingQueue<>());return executorService;}
}

这里面主要将重试策略,重试监听器,重试所使用的线程池都分别进行了装载配置到Spring容器当中。

测试方式:

通过http请求url的方式进行验证:http://localhost:8080/do-test?code=2

@RestController
public class TestController {public static int count = 0;@Retry(maxAttempts = 5, delay = 100, value = {ArithmeticException.class}, strategy = FastRetryStrategy.class, listener = DefaultRetryListener.class)@GetMapping(value = "/do-test")public String doTest(int code) {count++;System.out.println("code is :" + code + " result is :" + count % 3 + " count is :" + count);if (code == 1) {System.out.println("--this is a test");} else {if (count % 5 != 0) {System.out.println(4 / 0);}}return "success";}
}

请求之后可以看到控制台输出了对应的内容:

不足点

  1. 需要指定完全匹配的异常才能做到相关的重试处理,这部分的处理步骤会比较繁琐,并不是特别灵活。

  2. 一定要是出现了异常才能进行重试,但是往往有些时候可能会返回一些错误含义的DTO对象,这方面的处理并不是那么灵活。

guava-retryer的重试组件就在上述的几个不足点中有所完善,关于其具体使用就不在本文中介绍了,感兴趣的小伙伴可以去了解下这款组件的使用细节。

推荐:

主流Java进阶技术(学习资料分享)

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

从0到1带你手撸一个请求重试组件,不信你学不会!相关推荐

  1. 呆呆带你手撸一个思维导图-基础篇

    希沃ENOW大前端 公司官网:CVTE(广州视源股份) 团队:CVTE旗下未来教育希沃软件平台中心enow团队 「本文作者:」 前言 你盼世界,我盼望你无bug.Hello 大家好,我是霖呆呆! 哈哈 ...

  2. 手撸一个Switch开关组件

    一.前言 手撸系列又来了,这次咱们来撸一个Switch开关组件,废话不多说,咱们立刻发车. 二.使用效果 三.实现分析 首先我们先不想它的这个交互效果,我们就实现"不合格"时的一个 ...

  3. 还不懂spring IOC核心原理?200行代码带你手撸一个

    Spring做为Java企业级应用的开源开发框架,早已成为Java后端开发事实上的行业标准,无数的公司选择Spring作为基础的开发框架. 使用Spring框架的人一定都听过Spring的IoC(控制 ...

  4. 很多小伙伴不太了解ORM框架的底层原理,这不,冰河带你10分钟手撸一个极简版ORM框架(赶快收藏吧)

    大家好,我是冰河~~ 最近很多小伙伴对ORM框架的实现很感兴趣,不少读者在冰河的微信上问:冰河,你知道ORM框架是如何实现的吗?比如像MyBatis和Hibernate这种ORM框架,它们是如何实现的 ...

  5. .Net Core手撸一个基于Token的权限认证

    说明 权限认证是确定用户身份的过程.可确定用户是否有访问资源的权力 今天给大家分享一下类似JWT这种基于token的鉴权机制 基于token的鉴权机制,它不需要在服务端去保留用户的认证信息或者会话信息 ...

  6. Goroutine 并发调度模型深度解析之手撸一个高性能 goroutine 池

    文章目录 1 前言 2 Goroutine & Scheduler 2.1 线程那些事儿 2.1.1 用户级线程模型 2.1.2 内核级线程模型 2.1.3 两级线程模型 2.2 G-P-M ...

  7. 手撸一个springsecurity,了解一下security原理

    今天手撸一个简易版本的springsecurity,带大家理解下springsecurity的原理.转载自:www.javaman.cn 手撸一个springsecurity,了解一下security ...

  8. javascript实现图片轮播_手撸一个简易版轮播图(上)

    手撸一个简易版轮播图 实现原理,通过控制 swiper-warpper 容器的定位来达到切换图片的效果. 页面布局 简易版轮播图 < > 页面样式 .container{width: 60 ...

  9. php 六边形 属性图 能力数值图,详解基于 Canvas 手撸一个六边形能力图

    一.前言 六边形能力图如下,由 6 个 六边形组成,每一个顶点代表其在某一方面的能力.这篇文章我们就来看看如何基于 canvas 去绘制这么一个六边形能力图.当然,你也可以基于其他开源的 js 方案来 ...

最新文章

  1. 【Go语言】【4】GO语言类型和为类型增加方法
  2. 获腾讯增持,B站二次元的商业化道路仍布满荆棘
  3. 8 Redis 持久化RDB
  4. jenkins自动化打包部署,jenkins执行sh脚本不退出问题
  5. 【转】微服务实践(五):微服务的事件驱动数据管理
  6. 单片机检测stc没反应_我为什么不用STC单片机
  7. Ubuntu麒麟下搭建FTP服务
  8. SpringApplication:SpringBoot程序启动的一站式解决方案
  9. boost::lockfree::queue多线程读写实例
  10. Bootstrap 3之美03-独立行,文字环绕,图片自适应,隐藏元素
  11. [C#]C#补习——控制流语句相关
  12. Unity面试题加强版之三《Unity脚本基础》
  13. 2020-4-18 深度学习笔记20 - 深度生成模型 2 (深度信念网络DBN,深度玻尔兹曼机DBM)
  14. IO虚拟化 - virtio-blk前端驱动分析【转】
  15. java存根_Java方法存根
  16. java统计误码率_MATLAB通信工具箱来计算误码率
  17. MongoDB的用户管理总结
  18. 如何理解dubbox
  19. java实现下载excel文件
  20. 我的QT Creator学习笔记(二十一)——Qt国际化和Qt插件

热门文章

  1. 微信官方回应iOS 13.2频繁杀后台问题:建议先不升级
  2. 阿里巴巴数据报告:消费向境内回流 低线城市消费蓬勃
  3. 腾讯 AI“绝悟”升级至王者荣耀电竞职业水平 测试胜率达99.8%
  4. 华为今年或发两款5G产品:5G CPE Win和5G随行WiFi
  5. 珍珠全面屏!华为畅享9S/9e正式发布:千元三摄加持
  6. SQL COUNT() 语法总结及用法【原创】
  7. 嵌入式linux的tftp配置[ZT]
  8. 相关函数: atof, atoi, atol, strtod, strtoul
  9. Java并发编程之调度线程池
  10. flex布局导致拉伸的问题