推荐一下,比Spring-Retry还快的百万级任务重试框架Fast-Retry

往期热门文章:

1、分享9条高频SQL优化技巧

2、京东:MySQL 中的distinct和group by哪个效率更高?太刁钻!

3、分库分表也没吹的那么神…… 这 7 个问题难以解决

4、发现一款JSON可视化工具神器,惊艳了!

5、代码更新不停机:Spring Boot应用实现零停机更新!

来源:juejin.cn/post/7337989768637939739

前言

假设你的系统里有100万个用户,然后你要轮询重试的获取每个用户的身份信息, 如果你还在使用SpringRetry和GuavaRetry 之类的这种单任务的同步重试框架,那你可能到猴年马月也处理不完, 即使加再多的机器和线程也是杯水车薪, 而Fast-Retry正是为这种场景而生

Fast-Retry

一个高性能的多任务重试框架,支持百万级任务的异步重试、以及支持编程式和注解声明式等多种使用方式、 也支持自定义结果重试逻辑。

What is this?

与主流的Spring-Retry, Guava-Retry等单任务同步重试框架不同,Fast-Retry是一个支持异步重试框架,支持异步任务的重试、超时等待、回调。Spring-Retry, Guava-Retry均无法支持大批量任务的重试,即使加入线程池也无法解决,因为实际每个重试任务都是单独的同步逻辑,然后会会占用过多线程资源导致大量任务在等待处理,随着任务数的增加,系统吞吐量大大降低,性能指数级降低,而Fast-Retry在异步重试下的性能是前者的指数倍。下图是三者的性能对比
  • 测试线程池: 8个固定线程
  • 单个任务逻辑: 轮询5次,隔2秒重试一次,总耗时10秒
  • 未测预计公式:当我们使用线程池的时候, 一般线程池中 总任务处理耗时 = 任务数/并发度 x 单个任务重试耗时

可以看到即使是处理100万个任务,Fast-Retry的性能也比Spring-Retry和Guava-Retry处理在50个任务时的性能还要快的多的多属实降维打击,这么快的秘密在于除了是异步,重要的是当别人在重试间隔里休息的时候,Fast-Retry还在不停忙命的工作着。即使抛开性能不谈, SpringRetry使用繁琐,不支持根据结果的进行重试,GuavaRetry虽然支持,但是又没有提供注解声明式的使用。

快速开始

引入依赖

<dependency>
    <groupId>io.github.burukeyou</groupId>
    <artifactId>fast-retry-all</artifactId>
    <version>0.2.0</version>
</dependency>
有以下三种方式去构建我们的重试任务

使用重试队列

RetryTask就是可以配置我们重试任务的一些逻辑,比如怎么重试,怎么获取重试结果,隔多久后重试,在什么情况下重试。它可以帮助我们更加自由的去构建重试任务的逻辑。但如果只是简单使用,强烈建议使用FastRetryBuilder 或者 @FastRetry注解RetryQueue就是一个执行和调度我们重试任务的核心角色,其在使用上与线程池的API方法基本一致
ExecutorService executorService = Executors.newFixedThreadPool(8);
RetryQueue queue = new FastRetryQueue(executorService);
RetryTask<String> task = new RetryTask<String>() {
    int result = 0 ;

    // 下一次重试的间隔
    @Override
    public long waitRetryTime() {
        return 2000;
    }

    // 执行重试,每次重试回调此方法
    @Override
    public boolean retry() {
        return ++result < 5;
    }

     // 获取重试结果
    @Override
    public String getResult() {
        return  result + "";
    }
};
CompletableFuture<String> future = queue.submit(task);
log.info("任务结束 结果:{}",future.get());

使用FastRetryBuilder

底层还是使用的RetryQueue去处理, 只是帮我们简化了构建RetryTask的逻辑
RetryResultPolicy<String> resultPolicy = result -> result.equals("444");
FastRetryer<String> retryer = FastRetryBuilder.<String>builder()
        .attemptMaxTimes(3)
        .waitRetryTime(3, TimeUnit.SECONDS)
        .retryIfException(true)
        .retryIfExceptionOfType(TimeoutException.class)
        .exceptionRecover(true)
        .resultPolicy(resultPolicy)
        .build()
;

CompletableFuture<String> future = retryer.submit(() -> {
    log.info("重试");
    //throw new Exception("test");
    //int i = 1/0;
    if (0 < 10){
        throw new TimeoutException("test");
    }
    return "444";
});

String o = future.get();
log.info("结果{}", o);

使用@FastRetry注解

