当前位置: 首页 > news >正文

EasyExcel结合多线程+控制sheet页全量导出

业务分析

内部平台需要一个导出mysql数据到excel的方法,所以使用了EasyExcel

因为EasyExcel的sheet页是放到一个List里面的,如果把百万量级的数据放到sheet页中全量写入会有OOM风险,所以最终选择的方案是分sheet页写入

同时因为该平台是多用户的,所以不仅要控制当前方法的sheet页的总量,还要估算多用户场景下sheet页加起来的量大小,如果规定每个写入的sheet页的量级为50w,那么10个用户并行导出的时候是不是500w的量级?

那就会出现OOM问题,为了严格控制sheet页的和总量,就要细致化到控制每个线程的每次写入的sheet页的量

为了优化多线程,还可以上深分页优化来进一步优化

多优化逻辑:线程+多sheet页+深分页


代码逻辑解析

我会开3个线程,然后sheet页按照10w为一批写入,3个线程就是30w

0-30w就用主线程,30w-60w就用两个线程,60w以后就用3个线程

本次要导出的是90w量级的数据,我们严格控制sheet也就是每次写入的一批数据量batchSize为10w

导出数据总量、每批数据量,算出写入sheet页的总批次

总批次/要开启的线程数,得到每个线程要写入的批次

有3个线程,每个线程的第一次查询要拿到第一批的数据的最后一个数据的id(sql是根据id排序的)

SQL: @Select("select * from excel where id >#{lastMaxId} order by id limit #{batchsize} ")

然后每个线程除了第一次查询,其他查询都可以优化深分页

为什么我要写入多个文件而不是多线程写入一个文件?

首先我尝试并发写同一个文件,然后报错了,因为文件是不能并发写的,多线程写入会导致文件烂掉然后再也打不开

然后我想到了我们多线程写不同的文件,然后通过python脚本去合并多个sheet页和多个xsml文件

有个小问题就是xsml文件本身的问题:一个sheet页最多只能写入,所以文件本身就会有限制,这就是为什么要写入多个文件和多个sheet页最终合并文件和sheet页


测试类代码

package com.example.kiratest.test.Excel;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.kiratest.EasyExcel.Mapper.ExcelMapper;
import com.example.kiratest.EasyExcel.pojo.Excel;
import com.github.pagehelper.PageHelper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
//并发写同一个文件
@SpringBootTest
@Slf4j
public class MutiExcelTest {

    @Resource
    private ExcelMapper excelMapper;

    @Resource
    Map<String, ThreadPoolExecutor> ThreadPoolMap;

