CompletableFuture实现并发处理任务+汇总结果批量保存

CompletableFuture是一个非常好用的并发任务处理工具,本篇文章将介绍由此工具实现并发处理任务,并汇总结果批量保存DB,以此带来效率上的提升。

1 背景介绍

我们通常在项目中都会涉及到接收多天的原始数据,然后生成每天的数据报告,保存到DB。如果是循环每天数据顺序的执行生成每天报告保存DB,会有下面的问题:

  • 整个流程变成了串行,耗时较长
  • 写入DB的操作也是一条一条数据写入,没有批量写入效率高
java 复制代码
/**
 * 测试顺序串行生成报告,汇总批量保存DB
 */
@Test
public void testSequence() {
    long start = System.currentTimeMillis();
    // 模拟每天的数据
    List<String> days = new ArrayList<>();
    days.add("2024-03-01");
    days.add("2024-03-02");
    days.add("2024-03-03");

    // 循环每天的数据,生成每天报告
    List<DayReport> reportList = new ArrayList<>();
    for(String day: days) {
        DayReport result = generateDayReportTask(day);
        reportList.add(result);
    }

    // 汇总的报告list,批量保存到DB,提高写入的性能
    insertBatch(reportList);
    long execTime = System.currentTimeMillis() - start;
    log.info("执行耗时:{} ms", execTime);
}

耗时:743ms

如果是直接把生成报告的任务提交到线程池处理,主线程需要借助countDownLatch并发工具类等待线程池里面的任务执行完毕之后执行insertBatch(reportList)操作,代码实现上稍显复杂,同时还需考虑多个线程保存任务结果到reportList等线程安全问题。

所以针对上面的问题,引入CompletableFuture工具,实现并发处理任务,并汇总结果批量保存DB,以此带来效率上的提升。同时使用更加简单而且也不存在线程安全问题。

2 并发处理+汇总批量保存

java 复制代码
/**
 * @ClassName CompletableFutureTest
 * @Description
 * @Author
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class CompletableFutureTest {

  
    @Autowired
    @Qualifier("SummaryReportTask")
    ThreadPoolExecutor summaryReportTask;

    @Data
    private class DayReport{
        /**
         * 报告id
         */
        private Long reportId;

        /**
         * 每天的日期
         */
        private String day;

        /**
         * 是否执行异常
         */
        private Boolean ex = false;

        /**
         * 走路的步数
         */
        private int stepCount;

        public DayReport(Long reportId, String day, int stepCount) {
            this.reportId = reportId;
            this.day = day;
            this.stepCount = stepCount;
        }

        public DayReport(String day, Boolean ex) {
            this.day = day;
            this.ex = ex;
        }
    }

    /**
     * 生成每天报告
     * @param day
     * @return
     */
    private DayReport generateDayReportTask(String day) {
        log.info("模拟生成{}的报告...", day);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 报告id
        Long reportId = Long.parseLong(day.replace("-", ""));
        // 每天行走的步数
        int stepCount = RandomUtil.randomInt(1, 100);
        return new DayReport(reportId, day, stepCount);
    }

    /**
     * 处理任务执行产生的异常
     * @param e
     * @param day
     * @return
     */
    private DayReport handleException(Throwable e, String day) {
        // 打印异常信息,便于排查问题
        log.error("day: {}的任务执行异常:{}", day, e);
        // 返回异常标记的结果,便于后续判断任务是否出现异常,终止后续的业务流程
        return new DayReport(day, true);
    }


    /**
     * 并发生成报告,汇总批量保存DB
     */
    @Test
    public void testCompletableFuture() {
        long start = System.currentTimeMillis();
        // 模拟每天的数据
        List<String> days = new ArrayList<>();
        days.add("2024-03-01");
        days.add("2024-03-02");
        days.add("2024-03-03");

        List<CompletableFuture<DayReport>> futures = new ArrayList<>();
        // 循环每天的数据,使用CompletableFuture实现并发生成每天报告
        for(String day: days) {
            CompletableFuture<DayReport> future = CompletableFuture
                    // 提交生成报告任务到指定线程池,异步执行
                    .supplyAsync(() -> generateDayReportTask(day), summaryReportTask)
                    // 任务执行异常时,处理异常
                    .exceptionally(e -> handleException(e, day));
            // future对象添加到集合中
            futures.add(future);

        }

        try {
            // allOf方法等待所有任务执行完毕,最好设置超时时间以免长时间阻塞主线程
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(20, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error(" CompletableFuture.allOf异常: {}", e);
            // 出现异常,终止后续逻辑
            return;
        }
        // 循环获取任务执行返回的结果(即生成的每日报告)
        List<DayReport> reportList = new ArrayList<>();
        for (CompletableFuture<DayReport> future : futures) {
            DayReport result;
            try {
                result = future.get();
            } catch (Exception e) {
                log.error("future.get出现异常:{}", e);
                // 任何一个任务执行异常,则直接return,中断后续业务流程,防止产生的汇总报告不完整
                return;
            }
            // 每日报告汇总
            if(null != result && !result.getEx()) { // 判断任务执行没有出现异常
                reportList.add(result);
            } else {
                log.error("result为null或者任务执行出现异常");
                // 任何一个任务执行异常,则直接return,中断后续业务流程,防止产生的汇总报告不完整
                return;
            }
        }

        // 汇总的报告list,批量保存到DB,提高写入的性能
        insertBatch(reportList);
        long execTime = System.currentTimeMillis() - start;
        log.info("执行耗时:{} ms", execTime);
    }

    void insertBatch(List<DayReport> reportList) {
        log.info("报告批量保存reportList:{}", JSON.toJSONString(reportList));
    }

线程池配置

java 复制代码
@Configuration
@Slf4j
public class ExecutorConfig {


    /**
     * 处理每日汇总报告线程池
     * @return
     */
    @Bean("SummaryReportTask")
    public ThreadPoolExecutor summaryReportTaskExecutor() {
        int corePoolSize = cpuCores();
        int maxPoolSize = corePoolSize * 2;
        ThreadPoolExecutor threadPoolExecutor =  new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(800),
                // 自定义线程池名称,便于排查问题
                new CustomizableThreadFactory("summaryReportTaskExecutor"),
                // 超过最大线程数,拒绝任务并抛出异常
                new ThreadPoolExecutor.AbortPolicy());
        return threadPoolExecutor;
    }

    private int cpuCores() {
        return Runtime.getRuntime().availableProcessors();
    }
}

