MyBatis 整合 EasyExcel 实现百万数据导出实战
2026/9/1 6:47:30 网站建设 项目流程

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 及以上版本支持Cursor<T>返回值。游标查询不会一次性将全部结果加载到内存,而是按需从数据库逐条获取,配合 EasyExcel 的流式写入,形成「边查边写」的流水线,避免百万数据同时驻留内存。

2.3 整体流程

前端发起导出请求

创建 EasyExcel 写入器

MyBatis 游标查询数据库

逐条写入 Excel

关闭游标与写入器

返回下载链接/文件

3. 环境准备

3.1 依赖引入

以 Spring Boot 2.7 + MyBatis 3.5 为例,在pom.xml中添加依赖:

<!-- MyBatis Spring Boot Starter --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version></dependency><!-- EasyExcel --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version></dependency><!-- MySQL 驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version></dependency>

3.2 数据库准备

以用户表为例,建表语句如下:

CREATETABLE`t_user`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`name`varchar(50)NOTNULL,`phone`varchar(20)DEFAULTNULL,`email`varchar(100)DEFAULTNULL,`create_time`datetimeDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

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 接口中定义返回Cursor<T>的查询方法:

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")Cursor<UserExportModel>selectAllForExport();}

注意:使用游标查询时,必须保持数据库连接处于开启状态,直到游标遍历完毕。因此查询和写入必须在同一个事务或同一个 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(readOnly=true)publicvoidexportMillionUsers(HttpServletResponseresponse)throwsIOException{// 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");StringfileName=URLEncoder.encode("百万用户数据","UTF-8").replaceAll("\\+","%20");response.setHeader("Content-disposition","attachment;filename*=utf-8''"+fileName+".xlsx");// 使用 try-with-resources 确保游标和写入器正确关闭try(Cursor<UserExportModel>cursor=userMapper.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;@RestController@RequestMapping("/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 中设置fetchSizeInteger.MIN_VALUE,让驱动使用流式读取:

@Select("SELECT id, name, phone, email, create_time FROM t_user")@Options(fetchSize=Integer.MIN_VALUE)Cursor<UserExportModel>selectAllForExport();

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 游标 + EasyExcel20s~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 导出。核心要点有三个:

  1. 使用Cursor<T>而非List<T>,避免全量加载到内存。
  2. 保持数据库连接不释放,通过@Transactional保证游标可用。
  3. EasyExcel 流式写入,边读边写,形成流水线。

希望本文能帮助你解决大数据量导出的难题。如果你在实际项目中遇到了其他问题,欢迎在评论区交流讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询