大数据(百万)导出到excel

一.分析

1.大数据量,最好使用多线程导出提升效率。
2.多线程导出,需要考虑线程安全。
3.使用分页查询,防止发生内存溢出。
4.需要查询数据总量进行分页。

二.代码实现

1.springboot + swagger2 + easyexcel

使用到了spring的多线程,所以在启动类添加注解@EnableAsync,在需要多线程执行的方法上添加注解@Async

2.pom文件

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.5.0</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.0</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 阿里开源EXCEL -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.2.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.14</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>

        <!-- spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 表示依赖不会传递 -->
            <version>2.1.1.RELEASE</version>
        </dependency>

3.代码
配置类

@Configuration
public class ThreadPoolConfig {

    //配置线程池
    @Bean("threadPool")
    public ThreadPoolTaskExecutor threadPoolExecutor() {
        ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
        //核心线程数  Runtime.getRuntime().availableProcessors()获取CPU核数
        int coreSize = Runtime.getRuntime().availableProcessors();
        threadPoolExecutor.setCorePoolSize(coreSize);
        //最大线程数
        threadPoolExecutor.setMaxPoolSize(coreSize);
        //队列容量 4倍线程数
        threadPoolExecutor.setQueueCapacity(coreSize * 4);
        //线程池对拒绝任务(无线程可用)的处理策略
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return threadPoolExecutor;
    }
}
@Configuration
@EnableSwagger2
public class SwaggerConfig {


    /**
     * 创建API
     */
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .pathMapping("/")
                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
                .apiInfo(apiInfo())
                // 设置哪些接口暴露给Swagger展示
                .select()
                // 扫描所有有注解的api,用这种方式更灵活
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                // 扫描指定包中的swagger注解
                //.apis(RequestHandlerSelectors.basePackage("com.enn.security.project.tool.swagger"))
                // 扫描所有 .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 添加摘要信息
     */
    private ApiInfo apiInfo() {
        // 用ApiInfoBuilder进行定制
        return new ApiInfoBuilder()
                // 设置标题
                .title("标题:导出测试")
                // 描述
                .description("描述:导出测试.")
                // 作者信息
                .contact(new Contact("export", null, null))
                // 版本
                .version("版本号:" + "1.0.1")
                .build();
    }

    @Bean
    public Docket exportApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.ant("/export/**"))
                .build()
                .groupName("01-导出")
                .pathMapping("/");
    }
}

工具类
导出工具类

@Getter
public class ExportExcelUtil {

    private static final Logger log = LoggerFactory.getLogger(ExportExcelUtil.class);

    /**
     * 数据总量
     */
    private final Integer dataCount;

    /**
     * 每次从数据库查询数据,防止内存溢出
     */
    private final int limit;

    /**
     * 每个sheet总数据
     */
    private final int sheetCount;

    // Queue是java自己的队列,是同步安全的
    public Queue<Map<String, Integer>> queue;

    // 计数器所以任务都处理完才返回下载地址
    private CountDownLatch countDownLatch;

    public ExportExcelUtil(int dataCount, int limit, int sheetCount) {
        this.sheetCount = sheetCount;
        this.dataCount = dataCount;
        this.limit = limit;
        initQueue();
    }

    /**
     * 初始化队列
     */
    public void initQueue() {
        queue = new ConcurrentLinkedQueue<>();
        // 计算出多少页,即循环次数
        int count = dataCount / sheetCount + (dataCount % sheetCount > 0 ? 1 : 0);
        for (int i = 0; i < count; i++) {
            Map<String, Integer> map = new HashMap<>();
            //第几页
            map.put("page", i);
            //分页长度
            map.put("limit", limit);
            //每个sheet数据总量
            map.put("sheetCount", sheetCount);
            //添加元素
            queue.offer(map);
        }
        countDownLatch = new CountDownLatch(queue.size());
    }
}

压缩工具类

public class ZipFileUtil {

    /**
     * 文件压缩
     *
     * @param filePath 压缩文件路径
     * @param zipPath  zip文件路径
     */
    public static void zip(String filePath, String zipPath) throws IOException {
        // 判断文件是否存在,如文件不存在创建一个新文件
        File zipFile = new File(zipPath);
        if (!zipFile.exists()) {
            zipFile.createNewFile();
        }
        File file = new File(filePath);
        if (!file.exists()) {
            return;
        }
        // 创建一个zip文件输出流
        BufferedInputStream bufferedInput = null;
        try (ZipOutputStream zipOutput = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
            /*
              创建一个缓冲读取流,提高读取效率
              也可以直接创建一个 FileInputStream 对象,BufferedInputStream内部维护了一个8KB的缓冲区,BufferedInputStream本身不具备读取能力
              BufferedInputStream 可以手动指定缓冲区大小 单位为字节例如:new BufferedInputStream(new FileInputStream(file), 10240)
             */
            // 设置压缩条目名称
            zipOutput.putNextEntry(new ZipEntry(file.getName()));
            File[] files;
            int len;
            // 我的CPU1级缓存为256Kb
            byte[] bytes = new byte[256 * 1024];
            if (file.isFile()) {
                files = new File[]{file};
            } else {
                files = file.listFiles();
            }
            // 读取file内的字节流,写入到zipOutput内
            if (files != null) {
                for (File file1 : files) {
                    bufferedInput = new BufferedInputStream(Files.newInputStream(file1.toPath()));
                    while ((len = bufferedInput.read(bytes)) != -1) {
                        zipOutput.write(bytes, 0, len);
                    }
                    // 写入完毕后关闭条目
                    //zipOutput.closeEntry();
                }
            }
        } finally {
            if (bufferedInput != null) {
                bufferedInput.close();
            }
        }
    }
}

