SpringBoot实现百万级数据高效导出Excel和CSV实战

SpringBoot实现百万级数据高效导出Excel和CSV实战

你是否曾因为导出大量数据而导致系统响应缓慢甚至崩溃?用户抱怨导出功能卡顿,服务器CPU飙升,内存溢出?今天我就来分享一套完整的解决方案,让你轻松应对百万级数据导出的挑战!

一、为什么传统导出方式会崩溃?

在传统的数据导出实现中,我们通常会一次性将所有数据加载到内存中,然后再写入文件。这种方式在数据量较小时还能接受,但当数据达到百万级别时,就会暴露出严重的问题:

  1. 内存溢出:一次性加载百万条记录到内存,很容易超出JVM堆内存限制
  2. 响应时间长:用户需要等待很长时间才能获得导出结果
  3. 系统资源占用高:大量占用CPU和内存资源,影响其他功能正常使用
  4. 用户体验差:浏览器可能因等待时间过长而超时

二、高效导出的核心思路

要解决这些问题,我们需要采用分批处理流式写入的策略:

  1. 分批查询:每次只从数据库查询固定数量的记录
  2. 流式写入:边查询边写入文件,避免数据堆积在内存中
  3. 异步处理:对于大数据量导出,采用异步方式处理,避免阻塞主线程

三、技术选型对比

3.1 CSV vs Excel

特性CSVExcel
文件大小
处理速度
内存占用
功能丰富度简单丰富

3.2 导出工具选型

  1. CSV导出:使用Java原生IO流即可
  2. Excel导出:推荐使用阿里巴巴开源的EasyExcel

四、核心实现代码

4.1 CSV导出实现

@Service
public class CSVExportService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Value("${data.export.batch-size}")
    private int batchSize;
    
    public String exportUsersToCSV() throws IOException {
        // 生成文件名
        String fileName = "users_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".csv";
        String fullPath = filePath + fileName;
        
        // 创建文件写入器
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fullPath))) {
            // 写入CSV头部
            writer.write("ID,用户名,邮箱,手机号,年龄,地址,创建时间,更新时间");
            writer.newLine();
            
            // 分批查询并写入数据
            int offset = 0;
            int totalCount = userMapper.selectUserCount();
            
            while (offset < totalCount) {
                List<User> users = userMapper.selectUsersByPage(offset, batchSize);
                
                for (User user : users) {
                    // 写入一行数据
                    writer.write(formatUserToCSV(user));
                    writer.newLine();
                }
                
                offset += batchSize;
            }
            
            return fullPath;
        }
    }
}

4.2 Excel导出实现(基于EasyExcel)

@Service
public class ExcelExportService {
    
    public String exportUsersToExcel() {
        // 生成文件名
        String fileName = "users_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
        String fullPath = filePath + fileName;
        
        // 使用EasyExcel进行导出
        ExcelWriter excelWriter = null;
        try {
            excelWriter = EasyExcel.write(fullPath, User.class).build();
            WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
            
            // 分批查询并写入数据
            int offset = 0;
            int totalCount = userMapper.selectUserCount();
            
            while (offset < totalCount) {
                List<User> users = userMapper.selectUsersByPage(offset, batchSize);
                
                // 写入一批数据
                excelWriter.write(users, writeSheet);
                
                offset += batchSize;
            }
            
            return fullPath;
        } finally {
            // 关闭流
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }
}

4.3 异步导出实现

@RestController
@RequestMapping("/export")
public class ExportController {
    
    @Autowired
    private ExportTaskService exportTaskService;
    
    /**
     * 异步导出CSV文件
     */
    @GetMapping("/csv/async")
    public String exportCSVAsync() {
        String taskId = UUID.randomUUID().toString();
        exportTaskService.exportCSVAsync(taskId);
        return "CSV异步导出任务已提交,任务ID: " + taskId;
    }
    
    /**
     * 异步导出Excel文件
     */
    @GetMapping("/excel/async")
    public String exportExcelAsync() {
        String taskId = UUID.randomUUID().toString();
        exportTaskService.exportExcelAsync(taskId);
        return "Excel异步导出任务已提交,任务ID: " + taskId;
    }
}

@Service
public class ExportTaskService {
    
    @Async
    public void exportCSVAsync(String taskId) {
        try {
            String filePath = csvExportService.exportUsersToCSV();
            // 可以将结果存储到数据库或Redis中,供前端查询
        } catch (IOException e) {
            log.error("CSV导出任务失败,任务ID: {}", taskId, e);
        }
    }
    
    @Async
    public void exportExcelAsync(String taskId) {
        try {
            String filePath = excelExportService.exportUsersToExcel();
            // 可以将结果存储到数据库或Redis中,供前端查询
        } catch (Exception e) {
            log.error("Excel导出任务失败,任务ID: {}", taskId, e);
        }
    }
}

五、性能优化要点

5.1 数据库查询优化

  1. 合理设置批次大小:建议设置为1000-5000条记录
  2. 添加索引:确保查询字段有合适的索引
  3. 避免全表扫描:使用LIMIT子句分页查询

5.2 内存优化

  1. 及时释放资源:使用try-with-resources确保流正确关闭
  2. 避免对象堆积:每批数据处理完后,让JVM及时回收
  3. 调整JVM参数:适当增加堆内存大小

5.3 文件存储优化

  1. 临时文件清理:定期清理过期的导出文件
  2. 文件分片存储:对于超大文件可以考虑分片存储
  3. CDN加速:将导出文件存储到CDN上,提高下载速度

六、异常处理与监控

6.1 异常处理

@Async
public void exportCSVAsync(String taskId) {
    try {
        String filePath = csvExportService.exportUsersToCSV();
        log.info("CSV导出任务完成,任务ID: {},文件路径: {}", taskId, filePath);
        // 更新任务状态为成功
    } catch (IOException e) {
        log.error("CSV导出任务失败,任务ID: {}", taskId, e);
        // 更新任务状态为失败
    } catch (Exception e) {
        log.error("CSV导出任务未知异常,任务ID: {}", taskId, e);
        // 更新任务状态为异常
    }
}

6.2 监控指标

  1. 导出任务成功率
  2. 平均导出耗时
  3. 内存使用情况
  4. 数据库查询性能

七、最佳实践建议

  1. 合理设置批次大小:根据实际测试结果调整batchSize参数
  2. 添加进度提示:对于长时间运行的任务,提供进度反馈
  3. 限制导出数据量:避免用户一次性导出过多数据
  4. 使用异步处理:大数据量导出务必使用异步方式
  5. 文件压缩:对于大文件可以考虑ZIP压缩后再提供下载
  6. 权限控制:确保只有授权用户才能执行导出操作

八、总结

通过分批处理、流式写入和异步处理这三大核心技术,我们可以轻松应对百万级数据的导出需求。相比传统的全量加载方式,这种方案具有以下优势:

  1. 内存占用低:始终只在内存中保留少量数据
  2. 响应速度快:异步处理不会阻塞用户操作
  3. 系统稳定性好:避免了内存溢出和系统崩溃的风险
  4. 用户体验佳:用户可以继续其他操作,导出完成后通知用户

掌握了这套方案,再也不用担心大数据量导出带来的各种问题了。赶紧在你的项目中试试吧!


关注我,获取更多后端技术干货!


标题:SpringBoot实现百万级数据高效导出Excel和CSV实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304290456.html

    0 评论
avatar