高效excel导入关键在于分片合理、流式读取、批量落地三者协同:用easyexcel流式解析控内存,按逻辑数据块分片并行处理,线程池匹配db连接池,每片独立事务+批量插入+错误隔离。

Java用线程池高效处理大规模Excel导入,关键不在“多开线程”,而在于“分片合理 + 流式读取 + 批量落地”三者协同。盲目多线程反而容易引发CPU争抢、数据库连接池打满、甚至数据错乱。真正高效的方案,是让每个线程只负责一段连续、互不重叠的数据块,并确保解析和写入过程内存可控、事务隔离。
优先选流式读取,别加载全文件到内存
千万级Excel根本不能用XSSFWorkbook或常规SXSSFWorkbook——它们会把整张表结构、样式、公式全塞进堆内存,OOM几乎是必然结果。必须切换到事件驱动的流式解析:
- 推荐EasyExcel:封装了XSSF-SAX底层,自动处理共享字符串表、空单元格、日期格式等细节,只需实现
AnalysisEventListener,一行一行回调,内存稳定在10–50MB(与行数无关) - 若需更底层控制(如跳过隐藏sheet、禁用公式重算),可用Apache POI的
XSSFSheetXMLHandler配合XMLReader,但需手动维护行号计数和字符串缓存 - 无论哪种方式,打开文件时都用
OPCPackage.open(file, PackageAccess.READ),并预判sheet有效性:!workbook.isSheetHidden(i) && sheet.getPhysicalNumberOfRows() > 0
分片不是按“行号切段”,而是按“逻辑数据块”划分
Excel本身不支持随机访问某一行,所谓“分片”本质是让多个线程各自读取同一份文件的不同区间——这需要解析器支持“从第N行开始、读M行”的能力。EasyExcel原生支持:
- 使用
read(..., new PageReadListener<yourdata>() {...})</yourdata>配合setHeadRowNumber(1) - 提前获取总行数:
long total = reader.excelExecutor().sheet(0).getTotalRowNumber() - 设定每片大小(如5000行),计算分片数:
int shardCount = (int) Math.ceil(total * 1.0 / shardSize) - 每个线程传入起止行号:
new ExcelReaderBuilder(...).range(startRow, endRow)(注意:行号从1开始,且包含表头行)
线程池配置要匹配实际瓶颈,不是越多越好
数据库写入通常是最终瓶颈,线程数远超数据库连接池大小只会排队等待,徒增上下文切换开销:
- 先确认数据库连接池最大连接数(如HikariCP的
maximumPoolSize),线程池核心线程数建议设为该值的0.8–1倍 - 使用
LinkedBlockingQueue作任务队列,容量不宜过大(如50),避免积压任务拖垮JVM - 拒绝策略推荐
CallerRunsPolicy:当队列满时,由提交任务的主线程直接执行,起到自然限流作用 - 每个子任务内部仍需批量插入(如每1000条调用一次
batchUpdate),避免单条SQL反复网络往返
异常与一致性必须有兜底机制
多线程下一旦某片失败,不能简单抛异常中断全部——要保证已成功片段不回滚,失败片段可重试或单独记录:
- 每个分片任务独立try-catch,捕获后记录错误行号、原始内容、异常信息到临时表或文件
- 用
CountDownLatch等待所有分片完成,再统一汇总错误结果返回前端 - 数据库层开启事务粒度控制:每个分片一个独立事务(Spring中用
@Transactional(propagation = Propagation.REQUIRES_NEW)),避免互相影响 - 导入前可先校验关键字段非空、格式合规,将硬错误拦截在解析阶段,减少无效入库尝试
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