底层还是使用的RetryQueue去处理, 只是帮我们简化了构建RetryTask的逻辑,并且与Spring进行整合能对Spring的bean标记了FastRetry注解的方法进行代理, 提供了重试任务注解声明式的使用方式
  • 依赖Spring环境,所以需要在Spring配置类加上@EnableFastRetry注解启用配置 , 这个@FastRetry注解的使用才会生效
  • 如果将结果类型使用CompletableFuture包装,自动进行异步轮询返回,否则同步阻塞等待重试结果。(推荐)
下面定义等价于 RetryQueue.execute方法
// 如果发生异常,每隔两秒重试一次
@FastRetry(retryWait = @RetryWait(delay = 2))
public String retryTask(){
    return "success";
}
下面定义等价于 RetryQueue.submit方法,支持异步轮询
@FastRetry(retryWait = @RetryWait(delay = 2))
public CompletableFuture<String> retryTask(){
    return CompletableFuture.completedFuture("success");
}

自定义重试注解

如果不喜欢或者需要更加通用化的贴近业务的重试注解,提供一些默认的参数和处理逻辑,可以自行定义一个重试注解并标记上@FastRetry并指定factory,然后实现AnnotationRetryTaskFactory接口实现自己的构建重试任务的逻辑即可。@FastRetry默认实现就是:FastRetryAnnotationRetryTaskFactory

使用建议

无论是使用以上哪种方式去构建你的重试任务,都建议使用异步重试的方法,即返回结果是CompletableFuture的方法, 然后使用CompletableFuture的whenComplete方法去等待异步重试任务的执行结果。

对比案例

有一个天气服务的重试任务,需要重试N次才可能获取到某城市的天气情况。分别使用Fast-Retry注解和Spring-Retry注解去并发获取1000个城市的天气情况,看下系统耗时。同样的逻辑,Spring-Retry需要1256秒左右,Fast-Retry只需要10秒.左右
// 天气服务
@Component
public class WeatherService {
    
    // Fast-Retry  重试获取天气城市天气情况
    @FastRetry(
            maxAttempts = 100,
            retryWait = @RetryWait(delay = 2,timeUnit = TimeUnit.SECONDS))
    public CompletableFuture<WeatherResult> getFutureWeatherForCompare(String cityName){
        log.info("WeatherService进行重试  次数:{} 城市: {}",++index,cityName);
        WeatherResult weather = WeatherServer.getWeather(cityName);
        if (weather == null){
            //继续重试
            throw new RuntimeException("模拟异常进行重试");
        }

        return FastRetryBuilder.of(weather);
    }

   // Spring-Retry  重试获取天气城市天气情况
    @Retryable(maxAttempts = 100,backoff = @Backoff(delay = 2000))
    public WeatherResult getSpringWeatherForCompare(String cityName){
        log.info("WeatherService进行重试  次数:{} 城市: {}",++index,cityName);
        WeatherResult weather = WeatherServer.getWeather(cityName);
        if (weather == null){
            //继续重试
            throw new RuntimeException("模拟异常进行重试");
        }
        return weather;
    }

}
使用Spring-Retry去执行1000个重试任务
/**
 * spring-retry注解-测试
 * @throws Exception
 */

@Test
public void testFastRetryManyTaskForSpring() throws Exception {
    List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
    ExecutorService pool = Executors.newFixedThreadPool(8);

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    int taskSize = 1000;
    for (int i = 0; i < taskSize; i++) {
        WeatherService taskWeatherService = context.getBean(WeatherService.class);
        CompletableFuture<WeatherResult> testFuture = new CompletableFuture<>();
        futures.add(testFuture);

        String cityName = "北京" + i;
        pool.execute(() -> {
            WeatherResult weather = taskWeatherService.getSpringWeatherForCompare(cityName);
            testFuture.complete(weather);
        });
    }

    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    System.out.println("所有任务完成");
    for (CompletableFuture<WeatherResult> future : futures) {
        WeatherResult weatherResult = future.get();
        log.info("城市轮询结束  result:{}",weatherResult.data);
    }

    stopWatch.stop();
    log.info("Spring-Retry测试总耗时  任务数:{} 耗时:{}",taskSize,stopWatch.getTotalTimeSeconds());
}
使用Fast-Retry去执行1000个重试任务
/**
 * 测试FastRetry注解测试
 * @throws Exception
 */

