极致性能压榨:如何将 MyBatis 批量插入 10 万条数据从 5 分钟优化到 3 秒?

发布时间:2026/6/12 23:06:11

极致性能压榨:如何将 MyBatis 批量插入 10 万条数据从 5 分钟优化到 3 秒? 导读在日常的后端开发中数据迁移和海量数据导入是极其常见的业务场景。近期在处理一项老系统到新系统的数据迁移任务时面临着 10 万条用户数据的批量入库需求。最初的简单实现耗时高达 5 分钟经过一下午的排查与四次重构最终将接口响应时间压缩至惊人的 3 秒。本文将完整复盘这100 倍性能提升的演进历程并毫无保留地分享期间踩过的坑与最终的生产级代码。一、 灾难级的初始代码循环单条插入耗时300 秒最开始的实现逻辑非常直白直接遍历集合逐条调用 Mapper 的insert方法Java// 方式1循环单条插入极度不推荐 for (User user : userList) { userMapper.insert(user); }性能灾难分析10 万条数据意味着程序要执行 10 万次网络请求、数据库要进行 10 万次 SQL 解析、开启并提交 10 万次事务。假设单条数据完整的插入链路需要 3 毫秒10 万条就是 300 秒整整 5 分钟。这是典型的 IO 密集型“反模式”Anti-Pattern但在很多初级或遗留项目中却屡见不鲜。二、 第一次跃升XML 批量 SQL 拼接耗时30 秒为了减少致命的网络往返开销最直接的思路就是将多条INSERT语句合并为一条。我们利用 MyBatis 的foreach标签来实现Mapper.xml 配置XMLinsert idbatchInsert INSERT INTO user (name, age, email) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.age}, #{item.email}) /foreach /insertJava 业务层分批调用Java// 分批插入设定每批 1000 条防止单条 SQL 过长 int batchSize 1000; for (int i 0; i userList.size(); i batchSize) { int end Math.min(i batchSize, userList.size()); ListUser batch userList.subList(i, end); userMapper.batchInsert(batch); }优化成果耗时从 5 分钟骤降到 30 秒性能直接提升 10 倍。核心原理就是一条 SQL 携带多条数据极大降低了网络 I/O 频次。但这对于生产环境来说30 秒的接口响应依然存在超时的巨大风险。三、 第二次跃升JDBC 批处理模式耗时8 秒想要进一步压榨性能就必须深入到底层驱动。MySQL 实际上提供了一个原生参数rewriteBatchedStatements当它开启时能够将多条独立的INSERT语句在驱动层合并发送。第一步修改数据库连接池 URL在配置中显式追加参数Propertiesjdbc:mysql://localhost:3306/test?rewriteBatchedStatementstrue第二步采用 MyBatis 的 BATCH 执行器JavaAutowired private SqlSessionFactory sqlSessionFactory; public void batchInsertWithExecutor(ListUser userList) { // 开启 BATCH 模式的 SqlSession try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper sqlSession.getMapper(UserMapper.class); int batchSize 1000; for (int i 0; i userList.size(); i) { mapper.insert(userList.get(i)); // 每达到 1000 条刷入数据库并清空本地缓存 if ((i 1) % batchSize 0) { sqlSession.flushStatements(); sqlSession.clearCache(); } } // 处理剩余的尾部数据 sqlSession.flushStatements(); sqlSession.commit(); } }优化成果耗时从 30 秒降至 8 秒相比初始方案提升 37 倍。底层原理在ExecutorType.BATCH模式下MyBatis 不会立刻执行 SQL而是将其缓存。配合rewriteBatchedStatementstrueMySQL 驱动会在执行flushStatements时将缓存的多条 INSERT 语句改写并合并为高效的批量插入协议发送给数据库引擎。四、 终极杀招多线程并行切片耗时3 秒单线程的吞吐量榨干后剩下的就是利用多核 CPU 的算力进行并发处理了。我们将 10 万条数据进行切片交由线程池并行执行上面第三步的逻辑。Javapublic void parallelBatchInsert(ListUser userList) { // 根据数据库连接池的真实大小来评估核心线程数切忌盲目开大 int threadCount 4; int batchSize userList.size() / threadCount; ExecutorService executor Executors.newFixedThreadPool(threadCount); ListFuture? futures new ArrayList(); for (int i 0; i threadCount; i) { int start i * batchSize; int end (i threadCount - 1) ? userList.size() : (i 1) * batchSize; ListUser subList userList.subList(start, end); // 提交多线程任务 futures.add(executor.submit(() - { batchInsertWithExecutor(subList); // 复用方案三的高效插入逻辑 })); } // 阻塞主线程等待所有子任务执行完毕 for (Future? future : futures) { try { future.get(); } catch (Exception e) { throw new RuntimeException(e); } } executor.shutdown(); }优化成果耗时最终定格在3 秒完成了100 倍的性能飞跃⚠️ 并发警告1. 线程池的大小绝对不能超过数据库连接池的配置上限否则会导致线程饥饿等待。2. 多线程环境下各线程事务相互隔离。如果业务对数据一致性要么全成功要么全失败有强要求此方案需配合分布式事务或回滚补偿机制不可盲目套用。3. 需提前考量主键冲突导致部分线程失败的问题。五、 核心成效对比数据看板演进方案耗时评估 (10万条)性能跃升倍数核心抓手循环单条插入300 秒1x (基准)无批量 SQL (foreach)30 秒10倍减少网络往返JDBC 批处理8 秒37倍底层 SQL 合并执行多线程并行切片3 秒100倍CPU 与连接池并发压榨六、 实战踩坑血泪史高价值排雷在摸索出最终方案之前也曾掉入过不少陷阱总结如下供大家避雷坑一foreach拼接的 SQL 突破极限现象如果直接将 10 万条数据塞进一个foreach生成的 SQL 字符串会极其庞大。后果触发 MySQL 的max_allowed_packet异常数据包过大拒绝接收。破局务必在 Java 内存中预先分页通常每批 500 - 1000 条最为安稳。坑二rewriteBatchedStatements形同虚设现象加了参数但速度没变快。破局必须满足“三位一体”1. URL 参数拼写无误。 2. 必须使用ExecutorType.BATCH。 3. MySQL 驱动包版本不能太古老。坑三自增主键ID无法回写现象使用了insert useGeneratedKeystrue keyPropertyid但在批量插入后实体类里的 ID 依然是 null。破局这是旧版驱动对rewriteBatchedStatements支持不完善导致的 bug。解决方案是升级mysql-connector-java驱动至8.0.17 或更高版本。坑四JVM 内存直接 OOM现象10 万个甚至上百万个超大实体对象一次性加载到内存中直接撑爆堆内存OutOfMemoryError。破局不要一次性select全量数据。采用“分页读取 分批插入”的流式处理思路Javaint pageSize 10000; int total countTotal(); for (int i 0; i total; i pageSize) { ListUser page selectByPage(i, pageSize); batchInsertWithExecutor(page); // 查一批插一批内存稳如老狗 }七、 伸手党福音最终生产级封装代码为了方便日后复用我利用CountDownLatch将终极方案封装成了 Spring Boot 中的标准 Service大家可以直接 CtrlC/V 接入项目Javaimport org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.BiConsumer; /** * 通用高性能批量插入服务 * author hjm */ Service public class BatchInsertService { Autowired private SqlSessionFactory sqlSessionFactory; /** * 高性能并行批量插入调度 (泛型化) * param dataList 待插入数据集合 * param mapperClass MyBatis Mapper 接口类型 * param insertFunction 具体的插入方法引用 (例如 UserMapper::insert) */ public M, T void highPerformanceBatchInsert(ListT dataList, ClassM mapperClass, BiConsumerM, T insertFunction) { if (dataList null || dataList.isEmpty()) return; // 根据核心数调整线程建议不宜过多以免撑爆连接池 int threadCount Math.min(4, Runtime.getRuntime().availableProcessors()); int batchSize (int) Math.ceil((double) dataList.size() / threadCount); ExecutorService executor Executors.newFixedThreadPool(threadCount); CountDownLatch latch new CountDownLatch(threadCount); for (int i 0; i threadCount; i) { int start i * batchSize; int end Math.min((i 1) * batchSize, dataList.size()); if (start dataList.size()) { latch.countDown(); continue; } ListT subList new ArrayList(dataList.subList(start, end)); executor.submit(() - { try { doBatchInsert(subList, mapperClass, insertFunction); } finally { latch.countDown(); } }); } try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { executor.shutdown(); } } private M, T void doBatchInsert(ListT dataList, ClassM mapperClass, BiConsumerM, T insertFunction) { // 开启 BATCH 模式手动管理事务提交 try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH, false)) { M mapper sqlSession.getMapper(mapperClass); for (int i 0; i dataList.size(); i) { insertFunction.accept(mapper, dataList.get(i)); // 每 1000 条刷盘一次防止 SQL 过长或内存堆积 if ((i 1) % 1000 0) { sqlSession.flushStatements(); sqlSession.clearCache(); } } sqlSession.flushStatements(); sqlSession.commit(); } } }八、 技术总结性能优化的底层逻辑其实非常朴素万变不离其宗。回顾这 100 倍的性能提升我们只做了三件事减少网络 IO 的往返频次降低事务开启与提交的开销合理利用多核与连接池的并行能力掌握了这三板斧以后遇到再大的数据写入也能做到心中有数、游刃有余。

相关新闻