实体类

@Data
public class SecurityMapDetail {
    @ExcelProperty("id")
    private Long id;
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty("统计日期")
    private Date calDay;
    @ExcelProperty("省公司id")
    private Long xaReig;
    @ExcelProperty("省公司名称")
    private String xaReigTxt;
    @ExcelProperty("管理实体id")
    private Long cpmoCop;
    @ExcelProperty("管理实体名称")
    private String cpmoCopTxt;
    @ExcelProperty("指标名称")
    private String targetName;
    @NumberFormat("#.##%")
    @ExcelProperty("指标计算分子")
    private Double targetNumerator;
    @ExcelProperty("指标计算分母")
    @NumberFormat("#.##%")
    private Double targetDenominator;
    @ExcelProperty("指标值")
    @NumberFormat("#.##%")
    private Double targetValue;
    @ExcelProperty("排名")
    private Integer sort;
    @ExcelProperty("得分")
    @NumberFormat("#.##%")
    private Double score;
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty("记录生成时间")
    private Date createTime;
}

Controller

@RestController
@RequestMapping("/export")
public class Controller {

    private static final Logger log = LoggerFactory.getLogger(Controller.class);

    @Autowired
    private ISecurityMapDetailService securityMapDetailService;

    @GetMapping("/excelAliExcel")
    public String excelAliExcel(SecurityMapDetail securityMapDetail, HttpServletResponse response) {
        long start = System.currentTimeMillis();
        //返回数据总量
        int dataCount = securityMapDetailService.getDataCount(securityMapDetail);
        //初始化导出工具类
        ExportExcelUtil excelUtil = new ExportExcelUtil(dataCount, 200000, 200000);
        String filePath = "E:\\path\\excel\\" + UUID.randomUUID() + "\\";
        String fileZipPath = "E:\\path\\zip\\excel\\" + UUID.randomUUID() + ".zip";
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                file.mkdirs();
            }
            //使用 CountDownLatch保证最后全部导出压缩文件,返回下载地址
            CountDownLatch countDownLatch = excelUtil.getCountDownLatch();
            Queue<Map<String, Integer>> queue = excelUtil.getQueue();
            Map<String, Integer> poll;
            //循环获取任务,发生给线程池执行
            while ((poll = queue.poll()) != null) {
                securityMapDetailService.excelAliExcel(securityMapDetail, poll, countDownLatch, filePath);
            }
            countDownLatch.await();
            //压缩

            long end = System.currentTimeMillis();
            System.out.println("任务执行完毕共消耗:  " + (end - start) + "ms");
            // 下载EXCEL
            ZipFileUtil.zip(filePath, fileZipPath);
        } catch (InterruptedException e) {
            log.error("导出异常:{}", e.getMessage());
        } catch (IOException e) {
            log.error("压缩异常:{}", e.getMessage());
        }
        return fileZipPath;
    }
}

Service

public interface ISecurityMapDetailService {

    int getDataCount(SecurityMapDetail securityMapDetail);

    void excelAliExcel(SecurityMapDetail securityMapDetail, Map<String, Integer> poll, CountDownLatch countDownLatch, String filePath);
}
@Service
public class SecurityMapDetailServiceImpl implements ISecurityMapDetailService {

    private static final Logger log = LoggerFactory.getLogger(SecurityMapDetailServiceImpl.class);

    @Autowired
    private SecurityMapDetailMapper mapDetailMapper;

    @Override
    public int getDataCount(SecurityMapDetail securityMapDetail) {
        return mapDetailMapper.selectSecurityMapDetailCount(securityMapDetail);
    }

    @Override
    @Async("threadPool")
    public void excelAliExcel(SecurityMapDetail securityMapDetail, Map<String, Integer> poll, CountDownLatch countDownLatch, String filePath) {
        Integer page = poll.get("page");
        Integer limit = poll.get("limit");
        Integer sheetCount = poll.get("sheetCount");
        int count = sheetCount / limit + page;
        List<SecurityMapDetail> data;
        String fileName = page + ".xlsx";
        try (ExcelWriter excelWriter = EasyExcel.write(filePath + fileName, SecurityMapDetail.class).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet(page, "模板" + page).build();
            for (; page < count; page++) {
                // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                data = mapDetailMapper.selectSecurityMapDetailList(securityMapDetail, page * limit, limit);
                excelWriter.write(data, writeSheet);
            }
        } finally {
            countDownLatch.countDown();
        }
    }
}

启动类

@EnableAsync
@SpringBootApplication
public class SpringExexportExcelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringExexportExcelApplication.class, args);
    }
}

结果

执行时间为111秒,数据库中一条数据约为380b,导出190W+的数据。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结

如果不需要分页查询,可以一次性查询出来,减少了查询次数,效率应该会更高。
如果使用ForkJoin是不是会效率更高,还没试过,下次试试。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值