ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java实现Excel批量导入MySQL:EasyExcel与JDBC批处理实战

Java实现Excel批量导入MySQL:EasyExcel与JDBC批处理实战 简介这份资源面向Java后端初学者与需要处理数据迁移的开发者聚焦Excel与MySQL之间的双向数据流转问题。项目基于Apache POI解析xls/xlsx文件通过JDBC建立MySQL连接实现Excel数据导入数据库并在检测到重复数据时执行更新操作同时支持将库中数据反向导出为Excel表格覆盖文件操作、单元格类型解析、SQL条件判断与批量处理等核心技能点。压缩包共20个文件约1.31MB包含6个java源码与6个class编译文件、2个依赖jar包mysql驱动与jxl、1个建表sql脚本及Eclipse工程配置结构完整可直接导入运行。目前已有1737人学习下载适合作为JDBC与POI综合练习的参考案例帮助读者理解数据导入导出策略与工程目录组织方式。1. Java 把 Excel 灌进 MySQL一条被低估的脏活链路电商后台的运营丢过来一个 8 万行的 Excel说「今天下班前导进系统」。你打开一看合并单元格、手机号被存成科学计数法、日期列一半是文本一半是日期格式还有三行是空行夹在中间。这时候你才意识到Excel 导入 MySQL 这件事写个for循环读单元格谁都会真正难的是让这条链路在脏数据面前不崩、不重复、不 OOM。这个标题讲的就是这条链路用 Java 把 Excel 文件解析出来清洗成规整的行记录再批量写进 MySQL。它解决的是「业务方只给 Excel系统只认数据库」这个每天都在发生的对接问题。适合谁看写过 JDBC 但没处理过万行级导入的 Java 后端、需要给运营做数据导入功能的全栈、以及被 POI 内存溢出坑过一次想搞清楚边界的人。下面按「选型 → 解析 → 入库 → 排错 → 调优」的顺序把我实际跑通过的方案拆开讲。2. 选型先立住POI、EasyExcel 和 JDBC 批处理怎么搭2.1 解析层为什么我最终选 EasyExcel 而不是裸 POI裸 POI 的XSSFWorkbook会把整个 xlsx 一次性读进内存一个 10 万行、20 列的文件堆内存轻松吃掉 1G 以上线上直接 OOM。POI 官方给的SXSSFWorkbook是写场景的流式方案读场景要用XSSFReader SAX 自己写事件处理器代码量陡增还得自己维护共享字符串表sharedStrings和样式索引稍不留神就解析错位。EasyExcel 本质是把 POI 的 SAX 模式封装成了监听器回调读的时候一行一行触发invoke内存占用和行数基本无关。常见做法是继承AnalysisEventListener在invoke里攒够一批就落库doAfterAllAnalysed里处理最后一批。选它的核心理由不是「快」而是「内存可控 代码可读」团队里新人接手也能看懂。如果你的文件是老的.xls格式BIFF8EasyExcel 底层还是走 HSSF内存优势会打折这种文件建议先让业务方另存为 xlsx。至于.csv别用 Excel 解析库直接按行读、按逗号切性能高一个数量级但要注意引号包裹的字段里可能含逗号得用带状态的解析器而不是split(,)。2.2 入库层JDBC 批处理 rewriteBatchedStatements 才是关键很多人导入慢问题不在解析在入库。默认情况下 MySQL JDBC 驱动会把addBatch()的语句一条条发给服务端1 万行就是 1 万次网络往返。开启rewriteBatchedStatementstrue后驱动会把多条 INSERT 合并成一条INSERT INTO t VALUES (...),(...),(...)吞吐能差 5 到 10 倍。连接串我一般这么写jdbc:mysql://127.0.0.1:3306/import_demo?useUnicodetruecharacterEncodingutf8mb4rewriteBatchedStatementstrueuseServerPrepStmtsfalseallowMultiQueriestrue参数逐个说清楚rewriteBatchedStatementstrue是批处理合并的开关必须开useServerPrepStmtsfalse让驱动走客户端预编译配合批处理重写才生效开了服务端预编译反而会绕过重写逻辑characterEncodingutf8mb4保证 emoji 和生僻字不乱码allowMultiQueries在合并语句时可能被用到。这几个参数是导入性能的血泪经验少一个都可能让你以为「MySQL 就这么慢」。提示rewriteBatchedStatements只对PreparedStatement的addBatch生效用Statement拼字符串是享受不到的。2.3 表结构字段类型和索引要先想清楚导入前先把目标表建好别指望导入时动态建表。字段类型上手机号、身份证、订单号这类「看起来是数字但永远不参与运算」的列一律用VARCHAR用BIGINT存手机号迟早被前导零和超长号段坑。金额用DECIMAL(18,4)别用DOUBLE浮点误差在对账时是灾难。索引是双刃剑导入期间目标表上的二级索引会拖慢写入因为每插一行都要维护索引树。如果是一次性大批量导入常见做法是先ALTER TABLE ... DISABLE KEYS仅 MyISAM 有效或者干脆先删掉非唯一索引导完再建。InnoDB 没有 disable keys替代方案是先导入到一张无索引的临时表再用INSERT INTO ... SELECT灌到正式表。3. 动手跑通从 Excel 到 MySQL 的完整代码链路3.1 依赖和实体映射Maven 里引两个核心依赖EasyExcel 和 MySQL 驱动dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency实体类用注解把表头和字段绑起来ExcelProperty的 value 必须和 Excel 表头文字完全一致包括空格。index属性可以按列序号映射表头会变动的场景用 index 更稳public class UserRow { ExcelProperty(姓名) private String name; ExcelProperty(手机号) private String phone; ExcelProperty(注册时间) private String registerTime; // 先接字符串后面自己解析 // getter / setter 省略 }这里有个刻意的设计日期列先接成String。因为 Excel 里的日期可能是2024-01-01、2024/1/1、45000序列号三种形态交给 EasyExcel 的LocalDateTime转换器遇到序列号会直接抛异常。先接字符串在业务层用统一方法解析容错性高得多。3.2 监听器里做批量攒批和落库核心逻辑在监听器攒够BATCH_SIZE条就 flush 一次public class UserImportListener extends AnalysisEventListenerUserRow { private static final int BATCH_SIZE 1000; private final ListUserRow buffer new ArrayList(BATCH_SIZE); private final UserDao userDao; public UserImportListener(UserDao userDao) { this.userDao userDao; } Override public void invoke(UserRow row, AnalysisContext context) { // 空行过滤姓名和手机号都为空直接跳过 if (isBlank(row.getName()) isBlank(row.getPhone())) { return; } buffer.add(row); if (buffer.size() BATCH_SIZE) { userDao.batchInsert(buffer); buffer.clear(); // 必须清空否则内存持续增长 } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!buffer.isEmpty()) { userDao.batchInsert(buffer); buffer.clear(); } } }逻辑说明invoke每读一行触发一次攒批到 1000 条就调 DAO 落库并清空缓冲。doAfterAllAnalysed处理最后不足一批的尾巴这一步漏了就会丢数据是新手最常见的翻车点。buffer.clear()不能省否则 List 会一直涨攒批就失去意义了。参数说明BATCH_SIZE设 1000 是个经验值。太小则网络往返多太大则单条 SQL 过长可能撞上max_allowed_packet默认 4M1000 行 20 列大约几百 KB比较安全。如果列特别多降到 500。3.3 DAO 层的批处理写法DAO 用PreparedStatement的addBatch/executeBatchpublic void batchInsert(ListUserRow rows) { String sql INSERT INTO user_info(name, phone, register_time) VALUES (?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关自动提交手动控事务 for (UserRow row : rows) { ps.setString(1, row.getName()); ps.setString(2, row.getPhone()); ps.setString(3, normalizeDate(row.getRegisterTime())); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { throw new RuntimeException(批量插入失败, e); } }逻辑说明关掉autoCommit后整批插入在一个事务里提交减少 redo log 刷盘次数。executeBatch一次性把攒的语句发出去配合连接串里的rewriteBatchedStatementstrue驱动会合并成多值 INSERT。参数说明normalizeDate是自定义的日期归一化方法把2024/1/1、45000这类统一转成yyyy-MM-dd HH:mm:ss。序列号转日期用1900-01-01加天数注意 Excel 有个 1900 闰年 bug1900 年 3 月之前的日期要减一天这个坑后面排错章节细说。3.4 主流程串起来public class ImportMain { public static void main(String[] args) { String filePath D:/data/user_import.xlsx; UserDao userDao new UserDao(); EasyExcel.read(filePath, UserRow.class, new UserImportListener(userDao)) .sheet() .doRead(); System.out.println(导入完成); } }EasyExcel.read指定文件、实体类、监听器.sheet()默认读第一个 sheet多 sheet 场景用.sheet(0)或.sheet(Sheet1)指定。.doRead()是同步阻塞的读完整文件才返回。如果文件有多个 sheet 都要导链式调多个.sheet()即可。4. 避坑与排查导入翻车的 5 个真实场景4.1 手机号变成 1.38E10现象导入后手机号列全是1.38E10这种科学计数法或者末尾几位变成 0。原因Excel 把纯数字单元格当数值存储超过 11 位精度丢失读取时 POI 拿到的是 double。这是 Excel 本身的存储机制不是解析库的锅。解决读取时用DataFormatter或 EasyExcel 的ExcelProperty配合字符串转换器强制按文本读。更彻底的办法是让业务方在 Excel 里把该列设成文本格式再填。代码侧可以在invoke里对手机号做一次new BigDecimal(value).toPlainString()还原。4.2 日期列一半能解析一半报错现象同一列日期有的行正常入库有的行抛DateTimeParseException。原因Excel 里日期可能是真日期存为序列号、文本日期、或者带时区的字符串格式不统一。解决实体类里日期字段接String在业务层写一个normalizeDate方法按优先级尝试多种格式解析全失败就记日志跳过该行而不是整批失败。序列号转日期记得处理 1900 闰年 bug序列号小于 60 的加 1 天再算。4.3 导入到一半 OOM现象小文件正常几万行的大文件跑着跑着OutOfMemoryError。原因要么用了XSSFWorkbook全量读要么监听器里的 buffer 没清空要么在内存里攒了全部行才落库。解决确认用的是 EasyExcel 的监听器模式而非EasyExcel.read(file).doReadSync()后者会把所有行读进 List。检查buffer.clear()是否在每个 flush 分支都调用了。JVM 参数上给-Xmx留够余量但根本解法是流式处理。4.4 重复导入产生重复数据现象同一个文件导了两次表里出现两份数据。原因没有唯一约束也没有幂等控制。解决在业务主键比如手机号或订单号上建唯一索引插入用INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE。或者在导入前按文件 MD5 记录一张导入日志表同一文件不重复处理。唯一索引是最后一道防线别省。4.5 中文乱码现象姓名列入库后是问号或乱码。原因连接串没指定字符集或者数据库/表的字符集是latin1。解决连接串加characterEncodingutf8mb4建库建表时用DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci。注意utf8在 MySQL 里是阉割版存不了 emoji一律用utf8mb4。5. 进阶把导入做成可复用、可观测的组件5.1 用模板方法把导入流程抽象出来每张表的导入逻辑大同小异差异只在实体类、校验规则、DAO。我一般抽一个泛型基类public abstract class AbstractImportListenerT extends AnalysisEventListenerT { private static final int BATCH_SIZE 1000; private final ListT buffer new ArrayList(BATCH_SIZE); protected abstract void batchSave(ListT rows); protected abstract boolean validate(T row); Override public void invoke(T row, AnalysisContext context) { if (!validate(row)) return; buffer.add(row); if (buffer.size() BATCH_SIZE) { batchSave(buffer); buffer.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!buffer.isEmpty()) { batchSave(buffer); buffer.clear(); } } }子类只需实现batchSave和validate导入逻辑复用。validate里做必填校验、格式校验返回 false 的行直接跳过并计数最后统一报告「成功 N 行跳过 M 行」。5.2 加一层导入结果统计光导入不够得让调用方知道结果。在监听器里维护计数器指标含义用途totalRows解析到的总行数和文件行数对账successRows成功入库行数判断导入是否完整skipRows校验失败跳过行数定位脏数据failRows入库异常行数排查数据库问题costMs总耗时性能基线这几个数在doAfterAllAnalysed里汇总返回给调用方或写进导入日志表。运营看到「跳过 3 行」会主动去查那 3 行是什么问题比一句「导入完成」有用得多。5.3 大文件的分片与断点续传思路几十万行的文件单次导入可能跑十几分钟中途网络抖动就前功尽弃。常见做法是按 sheet 或按行号分片每片导入成功后记录进度到一张import_progress表失败重试时从上次成功的分片继续。EasyExcel 支持headRowNumber和自定义读取范围配合分片逻辑能实现断点续传。这个方案复杂度不低只有文件稳定超过 10 万行才值得上小文件别过度设计。5.4 一个我踩过的坑别在监听器里开事务早期我把事务开在监听器的invoke里每行一个事务结果 1 万行跑了 8 分钟。后来改成攒批后整批一个事务同样的数据 20 秒跑完。事务的粒度要和批处理的粒度对齐一行一事务是最慢的写法整文件一事务又太大容易锁表1000 行一批是平衡点。导入这件事写通一次不难难的是在脏数据、大文件、重复执行这些真实条件下还能稳住。我现在做任何导入功能第一件事就是建唯一索引和导入日志表这两样是后悔药出事时能救命。希望帮到你。本文还有配套的精品资源点击获取
返回列表