【架构实战】百万级Excel数据导入的“坑”与“填坑”指南(上):痛点剖析与破局利器 EasyExcel
前言
大家好,这里是程序员阿亮!今天来给大家讲解一下在传统企业中报表和数据处理业务非常常见的工具-Excel在后端的使用和场景!
引言:从一个看似简单的需求说起
在日常的 B2B 业务、ERP 系统或者后台管理系统中,“Excel 导入导出”几乎是一个标配功能。
通常情况下,业务流程是这样的:产品经理提了一个需求,要求运营人员能够通过 Excel 批量上传商品信息、用户名单或者历史对账数据。开发人员接到需求后,往往第一反应是:这题我会,引入 Apache POI,写几行代码解析 Workbook,然后写个 for 循环存入数据库,搞定!
然而,当系统的业务量级不断攀升,运营人员上传的 Excel 文件从几千条膨胀到几十万甚至上百万条时,原本岁月静好的系统就会开始频繁报警:页面卡死、请求超时、甚至直接 OOM(内存溢出)导致服务宕机。
面对百万级数据的 Excel 导入,这不仅是一个简单的功能点,更是一道综合了内存管理、多线程并发、数据库优化与容错处理的系统性架构题。
本文将分为上下两篇,本篇(上篇)将深入剖析百万级数据导入过程中面临的“三座大山”,并详细拆解我们为何选择 EasyExcel 作为破局的利器。在下篇中,我将结合具体的代码实战,手把手教你如何通过“EasyExcel + 多线程 + 数据库批量插入”实现一套高性能的导入方案。
一、 灾难现场:百万级 Excel 导入面临的“三座大山”
在动手写代码之前,我们要学会拆解问题。百万级数据从 Excel 读取并插入到数据库,到底会遇到哪些问题?
1. 第一座大山:OOM(内存溢出)—— 悬在 JVM 头上的达摩克利斯之剑
这是处理大文件时最致命的问题。
传统的 Excel 处理(例如基于 Apache POI 的 XSSFWorkbook)采用的是DOM(文档对象模型)解析模式。这意味着程序会将整个 Excel 文件的内容(本质上是庞大的 XML 文件结构)一次性完整地加载到内存中,构建成一棵巨大的对象树。
你可以算一笔账:
一个几十 MB 的 Excel 文件,由于包含了大量的样式、单元格属性、空行等冗余信息,被 POI 解析成 Java 对象驻留在堆内存中时,其占用的内存大小可能会膨胀数倍甚至十倍。对于一个包含百万行数据的 Excel,瞬间吃掉几个 G 的内存是常有的事。如果此时系统正好处于高并发期,JVM 根本来不及进行垃圾回收(GC),直接就会抛出 java.lang.OutOfMemoryError: Java heap space,导致整个服务崩溃。
2. 第二座大山:龟速的性能 —— 让人绝望的等待
百万级数据的处理,如果按照传统的“单线程读取 -> 单条校验 -> 单条 Insert”模式,性能是极其低下的。
读取慢:单线程线性解析百万行数据,本身就非常耗时。
网络与 IO 瓶颈:如果在循环里逐条调用 INSERT 语句,意味着程序要和数据库进行一百万次网络通信,经历一百万次事务的开启与提交。即使数据库性能再好,也会被频繁的网络 IO 彻底拖垮。
用户体验极差:导入请求通常通过 HTTP 发起,处理时间过长不仅会导致前端请求超时(Timeout),还会长期占用服务器的连接池资源。
3. 第三座大山:脆弱的错误处理 —— 牵一发而动全身
在文件的读取和数据库写入过程中,业务逻辑往往是复杂的。由于 Excel 数据是由人工填写的,你永远无法预知用户会输入什么奇葩数据:
数据格式错误:本该填数字的地方填了汉字。
数据缺失:必填字段为空。
数据重复:Excel 内部包含重复数据,或者与数据库已有数据冲突(违反唯一性约束)。
传统模式的痛点在于:
如果在导入了 99 万条数据时,第 990001 条数据发生了异常,此时该怎么办?
如果是整体包裹在一个大事务中,直接回滚(Rollback)会导致前面的 99 万条处理时间全部浪费,且大事务极易锁死数据库;如果不回滚,就会产生“部分成功、部分失败”的脏数据,后续的重试和数据清洗将是一场灾难。
二、 破局思路:从“全局掌控”到“分批流水线”
面对这三座大山,我们的架构思路必须发生转变。
解决 OOM:放弃“一口吃成胖子”,改用“流水线处理”。
百万级数据绝不能一次性读入内存,必须采用基于流式读取(Streaming)的方式,读一行,处理一行,丢弃一行,让内存始终保持在一个极低的平稳状态。解决性能问题:引入“多线程并发”与“批量处理”。
读阶段:我们可以将百万数据分散在 Excel 的不同 Sheet 中,利用多线程同时读取不同的 Sheet,提升解析速度。
写阶段:放弃单条插入,在内存中暂存一小批数据(例如 1000 条),然后利用 MyBatis 或 JDBC 的批量插入(Batch Insert)功能,极大地减少数据库交互次数。这就是经典的生产者-消费者模型。
解决容错问题:剥离校验与入库,实施“容错与日志补偿”策略。
分为两步走。第一步先做数据基础格式校验;第二步在入库时,不建议对百万级数据使用大事务回滚。更合理的做法是:捕获异常,跳过错误数据,通过日志记录下失败的行号和原因(或记录到异常表中),让正确的数据先入库,失败的数据后续由人工根据日志进行修正和重新导入。
明确了思路,接下来就是技术选型。为了实现上述的“流式读取”,我们引入了阿里开源的神器 ——EasyExcel。
三、 内存杀手的克星:EasyExcel 深度解析
1. 为什么不直接用 Apache POI 的 SAX 模式?
前面提到 POI 的 DOM 模式会导致 OOM。其实 POI 官方也知道这个问题,所以提供了一种底层基于 SAX(Simple API for XML)的事件驱动解析模式。SAX 模式确实可以做到逐行读取,解决内存溢出。
但是,POI 的 SAX 模式 API 极其底层且反人类!开发者需要自己去处理繁杂的 XML 标签,处理行、列的索引映射,甚至还要自己处理不同 Excel 版本(xls vs xlsx)的底层差异。写一个健壮的 SAX 解析器,代码量巨大且极其容易出 Bug。
2. EasyExcel 是什么?
EasyExcel是阿里巴巴开源的一个基于 Java 的、简单、省内存的读写 Excel 的开源项目。
在技术选型上,我们毫不犹豫地选择了 EasyExcel,因为它是特别针对大数据量和复杂 Excel 文件处理进行优化的。
它的核心优势可以用一句话概括:在尽可能节约内存的情况下,提供极其极其简单的 API 让开发者轻松读写大 Excel。
3. EasyExcel 的核心工作原理(为什么它不内存溢出?)
EasyExcel 能够轻松搞定百万级数据的核心秘密,在于其彻底重写了 POI 对 xlsx 文件的解析过程。
当 EasyExcel 解析 Excel 时:
不构建完整 DOM 树:它绝对不会将整个 Excel 文件一次性全部加载到内存中。
基于磁盘与流的逐行解析:它从磁盘上一行一行地读取数据流。EasyExcel 会将每一行解析出来的数据,转换为我们定义的 Java 实体类(POJO)。
事件驱动回调(核心):每次解析完一行数据,它就会自动触发(回调)一个名为 ReadListener 的接口。
即用即毁:在 ReadListener 中处理完这行数据后,这行数据占用的内存就会失去强引用,随时可以被 JVM 垃圾回收器(GC)回收掉。
通过这种“细水长流”的方式,无论你的 Excel 文件是 10 万行还是 1000 万行,它占用的内存都仅仅是当前正在解析的那几行数据所占用的极小空间,完美避开了 OOM 的雷区。
4. 灵魂组件:ReadListener (监听器)
理解 EasyExcel 的精髓,就在于理解 ReadListener。这就好比一条工厂的流水线:
EasyExcel 解析引擎就是传送带,它负责把 Excel 里的每一行数据变成零件,源源不断地送过来。
ReadListener就是站在传送带旁边的工人。你可以自定义这个工人要做什么。
在实际开发中,我们通常会实现这个接口,主要关注其中的两个方法:
invoke(T data, AnalysisContext context):
这是最核心的方法。每读取到一行数据,就会调用一次此方法。我们可以在这里进行数据的合法性校验,并将有效的数据暂存到一个本地集合(如 List)中。当集合大小达到我们设定的阈值(例如 1000 条,这就是上文提到的分批次处理)时,触发一次数据库的批量插入,然后清空集合。doAfterAllAnalysed(AnalysisContext context):
当整个 Excel(或当前 Sheet)的所有数据都读取完毕后,会调用此方法。因为最后一次读取的数据往往凑不够一个批次(比如最后只剩下 300 条),我们必须在这个方法里进行“收尾工作”,将最后剩余的数据也插入到数据库中。
四、 总结与下篇预告
到这里,我们已经清晰地梳理了百万级 Excel 数据导入的背景和痛点。
我们明确了不能使用传统的“一把梭”读写方式,而是要构建一套基于流式读取、内存批处理、多线程并发协作的完整方案。而 EasyExcel 凭借其优秀的事件驱动架构和极致的内存管理,成为了我们承载数据读取任务的最佳载体。
方案整体的宏观蓝图已经绘就:
利用多线程池,并发读取含有百万级数据 Excel 的不同 Sheet。
借助 EasyExcel 的 ReadListener 逐行解析,避免内存溢出。
在 Listener 中维持一个缓冲池,满 1000 条则利用 MyBatis 触发一次批量插入,大幅降低数据库 I/O 压力。
抛弃全局事务回滚,针对冲突数据采用“跳过+记录日志”的降级容错方案。
纸上得来终觉浅,绝知此事要躬行。
在接下来的《百万级数据Excel导入的“坑”与“填坑”指南(下):多线程与MyBatis批量入库实战》中,我将放出真正的核心代码。我们将详细探讨:
如何优雅地通过线程池并发读取多 Sheet 代码实现?
自定义 ReadListener 的编写规范与内部重试机制应该怎么写?
MyBatis 批量插入的 XML 是如何配置的?
关注我,我们下篇文章见真章!
