尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MyBatis 整合 EasyExcel 实现百万数据导出实战

MyBatis 整合 EasyExcel 实现百万数据导出实战 1. 引言在业务系统中数据导出是高频需求。当数据量达到百万级别时传统的 POI 直接导出方式往往会面临内存溢出OOM或导出耗时过长的问题。EasyExcel 是阿里巴巴开源的一款基于 SAX 模式解析的 Excel 工具它通过流式读写大幅降低了内存占用配合 MyBatis 的游标查询Cursor可以轻松实现百万级数据的平滑导出。本文将带你从零开始完成 MyBatis 与 EasyExcel 的整合实现百万数据的高效导出并给出完整的代码示例与性能优化建议。2. 技术选型与核心思路2.1 为什么选择 EasyExcel低内存占用EasyExcel 采用 SAX 模式逐行读写不将整个 Excel 加载到内存。API 简洁提供注解模型几行代码即可完成导出。社区活跃阿里巴巴开源文档完善遇到问题容易排查。2.2 为什么配合 MyBatis 游标查询MyBatis 3.4.0 及以上版本支持CursorT返回值。游标查询不会一次性将全部结果加载到内存而是按需从数据库逐条获取配合 EasyExcel 的流式写入形成「边查边写」的流水线避免百万数据同时驻留内存。2.3 整体流程前端发起导出请求创建 EasyExcel 写入器MyBatis 游标查询数据库逐条写入 Excel关闭游标与写入器返回下载链接/文件3. 环境准备3.1 依赖引入以 Spring Boot 2.7 MyBatis 3.5 为例在pom.xml中添加依赖!-- MyBatis Spring Boot Starter --dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.3.1/version/dependency!-- EasyExcel --dependencygroupIdcom.alibaba/groupIdartifactIdeasyexcel/artifactIdversion3.3.2/version/dependency!-- MySQL 驱动 --dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-java/artifactIdversion8.0.33/version/dependency3.2 数据库准备以用户表为例建表语句如下CREATETABLEt_user(idbigint(20)NOTNULLAUTO_INCREMENT,namevarchar(50)NOTNULL,phonevarchar(20)DEFAULTNULL,emailvarchar(100)DEFAULTNULL,create_timedatetimeDEFAULTNULL,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8mb4;4. 代码实现4.1 定义导出模型使用 EasyExcel 注解定义导出列importcom.alibaba.excel.annotation.ExcelProperty;importlombok.Data;importjava.util.Date;DatapublicclassUserExportModel{ExcelProperty(用户ID)privateLongid;ExcelProperty(姓名)privateStringname;ExcelProperty(手机号)privateStringphone;ExcelProperty(邮箱)privateStringemail;ExcelProperty(创建时间)privateDatecreateTime;}4.2 Mapper 层游标查询在 Mapper 接口中定义返回CursorT的查询方法importorg.apache.ibatis.cursor.Cursor;importorg.apache.ibatis.annotations.Mapper;importorg.apache.ibatis.annotations.Select;MapperpublicinterfaceUserMapper{Select(SELECT id, name, phone, email, create_time FROM t_user)CursorUserExportModelselectAllForExport();}注意使用游标查询时必须保持数据库连接处于开启状态直到游标遍历完毕。因此查询和写入必须在同一个事务或同一个 SqlSession 内完成。4.3 Service 层实现导出importcom.alibaba.excel.EasyExcel;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.net.URLEncoder;ServicepublicclassUserExportService{AutowiredprivateUserMapperuserMapper;Transactional(readOnlytrue)publicvoidexportMillionUsers(HttpServletResponseresponse)throwsIOException{// 设置响应头response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet);response.setCharacterEncoding(utf-8);StringfileNameURLEncoder.encode(百万用户数据,UTF-8).replaceAll(\\,%20);response.setHeader(Content-disposition,attachment;filename*utf-8fileName.xlsx);// 使用 try-with-resources 确保游标和写入器正确关闭try(CursorUserExportModelcursoruserMapper.selectAllForExport()){EasyExcel.write(response.getOutputStream(),UserExportModel.class).sheet(用户数据).doWrite(()-cursor);}}}4.4 Controller 层接口importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;RestControllerRequestMapping(/api/export)publicclassUserExportController{AutowiredprivateUserExportServiceuserExportService;GetMapping(/users)publicvoidexportUsers(HttpServletResponseresponse)throwsIOException{userExportService.exportMillionUsers(response);}}5. 关键点解析5.1 为什么必须加Transactional游标查询依赖底层 JDBC 连接保持打开。如果不加事务MyBatis 在查询结束后可能立即关闭连接导致游标无法继续读取。加上Transactional(readOnly true)可以保证整个导出过程中连接不释放。5.2 游标查询的 fetchSize 优化对于 MySQL可以在 Mapper 中设置fetchSize为Integer.MIN_VALUE让驱动使用流式读取Select(SELECT id, name, phone, email, create_time FROM t_user)Options(fetchSizeInteger.MIN_VALUE)CursorUserExportModelselectAllForExport();5.3 分批写入与内存控制EasyExcel 默认每 100 条写入一次可通过inMemory参数控制EasyExcel.write(response.getOutputStream(),UserExportModel.class).inMemory(false)// 使用临时文件降低内存占用.sheet(用户数据).doWrite(()-cursor);6. 性能对比与优化建议6.1 性能对比方案内存占用导出 100 万条耗时参考适用场景POI 一次性加载极高易 OOM60s小数据量POI 分批查询中等40s中等数据量MyBatis 游标 EasyExcel低20s~30s百万级数据6.2 优化建议关闭自动提交在导出过程中关闭事务自动提交减少磁盘 IO。合理设置 fetchSize根据数据库类型调整MySQL 使用Integer.MIN_VALUE触发流式读取。异步导出百万数据导出耗时较长建议改为异步任务导出完成后通知用户下载。限制导出条件如果业务允许尽量通过时间范围等条件分批导出降低单次压力。7. 常见问题排查7.1 导出时提示 “Streaming result set … is still active”这是因为游标未关闭或连接被提前释放。检查是否添加了Transactional以及是否使用了 try-with-resources 正确关闭游标。7.2 导出文件为空检查 Mapper 查询是否返回了数据以及 EasyExcel 的模型字段与查询列是否对应。7.3 内存仍然很高确认是否设置了inMemory(false)并检查是否误用了List接收查询结果应使用Cursor。8. 总结通过 MyBatis 游标查询与 EasyExcel 流式写入的结合我们可以在较低的内存占用下完成百万级数据的 Excel 导出。核心要点有三个使用CursorT而非ListT避免全量加载到内存。保持数据库连接不释放通过Transactional保证游标可用。EasyExcel 流式写入边读边写形成流水线。希望本文能帮助你解决大数据量导出的难题。如果你在实际项目中遇到了其他问题欢迎在评论区交流讨论。
返回列表