主要流程:

  1. 循环每天数据,提交生成报告任务到线程池,并发执行生成每日报告
  2. 任务发生异常处理,主要是打印异常信息,标记结果
  3. CompletableFuture.allOf方法等待所有任务执行完毕
  4. 获取任务执行结果,进行每日报告汇总为list
  5. 最后汇总的list批量保存DB

耗时:331ms,比串行处理节省一半的时间。

最后需要注意的点:

  1. CompletableFuture需要配置自定义线程池使用,可以做到不同业务线的线程池隔离,避免相互影响
  2. 任务的异常处理打印必要的异常日志便于排查问题
  3. 每个任务出现异常时,记得中断后续逻辑,避免汇总的数据出现不完整

3 总结

本章主要介绍了CompletableFuture使用的一类场景:实现并发处理任务 && 等待多个并发任务完成,并汇总各个任务返回的结果 && 批量保存。有类似这种业务场景的可以使用CompletableFuture来实现,以此提高运行效率。

相关推荐
武子康21 小时前
Java-159 MongoDB 副本集容器化 10 分钟速查卡|keyfile + –auth + 幂等 init 附 docker-compose
java·数据库·mongodb·docker·性能优化·nosql·1024程序员节
九皇叔叔1 天前
Java循环结构全解析:从基础用法到性能优化
java·开发语言·性能优化
UWA1 天前
有什么指标可以判断手机是否降频
人工智能·智能手机·性能优化·memory·游戏开发
鼠鼠我捏,要死了捏1 天前
深入解析Java GC调优:从原理到实战
java·性能优化·gc调优
武子康2 天前
Java-154 深入浅出 MongoDB 用Java访问 MongoDB 数据库 从环境搭建到CRUD完整示例
java·数据库·分布式·sql·mongodb·性能优化·nosql
性感博主在线瞎搞2 天前
【人工智能】神经网络的优化器optimizer(四):Adam自适应动量优化器
人工智能·深度学习·神经网络·性能优化·优化器
鼠鼠我捏,要死了捏2 天前
深入剖析Java垃圾回收性能优化实战指南
java·性能优化·gc
码界奇点2 天前
MongoDB 排序操作详解sort方法使用指南
数据库·mongodb·性能优化
武子康2 天前
Java-157 MongoDB 存储引擎 WiredTiger vs InMemory:何时用、怎么配、如何验证 mongod.conf
java·数据库·sql·mongodb·性能优化·系统架构·nosql
白帽子凯哥哥2 天前
SpringBoot + Elasticsearch实战:从零构建高性能搜索与数据分析平台
大数据·spring boot·后端·elasticsearch·搜索引擎·性能优化·聚合查询