添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

场景:项目中业务需要导出大批量数据,大到4,50w的数据到excel中,将需求写成功能

  • 大批量数据一次性查询出较为耗时,需要对表结构设计要求高方可提升查询速度。
  • 内存溢出,IO异常,一次性几十万的数据读取和写入很大,并发如果上来的场景下可能导致系统接口挂掉。
  • 接口设计响应速度考虑

实现思路:

  • 以异步接口的形式完成导出的需求功能,将导出业务分为两个过程:1.申请导出、2.下载导出文件。
  • 文件分离设计,单个文件数据量最大为10w,总文件数为:n/n + n%n>0?1:0。
  • 即使对文件做分离处理依旧无法解决IO溢出问题,对单个文件数据做边查边写处理,通过手动分页设置偏移量的形式,对单个文件的数据做分页处理。

老规矩直接上代码,注释写的比较清晰,评论区交流见解和问题
接口层代码:

* excel导出记录(异步) * @param request * @return @PostMapping ( "/export" ) public Rest < BaseResponse > export ( @RequestBody exportRequest request ) { Long accountId = ContextHolder . currentAccountId ( ) ; // 获取锁 RBucket < Integer > bucket = redissonClient . getBucket ( "EXPORT:LOCK:" + accountId ) ; if ( bucket . isExists ( ) ) { // 抛出自定义异常,提醒导出请求在执行中, // 抛出异常方式跟随各自公司习惯,本文中的自定义异常均使用RuntimeException代替 throw new RuntimeException ( ) ; bucket . set ( 1 , 5 , TimeUnit . MINUTES ) ; // 计算总数,在操作io之前校验查询结果数据是否合理 long count = 0 ; try { count = PageHelper . count ( ( ) -> exportRecordService . exportList ( request ) ) ; } catch ( Exception e ) { bucket . delete ( ) ; log . error ( "统计数量sql出错" , e ) ; throw new RuntimeException ( ) ; if ( count <= 0 ) { bucket . delete ( ) ; // 数据为零不满足导出条件,抛出自定义异常 throw new RuntimeException ( ) ; // 计算查询分片数量,方法会在exportRecordService中展示 int shardNum = exportRecordService . getShardNum ( ( int ) count , 100000 ) ; // 例:2022-01-01 10:00:00~2022-01-02 11:00:00-数据记录-shardNum-1...3 // 预处理文件属性,设置文件名每批次后缀加上"-数量"用作标识 String title = DATE_TIME_FORMATTER . format ( request . getStartTime ( ) ) + "~" + DATE_TIME_FORMATTER . format ( request . getEndTime ( ) ) + "-采样记录" + "-" + shardNum + "-" ; List < ExportDTO > fileDTOList = new ArrayList < > ( ) ; for ( int i = 1 ; i <= shardNum ; i ++ ) { String fileName = title + i ; ExportDTO fileDTO = new ExportDTO ( ) . setFileName ( fileName ) . setFileNum ( i ) ; // 计算单个文件循环查询次数 每个文件100000条,每次查询2000条导出到excel,最大50 int pageNum = 50 ; if ( i == shardNum && count % 100000 > 0 ) { pageNum = samplingRecordService . getShardNum ( Math . toIntExact ( count % 100000 ) , 2000 ) ; fileDTO . setPageNum ( pageNum ) ; fileDTOList . add ( fileDTO ) ; // 因为是异步处理service,使用父子线程交互的ThreadLocal,传递上下文用户属性ID // 用于在需要的时候删除锁 InheritableThreadLocal < Long > threadLocal = new InheritableThreadLocal < > ( ) ; threadLocal . set ( accountId ) ; // 将request查询条件透传过去 exportRecordService . exportList ( request , fileDTOList , threadLocal ) ; return Rest . success ( ) ;

service层代码:

	@Async
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void exportList(ExportRequest request, List<ExportDTO> fileDTOList, InheritableThreadLocal<Long> threadLocal) {
        long startTime = System.currentTimeMillis();
        RBucket<Integer> bucket = redissonClient.getBucket("EXPORT:LOCK:" + threadLocal.get());




    

        for (ExportDTO fileDTO : fileDTOList) {
            // 获取target/resource/目录路径
            String dirPath = new ApplicationHome(getClass()).getSource().getParentFile().toString();
            File file = new File(dirPath + File.separator + fileDTO.getFileName() + ".xlsx");
			// 自增对象,用于记录文件中数据的序号
            AtomicInteger atomicInteger = new AtomicInteger();
            // 记录实际总数
            int num = 0;
            ExcelWriter excelWriter = null;
            try {
                // 指定class,存在表头注解则使用,不存在则使用对象字段名
                excelWriter = EasyExcel.write(file, ExportResponse.ExportVo.class).build();
                // 同一个sheet只需要创建一次
                WriteSheet writeSheet = EasyExcel.writerSheet().build();
                for (int i = 1; i <= fileDTO.getPageNum(); i++) {
                    // 分页说明:总数据分为n个文件,文件分为50次查询数据并导出,第a文件的第i次的页数则为:(a-1)*50 + i
                    PageHelper.startPage((fileDTO.getFileNum() - 1) * 50 + i, 2000, false);
                    List<ExportResponse.ExportVo> data = this.exportList(request);
                    if (CollUtil.isEmpty(data)) {
                        break;
                    for (ExportResponse.ExportVo vo : data) {
                        vo.setNo(atomicInteger.incrementAndGet());
                    excelWriter.write(data, writeSheet);
                    num += data.size();
                    PageHelper.clearPage();
                excelWriter.finish();
                log.info("{}写入完成路径:{}", fileDTO.getFileName(), file.getAbsolutePath());
                // 文件上传oss,这个就不用展开了吧
                String objectId = ossUtil.upload(file, "记录导出");
                log.info(file.getAbsolutePath() + "上传完成,objectId:{}", objectId);
                // 保存对象存储记录,存储相关联信息,操作人
                exportRecordService.save(objectId, num, threadLocal.get(), fileDTO.getFileName());
                log.info("{}保存至记录表", fileDTO.getFileName());
            } catch (Exception e) {
                log.error("记录导出出错", e);
                // 注意这是for循环,所以不在finally里面删除锁
                bucket.delete();
                throw new RuntimeException();
            } finally {
                // 千万别忘记finish 会帮忙关闭流
                if (excelWriter != null) {
                    excelWriter.finish();
            	if (file.exists()) {
                	file.delete();
        long endTime = System.currentTimeMillis();
        bucket.delete();
        log.info("耗时:{}", endTime - startTime);
    @Override
    public List<ExportResponse.ExportVo> exportList(ExportRequest request) {
        return recordMapper.exportList(request);
	// 根据总数和单片大小,计算分片数量
    @Override
    public int getShardNum(Integer total, Integer shardSize) {
        return (int) Math.ceil((double) total / (double) shardSize);
				
导入:使用Excel Streaming Reader进行海量数据读取 这个第三方工具会把一部的行(可以设置)缓存到内存中,在迭代时不断加载行到内存中,而不是一次性的加载所有记录到内存,这样就可以不断的读取excel内容并且不影响内存的使用。 但是这个工具也有一定的限制:只能用于读取excel的内容,写入操作不可用;可以使用getSheetAt()方法获取到对应的Sheet,因为当前只是加载了...
解决该问题的一个思路是使用多线程并行处理,将数据成若干块,每个线程负责导出一块数据。 以下是一个关键代码片段: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExcelExport { private final int threadCou...
SpringBoot3.x版本将swagger2.0升级到swagger3.0,使用knife4j-openapi3-jakarta-spring-boot-starter依赖 Tony666688888: springboot版本要和swagger版本配对,否则项目启动报错 swagger文档无法测试下载文件的接口解决方式 Gavin_A_Chan: apifox挺好用的呀,可以下载文件什么的,另外idea装了apifox的插件,可以一键同步controller的接口到apifox里面。 swagger文档无法测试下载文件的接口解决方式 lngg057: 你需要通过response.setContentType的方式同时指定返回类型为application/octet-stream.。然後response.getOutputStream().write就完事兒了 搞不懂『三级缓存』?与生命周期冲突?为什么代理对象二级缓存不可用?进来看看,助你掌握概念思路 奋斗中的小胖子: 那第一种有没有弊端,如果每个bean都直接生成代理放入,那不需要代理的bean??? 不需要代理的对象就不生成代理对象,把原始对象放到二级缓存不行吗 swagger文档无法测试下载文件的接口解决方式 山椒鱼摆摆: 我之前针对导出都是用postman调试或者浏览器直接访问地表情包