MyBatis批量插入性能优化:从5分钟到3秒的实战 1. 从5分钟到3秒MyBatis批量插入性能优化实战上周接手了一个数据迁移任务需要将10万条用户数据从旧系统迁移到新系统。最初采用最简单的单条插入方式整个过程耗时约5分钟。经过三个关键优化步骤后最终将时间压缩到3秒。本文将详细分享整个优化过程包括代码实现、原理分析以及踩坑经验。2. 初始方案循环单条插入300秒2.1 问题代码分析最常见的MyBatis插入方式就是循环调用单条插入// 方式1循环单条插入最慢 for (User user : userList) { userMapper.insert(user); }这种写法虽然简单直观但性能极差。10万条数据需要执行10万次独立的SQL语句每次插入都包含以下开销应用层到数据库的网络往返SQL解析和优化事务处理索引维护假设每条插入平均耗时3ms10万条就是300秒5分钟。在实际测试中这个数字还会受到数据库负载、网络延迟等因素影响而变得更糟。2.2 性能瓶颈解析这种方式的性能瓶颈主要来自网络IO每次插入都需要完整的请求-响应过程事务开销默认情况下MyBatis自动提交事务每条insert都是一个独立事务SQL解析数据库需要重复解析结构相同的SQL语句提示即使将事务改为手动提交也只是减少了部分事务开销网络和SQL解析的开销仍然存在。3. 第一层优化批量SQL30秒3.1 批量插入实现MyBatis提供了foreach标签支持批量SQL生成!-- Mapper.xml -- insert idbatchInsert INSERT INTO user (name, age, email) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.age}, #{item.email}) /foreach /insertJava调用代码// 分批插入每批1000条 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); }3.2 性能提升原理这种方式将多条insert合并为一条SQL语句例如INSERT INTO user (name, age, email) VALUES (张三, 25, zhangsanexample.com), (李四, 30, lisiexample.com), ... (王五, 28, wangwuexample.com);优势在于减少网络往返次数从N次变为N/batchSize次数据库只需解析一次SQL单次事务提交实测10万条数据插入时间从300秒降至30秒提升10倍。3.3 注意事项批大小选择建议500-1000条/批过大可能导致SQL过长超过max_allowed_packet限制内存占用过高数据库锁持有时间过长主键返回如果需要获取自增ID需配置insert idbatchInsert useGeneratedKeystrue keyPropertyid但注意MySQL对批量插入返回自增ID的支持有限制。4. 第二层优化JDBC批处理8秒4.1 启用批处理模式MySQL提供了rewriteBatchedStatements参数优化批处理jdbc:mysql://localhost:3306/test?rewriteBatchedStatementstrue配合MyBatis的批处理执行器public void batchInsertWithExecutor(ListUser userList) { 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)); if ((i 1) % batchSize 0) { sqlSession.flushStatements(); sqlSession.clearCache(); } } sqlSession.flushStatements(); sqlSession.commit(); } }4.2 技术原理ExecutorType.BATCHMyBatis会缓存预处理语句而不是立即执行rewriteBatchedStatementsMySQL驱动会将多个INSERT重写为更高效的形式分批刷新定期清空缓存防止内存溢出这种组合使得实际发送到数据库的SQL数量大幅减少10万条数据降至8秒完成。4.3 常见问题排查参数不生效检查确认连接URL包含rewriteBatchedStatementstrue确认使用了ExecutorType.BATCHMySQL驱动版本建议5.1.138.0最佳主键返回异常批量模式下自增ID可能不连续解决方案升级MySQL驱动到8.0.17内存管理大批量操作需定期flushStatements考虑使用clearCache防止二级缓存膨胀5. 第三层优化多线程并行3秒5.1 并行处理实现public void parallelBatchInsert(ListUser userList) { int threadCount 4; // 根据CPU核心数和连接池大小调整 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(); }5.2 关键配置要点线程数选择不超过数据库连接池最大连接数建议CPU核心数×1.5可通过压测找到最优值数据分片均匀分配数据量到各线程使用subList避免数据拷贝资源管理使用CountDownLatch确保所有线程完成正确关闭线程池5.3 适用场景与限制适用场景数据量大且可并行处理对实时性要求高的迁移任务非事务性操作或可接受最终一致性限制需要足够数据库连接资源可能引发主键冲突需确保数据分区不重叠错误处理更复杂6. 优化效果对比优化阶段耗时提升倍数核心优化点循环单条插入300s1x-批量SQL30s10x减少网络往返JDBC批处理8s37x减少SQL解析和事务次数多线程并行3s100x并行利用多核CPU7. 完整解决方案代码Service public class BatchInsertService { Autowired private SqlSessionFactory sqlSessionFactory; /** * 高性能批量插入 * 10万条数据约3秒 */ public void highPerformanceBatchInsert(ListUser userList) { if (userList null || userList.isEmpty()) { return; } int threadCount Math.min(4, Runtime.getRuntime().availableProcessors()); int batchSize (int) Math.ceil((double) userList.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, userList.size()); if (start userList.size()) { latch.countDown(); continue; } ListUser subList new ArrayList(userList.subList(start, end)); executor.submit(() - { try { doBatchInsert(subList); } finally { latch.countDown(); } }); } try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } executor.shutdown(); } private void doBatchInsert(ListUser userList) { try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH, false)) { UserMapper mapper sqlSession.getMapper(UserMapper.class); for (int i 0; i userList.size(); i) { mapper.insert(userList.get(i)); if ((i 1) % 1000 0) { sqlSession.flushStatements(); sqlSession.clearCache(); } } sqlSession.flushStatements(); sqlSession.commit(); } } }8. 关键问题与解决方案8.1 SQL过长问题现象报错Packet for query is too large数据库拒绝执行超长SQL解决方案减小批处理大小建议500-1000条/批调整MySQL配置SET GLOBAL max_allowed_packet256*1024*1024;8.2 内存溢出问题现象OOM异常应用内存持续增长解决方案分页读取源数据int pageSize 10000; int total countTotal(); for (int i 0; i total; i pageSize) { ListUser page selectByPage(i, pageSize); batchInsertWithExecutor(page); }定期清空MyBatis缓存增加JVM堆内存8.3 主键冲突问题现象多线程插入时出现主键重复自增ID不连续解决方案使用UUID或其他分布式ID方案确保数据分片不重叠使用数据库序列Oracle或调整自增步长MySQL9. 优化原则总结减少网络往返批量操作优于单条操作降低事务开销合并事务提交次数并行处理充分利用多核CPU资源控制合理设置批大小和线程数监控调整根据实际运行情况持续优化在实际项目中建议从简单的批量SQL开始逐步应用更高级的优化技术。同时要注意并非所有场景都适合多线程并行需要根据具体业务需求和系统资源情况选择合适的方案。