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来实现,以此提高运行效率。

相关推荐
无知的前端11 分钟前
Flutter 一文精通Isolate,使用场景以及示例
android·flutter·性能优化
人工智能培训咨询叶梓18 分钟前
LLAMAFACTORY:一键优化大型语言模型微调的利器
人工智能·语言模型·自然语言处理·性能优化·调优·大模型微调·llama factory
计算机毕设定制辅导-无忧学长31 分钟前
HTML 性能优化之路:学习进度与优化策略(二)
学习·性能优化·html
庸俗今天不摸鱼3 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
Process8 小时前
前端图片技术深度解析:格式选择、渲染原理与性能优化
前端·面试·性能优化
沐土Arvin10 小时前
Nginx 核心配置详解与性能优化最佳实践
运维·开发语言·前端·nginx·性能优化
爱的叹息10 小时前
针对 SQL 查询中 IN 子句性能优化 以及 等值 JOIN 和不等值 JOIN 对比 的详细解决方案、代码示例及表格总结
数据库·sql·性能优化
我有医保我先冲19 小时前
SQL复杂查询与性能优化:医药行业ERP系统实战指南
数据库·sql·性能优化
anda010920 小时前
11-leveldb compact原理和性能优化
java·开发语言·性能优化
爱吃馒头爱吃鱼20 小时前
QML编程中的性能优化二
开发语言·qt·学习·性能优化