如何落地一款重试组件(上)

简介: 如何落地一款重试组件(上)

背景介绍



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


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


方案整理


\

基于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 {
    @Resource
    private 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;
        }
        @Override
        public Object getRetryResult() {
            return result;
        }
        @Override
        public Boolean getRetryStatus() {
            return asyncRetryState;
        }
        @Override
        public void setRetrySuccess() {
            this.setAsyncRetryState(true);
        }
        @Override
        public 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;
    }
    @Override
    public void initArgs(Retry retry, RetryTask retryTask) {
        this.retry = retry;
        this.retryTask = retryTask;
    }
    @Override
    public 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() {
            @Override
            public 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;
                            }
                        }
                    }
                }
            }
        });
    }
    @Override
    public 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 {
    @Override
    public abstract void notifyObserver();
}
复制代码


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


public class DefaultRetryListener implements RetryListener {
    @Override
    public void notifyObserver() {
        System.out.println("this is a DefaultRetryListener");
    }
}
复制代码


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


@Configuration
public class RetryConfig {
    @Bean
    public FastRetryStrategy fastRetryStrategy(){
        return new FastRetryStrategy();
    }
    @Bean
    public RetryListener defaultRetryListener(){
        return new DefaultRetryListener();
    }
    @Bean
    public 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对象,这方面的处理并不是那么灵活。

目录
相关文章
|
XML 人工智能 缓存
使用 Higress 快速构建 AI 应用
Higress 基于企业内外的丰富场景沉淀了众多面向 AI 的功能,推出了 AI 原生的 API 网关形态并且全部开源。
453 85
|
10月前
|
负载均衡 并行计算 安全
秒懂IT术语
本文以幽默的方式,将恋爱关系比作计算机领域的各种技术概念,如冷备份、双机热备份、异地容灾备份、云备份、灾难演练、ping、TraceRoute、心跳监测、故障切换、主从配置、负载均衡、集群LVS、多集群横向扩容、纵向扩容、CDN、DOS/DDOS攻击、访问控制、入侵检测/防御系统、SIEM/SOC、等级保护、专线/租用专线/SDWAN、VPN、逆向工程、压力测试、数据分析、并行计算、云计算、数据挖掘、精准推送、容器、实时数据采集、虚拟化、大数据中心、大数据应用、大数据脱敏、数据融合跨域建模、大数据变现、数据简化等。通过这些比喻,生动形象地展示了技术原理及其应用场景。
290 59
|
11月前
|
数据采集 前端开发 测试技术
Selenium中定位元素的9种方法
在Selenium中,定位页面元素是自动化测试和网页爬虫的基础。常用的9种元素定位方法包括:ID、Name、Class Name、Tag Name、CSS Selector、XPath、Link Text、Partial Link Text,以及XPath和CSS选择器的组合使用。每种方法各有优劣,建议根据页面的具体情况和元素的属性选择最合适的方法,并使用显式等待确保元素可用。
1541 5
|
11月前
|
SQL 关系型数据库 MySQL
MySQL语法
MySQL语法
246 4
|
12月前
|
数据采集 存储 JavaScript
Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping
在现代Web开发中,数据采集尤为重要,尤其在财经领域。本文以“东财股吧”为例,介绍如何使用Puppeteer结合代理IP技术进行高效的数据抓取。Puppeteer是一个强大的Node.js库,支持无头浏览器操作,适用于复杂的数据采集任务。通过设置代理IP、User-Agent及Cookies,可显著提升抓取成功率与效率,并以示例代码展示具体实现过程,为数据分析提供有力支持。
473 2
Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping
Visual paradigm社区版下载及中文菜单的设置
Visual paradigm社区版下载及中文菜单的设置
541 0
|
前端开发 Java 关系型数据库
「架构」分层架构
**分层架构**是软件设计的关键模式,它将应用划分为独立层,如表示层、业务逻辑层和数据访问层,强调**单一职责**和**松耦合**。优点包括**代码组织**、**技术多样性**、**团队协作**和**可扩展性**,但可能带来**性能影响**和**设计复杂性**。通过定义清晰接口和合理划分层次来管理。常用技术栈涉及Web前端、后端框架、数据库、ORM和通信协议等。
290 0
|
存储 SQL 人工智能
揭秘数据库技术:核心原理、应用场景与未来趋势
一、引言 在数字化和智能化的今天,数据库技术已经成为企业和组织不可或缺的基础设施
|
Java Spring
Spring5源码(29)-Pointcut和Advisor以及静态普通方法名匹配切面
Spring5源码(29)-Pointcut和Advisor以及静态普通方法名匹配切面
161 0
|
存储 芯片 异构计算
【FPGA原型验证】FPGA 技术:芯片和工具-当今的 FPGA 器件技术_fpga原型版本
【FPGA原型验证】FPGA 技术:芯片和工具-当今的 FPGA 器件技术_fpga原型版本
340 0