    @Test
    public void getData() throws InterruptedException {

        ThreadPoolExecutor easyExcelThreadPool = ThreadPoolMap.get("EasyExcelThreadPool");//拿到线程池

        int pageSize = 100000;//10w为一批
        Integer total = excelMapper.countTotal();//统计数据总数
        log.info("总数:{}",total);
        //batchSize<3,用一个线程,6就是2个线程,9就是3个线程
        int batchSize = (int) Math.ceil((double) total / pageSize);//一共要执行的批次
        log.info("batchSize:{}",batchSize);
        int everythreadSize = batchSize / 3;//一共开三个线程,每个线程应该负责多少批次



        int threadCount = 1; // 主线程
        if (batchSize > 3 && batchSize < 6) threadCount += 1;
        else if (batchSize > 6) threadCount += 2;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        //开始EasyExcel写入
        String filePath = "C:\\Users\\ziJian.zheng\\IdeaProjects\\Kira-Test\\src\\main\\resources\\templates\\test1.xlsx";
        //用来写入的ExcelWriter
        ExcelWriter writer = EasyExcel.write(filePath).build();


//
//
//
        //开启异步线程
        if (batchSize > 3 ) {
            easyExcelThreadPool.execute(() -> {
                try {
                    //用来写入的ExcelWriter
                    ExcelWriter writer1 = EasyExcel.write(filePath).build();
                    int first = everythreadSize + 1;//作为sheet页标识,也就是批次标识
                    Integer lastId = null;
                    //多Sheet写入
                    for (int i = 0; i < everythreadSize; i++) {
                        WriteSheet sheet = EasyExcel.writerSheet("批次" + first++).build();
                        if (i == 0) {//也就是我们的第一次写入
                            PageHelper.startPage(everythreadSize+1,pageSize);//第一次普通的分页查询,后面要知道id,使用书签法来优化深度分页问题
                            List<Excel> excels = excelMapper.selectAllOrderById();

                            if (excels.size() > batchSize)
                                lastId = excels.get(excels.size() - 1).getId();
                            log.info("LastId:{}",lastId);
                            writer1.write(excels, sheet);

                        } else { //我们的书签法优化
                            List<Excel> excels = excelMapper.deepPaginationSelect(lastId, pageSize);

                            lastId = excels.get(excels.size() - 1).getId();
                            log.info("LastId:{}",lastId);
                            writer1.write(excels, sheet);

                        }
                    }
                    writer1.finish();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        if (batchSize > 6) {
            easyExcelThreadPool.execute(() -> {
                try {
                    //用来写入的ExcelWriter
                    ExcelWriter writer2 = EasyExcel.write(filePath).build();
                    int first = 2*everythreadSize + 1;//作为sheet页标识,也就是批次标识
                    Integer lastId = null;
                    //多Sheet写入
                    for (int i = 0; i < everythreadSize; i++) {
                        WriteSheet sheet = EasyExcel.writerSheet("批次" + first++).build();
                        if (i == 0) {//也就是我们的第一次写入
                            PageHelper.startPage(everythreadSize*2+1,pageSize);//第一次普通的分页查询,后面要知道id,使用书签法来优化深度分页问题
                            List<Excel> excels = excelMapper.selectAllOrderById();

                            if (excels.size() > batchSize)
                                lastId = excels.get(excels.size() - 1).getId();
                            log.info("LastId:{}",lastId);
                            writer2.write(excels, sheet);

                        } else { //我们的书签法优化
                            List<Excel> excels = excelMapper.deepPaginationSelect(lastId, pageSize);
                            lastId = excels.get(excels.size() - 1).getId();
                            log.info("LastId:{}",lastId);
                            writer2.write(excels, sheet);

                        }
                    }
                    writer2.finish();
                } finally {
                    countDownLatch.countDown();
                }


            });
        }


        //主线程执行
        try {
            int first = 0;//作为sheet页标识,也就是批次标识
            Integer lastId = null;
            //多Sheet写入
            for (int i = 0; i < everythreadSize; i++) {
                WriteSheet sheet = EasyExcel.writerSheet("批次" + first++).build();
                if (i == 0) {//也就是我们的第一次写入
                    PageHelper.startPage(1, pageSize);//第一次普通的分页查询,后面要知道id,使用书签法来优化深度分页问题
                    List<Excel> excels = excelMapper.selectAllOrderById();
                    if (excels.size() > batchSize)
                        lastId = excels.get(excels.size() - 1).getId();
                    log.info("LastId:{}",lastId);
                    writer.write(excels, sheet);

                } else { //我们的书签法优化
                    List<Excel> excels = excelMapper.deepPaginationSelect(lastId, pageSize);
                    lastId = excels.get(excels.size() - 1).getId();
                    log.info("LastId:{}",lastId);
                    writer.write(excels, sheet);

                }
            }
        }
        finally {
            writer.finish();
            countDownLatch.countDown();
        }

        countDownLatch.await();

        log.info("导出结束");


    }

    @Test
    void test(){
        List<Excel> excels = excelMapper.deepPaginationSelect(100, 1000);
        System.out.println(excels);
    }


}

Mapper

package com.example.kiratest.EasyExcel.Mapper;

import com.example.kiratest.EasyExcel.pojo.Excel;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface ExcelMapper {
    @Select("select * from excel order by id")
     List<Excel> selectAllOrderById();  //查询数据根据id排序,如果这个要分页的话我们就是用我们的PageHelper

    @Select("select count(*) from excel")
    Integer countTotal();

    @Select("select * from excel where id >#{lastMaxId} order by id limit #{batchsize} ")
    List<Excel> deepPaginationSelect(Integer lastMaxId,Integer batchsize); //深度分页通过书签法解决

}

POJO类

package com.example.kiratest.EasyExcel.pojo;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ColumnWidth(20)
public class Excel {

    @ExcelIgnore
    private Integer id;

    @ExcelProperty(value = "用户姓名",index = 0)
    private String name;

    @ExcelProperty(value = "用户Id",index = 1)
    private String uesrId;

    @ExcelProperty(value = "居住地址",index = 2)
    private String location;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date createTime;
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.dtcms.com/a/120155.html

相关文章:

  • 【每日随笔】目标制定的 “ 降维哲学 “ ( 目标过高引发 “ 行动力损耗 “ | 目标过低引发 “ 结果递减 “ | 目标制定最佳策略 )
  • 【Java设计模式】第2章 UML急速入门
  • #MongoDB 快速上手
  • swift-08-属性、汇编分析inout本质
  • StarRocks 助力首汽约车精细化运营
  • react实现上传图片到阿里云OSS以及问题解决(保姆级)
  • Spring中使用Kafka的详细配置,以及如何集成 KRaft 模式的 Kafka
  • 数据结构和算法(十二)--最小生成树
  • 做好一个测试开发工程师第二阶段:java入门:idea新建一个project后默认生成的.idea/src/out文件文件夹代表什么意思?
  • 基于开源AI大模型AI智能名片S2B2C商城小程序,探究私域电商中人格域积累与直播电商发展
  • 每日算法-250408
  • 使用Java多线程和POI进行Elasticsearch大批量数据导出
  • linux开发环境
  • 物联网外设管理服务平台
  • 吊舱的陀螺稳定系统技术要点!
  • java设计模式-建造者模式
  • 【算法竞赛】树上最长公共路径前缀(蓝桥杯2024真题·团建·超详细解析)
  • 【家政平台开发(27)】商务部信用对接、法律咨询与视频面试功能开发全攻略
  • ADI的BF561双核DSP怎么做开发,我来说一说(六)IDE硬盘设计
  • EasyExcel实现图片导出功能(记录)
  • OpenHarmony-AI调研
  • Proximal Policy Optimization (PPO)2017
  • MySQL详解最新的官方备份方式Clone Plugin
  • 【机器学习】决策树
  • Java的JDK、JRE、JVM关系与作用
  • 【Axure元件分享】移动端滑动拨盘日期选择器
  • WHAT - React 惰性初始化
  • Qwen - 14B 怎么实现本地部署,权重参数大小:21GB
  • 快速上手Vue3国际化 (i18n)
  • DeepSeek和文心一言的区别