@Test
public  void testFastRetryManyTask() throws Exception {

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    int taskSize = 1000;

    List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
    for (int i = 0; i < taskSize; i++) {
        WeatherService taskWeatherService = context.getBean(WeatherService.class);
        String cityName = "北京" + i;
        CompletableFuture<WeatherResult> weather = taskWeatherService.getFutureWeatherForCompare(cityName);
        futures.add(weather);
    }

    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    System.out.println("所有任务完成");
    for (CompletableFuture<WeatherResult> future : futures) {
        WeatherResult weatherResult = future.get();
        log.info("城市轮询结束  result:{}",weatherResult.data);
    }

    stopWatch.stop();
    log.info("FastRetry测试总耗时  任务数:{} 耗时:{}",taskSize,stopWatch.getTotalTimeSeconds());
}
那么,fast-retry相比其它重试框架快在哪里呢?与其说快在哪,不如说同步型重试框架慢在哪。因为同步重试是阻塞的同步的,比如有 100 个重试任务,每个重试任务要重试 1 分钟,线程池有 10 个线程池,最多只能同时处理10 个重试任务。也就是说起码要 10 分钟后才去执行剩下的 90 个任务。最后 100 个任务轮询完都要 100 分钟了。实际没必要等它轮询完一个任务才去执行下一个任务,它开始轮询一个任务后就可以开始去执行下一个任务了。具体来说,Fast-Retry 的“快”主要体现在以下几个方面:
  1. 异步执行:Fast-Retry 通常采用异步方式执行重试逻辑,这意味着它可以在等待重试间隔时不阻塞主线程,从而提高应用程序的整体响应性和吞吐量。

  2. 非阻塞 I/O:在处理需要 I/O 操作(如网络请求、文件读写等)的重试时,Fast-Retry 可以利用非阻塞 I/O 机制,这样在等待 I/O 操作完成时不会占用宝贵的线程资源。

  3. 优化的重试策略:Fast-Retry 允许用户自定义重试策略,包括重试次数、重试间隔、退避算法等。通过智能的退避算法(如指数退避),它可以在保持高效率的同时减少对资源的不必要消耗。

  4. 资源利用:Fast-Retry 框架可能会优化资源的使用,例如通过复用连接或线程来减少创建和销毁资源的开销。

  5. 错误处理:Fast-Retry 能够快速识别和处理重试中的错误,减少错误处理的时间开销。

  6. 集成和扩展性:Fast-Retry 框架往往设计得易于集成和扩展,这意味着它可以快速地被添加到现有的系统中,并且可以根据需要进行定制。

  7. 避免不必要的重试:Fast-Retry 能够根据错误类型或其他条件判断是否需要重试,避免在明显无望的情况下进行无效的重试尝试。

  8. 性能监控:Fast-Retry 可能包含性能监控功能,这有助于及时发现性能瓶颈并进行优化。

注意,Fast-Retry 的具体实现可能会根据不同的编程语言和框架有所不同。

其他

Github 项目地址https://github.com/burukeYou/fast-retrymaven仓库地址:https://central.sonatype.com/artifact/io.github.burukeyou/fast-retry-all
往期热门文章:

1、服务down机了,线程池中的数据如何保证不丢失?2、Guava的这些骚操作,让我的代码量减少了50%3、告别类型错误:Java泛型上下界,这些坑你千万别踩!4、try catch 应该在 for 循环里面还是外面?5、困扰我 1 小时的 404 错误 别人 1 分钟解决了6、公司新来一个同事,把枚举运用得炉火纯青...7、Java8 中一个极其强悍的新接口,很多人没用过8、SpringBoot接口防抖(防重复提交)的一些实现方案9、入职第一天,看了公司代码,牛马沉默了...10、动态上传jar包热部署,看完还不会吗?


相关推荐

  • 3D点云学习新架构!PointRWKV联合RWKV,刷新点云表征学习性能及FLOPs
  • Kafka 为什么这么快?
  • 35岁中年博士失业,决定给找高校教职的后辈一些建议
  • 透视镜 !!! 一眼看穿485信号
  • 六个问题带你看懂什么是理工科学霸-OpenAI o1!
  • 顶刊TPAMI 2024!北理等提出FreqFusion,让CVPR投稿轻松涨点
  • 战胜100多位NLP研究人员!杨笛一团队最新百页论文:首次统计学上证明,LLM生成的idea新颖性优于人类
  • 中国最好的月色,被唐朝人写绝了
  • 11K Star 游戏创作神器 !!! 小白一下变大师级
  • 架构师必备底层逻辑:设计与建模
  • 低秩近似之路:伪逆(Pseudo Inverse)
  • Lombok常用注解介绍
  • 这个python库简直是office办公利器~
  • 实时数仓行业方案!
  • o1方法性能无上限!姚班马腾宇等数学证明:推理token够多,就能解决任意问题
  • 倒计时三年:国产数据库100%替代走到哪了?
  • 作者硬核,内容透彻接地气的多模态大模型通识读本 | 留言赠书
  • 成都周报丨清华成立百亿母基金,成渝国资再次联手出资
  • 422页新书《构建实用的全栈机器学习指南》pdf下载
  • 大厂也是草台班子!