多线程导出excel (千万级数据导出)

多线程导出excel

公司项目需求,数据库有上千万条数据需要导出excel,使用EasyExcel导出,数据量不大时,没什么问题,但数据量超过上百网时,mysql出现连接超时,,虚拟机内存也会出现问题,后来考虑使用多线程分批导出多个excel,再把多个excel压缩成zip包发送到浏览器,这里每批次可处理100000条数据,大概两到三分钟执行完,五万条每批次大概三到四分钟,具体根据java虚拟机情况测试

配置线程池

项目使用springboot框架,所以线程池也是用springboot配置

@Configuration
@EnableAsync
public class TaskPoolConfig {

    @Bean("taskExecutor")
    public Executor taskExecutro(){
        int i = Runtime.getRuntime().availableProcessors();
        System.out.println("系统最大线程数  : "+i);
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(i);
        taskExecutor.setMaxPoolSize(i);
        taskExecutor.setQueueCapacity(99999);
        taskExecutor.setKeepAliveSeconds(60);
        taskExecutor.setThreadNamePrefix("taskExecutor--");
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        taskExecutor.setAwaitTerminationSeconds(60);
        return taskExecutor;
    }
}

定义异步执行方法

 /**
     * 分批次异步导出数据
     * @param map 要导出的批次数据信息
     * @param cdl countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
     */
    @Async("taskExecutor")
    public void executeAsyncTask(Map<String, Object> map, CountDownLatch cdl) {
        long start = System.currentTimeMillis();
        // 导出文件路径
        List<CodeReceptionList> list = null;
        try {
            PageUtil.pageUtil(map);
            //查询要导出的批次数据
            list = codeReceptionListDao.queryAllByLimit(map);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 写法1
        String filepath = map.get("path").toString() + map.get("page") + ".xlsx";
        TestFileUtil.createFile(filepath);
        // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(filepath, CodeReceptionList.class).sheet("模板").doWrite(list);
        long end = System.currentTimeMillis();
        System.out.println("线程:" + Thread.currentThread().getName() + " , 导出excel   " + map.get("page") + ".xlsx   成功 , 导出数据 " + list.size() + " ,耗时 :" + (end - start));
        list.clear();
        //执行完毕线程数减一
        cdl.countDown();
        System.out.println("剩余任务数  ===========================> " + cdl.getCount());
        
    }

service层

这里使用mybatis操作数据库,先查询出数据库有多少数据,并处理每批次的分页信息存入队列,然后每从队列取出一个批次信息开启一个线程,调用异步导出方法,CountDownLatch记录任务执行完毕后对文件目录压缩成zip并发送到浏览器,这里从网上找了个zip压缩工具类

@Service
@Slf4j
public class TestService {
    /**
     *每批次处理的数据量
     */
    private static  final int LIMIT = 100000;

    @Resource
    private CodeReceptionListDao codeReceptionListDao;

    public static Queue<Map<String, Object>> queue;//Queue是java自己的队列,具体可看API,是同步安全的

    static {
        queue = new ConcurrentLinkedQueue<Map<String, Object>>();
    }

    private String filePath = "/Users/fishfly/localFile/excel/";

    @Resource
    private AsyncTaskService asyncTaskService;

    /**
     * 初始化队列
     */
    public void initQueue() {
        // 设置数据
        //long count = codeReceptionListDao.count(new HashMap<>());
        long listCount = 5000000;
        int listCount1 = (int) listCount;
        //导出6万以上数据。。。
        int count = listCount1 / LIMIT + (listCount1 % LIMIT > 0 ? 1 : 0);//循环次数
        for (int i = 1; i <= count; i++) {
            Map<String, Object> map = new HashMap<>();
            map.put("page", i);
            map.put("limit", LIMIT);
            map.put("path", filePath);
            //添加元素
            queue.offer(map);
        }
    }

    /**
     * 多线程批量导出 excel
     * @param response 用于浏览器下载
     * @throws InterruptedException
     */
    public void threadExcel(HttpServletResponse response) throws InterruptedException {
        initQueue();
        long start = System.currentTimeMillis();
        //异步转同步,等待所有线程都执行完毕返会 主线程才会结束
        try {
            CountDownLatch cdl = new CountDownLatch(queue.size());
            while (queue.size() > 0) {
                asyncTaskService.executeAsyncTask(queue.poll(), cdl);
            }
            cdl.await();

            //压缩文件
            File zipFile = new File(filePath.substring(0, filePath.length() - 1) + ".zip");
            FileOutputStream fos1 = new FileOutputStream(zipFile);
            //压缩文件目录
            ZipUtils.toZip(filePath, fos1, true);
            //发送zip包
            ZipUtils.sendZip(response, zipFile);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("任务执行完毕       共消耗   :  " + (end - start) / 1000 / 60 + "  分钟");
    }
}

zip压缩工具

@Component
@Slf4j
public class ZipUtils {
    private static final int BUFFER_SIZE = 2 * 1024;

    /**
     * 压缩成ZIP 方法     * @param srcDir 压缩文件夹路径
     *
     * @param out              压缩文件输出流
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure)
            throws RuntimeException {
        log.info("正在压缩文件。。。");
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
            File sourceFile = new File(srcDir);
            compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
            long end = System.currentTimeMillis();
            log.info("压缩完成,耗时:" + (end - start) + " ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 压缩成ZIP 方法     * @param srcFiles 需要压缩的文件列表
     *
     * @param out 压缩文件输出流
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(List<File> srcFiles, OutputStream out) throws RuntimeException {
        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
            for (File srcFile : srcFiles) {
                byte[] buf = new byte[BUFFER_SIZE];
                zos.putNextEntry(new ZipEntry(srcFile.getName()));
                int len;
                FileInputStream in = new FileInputStream(srcFile);
                while ((len = in.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
                zos.closeEntry();
                in.close();
            }
            long end = System.currentTimeMillis();
            System.out.println("压缩完成,耗时:" + (end - start) + " ms");
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws Exception
     */
    private static void compress(File sourceFile, ZipOutputStream zos, String name,
                                 boolean KeepDirStructure) throws Exception {
        byte[] buf = new byte[BUFFER_SIZE];
        if (sourceFile.isFile()) {
            // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            // copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);
            while ((len = in.read(buf)) != -1) {
                zos.write(buf, 0, len);
            }
            // Complete the entry
            zos.closeEntry();
            in.close();
        } else {
            //是文件夹
            File[] listFiles = sourceFile.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if (KeepDirStructure) {
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + "/"));
                    // 没有文件,不需要文件的copy
                    zos.closeEntry();
                }
            } else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    if (KeepDirStructure) {
                        // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
                        compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
                    } else {
                        compress(file, zos, file.getName(), KeepDirStructure);
                    }

                }
            }
        }
    }

    /**
     * 向浏览器发送zip包
     *
     * @param response
     */
    public static void sendZip(HttpServletResponse response, File zipFile) {
        log.info("正在发送zip包");
        OutputStream outputStream = null;
        BufferedInputStream fis = null;
        try {
            // 以流的形式下载文件。
            fis = new BufferedInputStream(new FileInputStream(zipFile.getPath()));
            byte[] buffer = new byte[fis.available()];
            fis.read(buffer);
            // 清空response
            response.reset();
            outputStream = new BufferedOutputStream(response.getOutputStream());
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment;filename=" + new String(zipFile.getName().getBytes("UTF-8"), "ISO-8859-1"));
            outputStream.write(buffer);
            outputStream.flush();
            log.info("发送成功。");
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            try {
                if (fis != null) { fis.close(); }
                if (outputStream != null) { outputStream.close(); }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  • 18
    点赞
  • 138
    收藏
    觉得还不错? 一键收藏
  • 60
    评论
以下是Java多线程千万级数据导出的示例代码: ```java import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.metadata.Sheet; import com.alibaba.excel.support.ExcelTypeEnum; public class ExcelExportUtil { /** * 导出Excel * * @param dataList 数据列表 * @param sheetName sheet名称 * @param filePath 文件路径 * @param fileName 文件名 * @param sheetSize 每个sheet中数据的行数 */ public static void exportExcel(List<List<Object>> dataList, String sheetName, String filePath, String fileName, int sheetSize) { // 创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建ExcelWriter ExcelWriter excelWriter = new ExcelWriter(new File(filePath + fileName), ExcelTypeEnum.XLSX); // 计算需要创建的sheet数量 int sheetCount = dataList.size() % sheetSize == 0 ? dataList.size() / sheetSize : dataList.size() / sheetSize + 1; // 创建计数器 CountDownLatch countDownLatch = new CountDownLatch(sheetCount); // 创建sheet for (int i = 0; i < sheetCount; i++) { // 计算每个sheet中数据的起始位置和结束位置 int startIndex = i * sheetSize; int endIndex = Math.min(startIndex + sheetSize, dataList.size()); // 创建sheet Sheet sheet = new Sheet(i + 1, 0); sheet.setSheetName(sheetName + (i + 1)); // 创建数据列表 List<List<Object>> sheetDataList = new ArrayList<>(); for (int j = startIndex; j < endIndex; j++) { sheetDataList.add(dataList.get(j)); } // 将sheet数据提交到线程池中进行处理 executorService.submit(new ExcelExportTask(excelWriter, sheet, sheetDataList, countDownLatch)); } try { // 等待所有线程处理完 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 关闭ExcelWriter和线程池 excelWriter.finish(); executorService.shutdown(); } } } ``` 其中,ExcelExportTask是一个实现了Runnable接口的任务类,用于将数据写入Excel中。具体代码如下: ```java import java.util.List; import java.util.concurrent.CountDownLatch; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.metadata.Sheet; public class ExcelExportTask implements Runnable { private ExcelWriter excelWriter; private Sheet sheet; private List<List<Object>> dataList; private CountDownLatch countDownLatch; public ExcelExportTask(ExcelWriter excelWriter, Sheet sheet, List<List<Object>> dataList, CountDownLatch countDownLatch) { this.excelWriter = excelWriter; this.sheet = sheet; this.dataList = dataList; this.countDownLatch = countDownLatch; } @Override public void run() { try { // 写入数据 excelWriter.write0(dataList, sheet); } finally { // 计数器减1 countDownLatch.countDown(); } } } ``` 使用示例: ```java import java.util.ArrayList; import java.util.List; public class ExcelExportTest { public static void main(String[] args) { // 模拟数据 List<List<Object>> dataList = new ArrayList<>(); for (int i = 0; i < 10000000; i++) { List<Object> row = new ArrayList<>(); row.add("数据" + i); dataList.add(row); } // 导出Excel ExcelExportUtil.exportExcel(dataList, "Sheet1", "D:/", "data.xlsx", 1000000); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 60
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值