MyBatis批量插入踩坑实录:从‘20分钟’优化到‘6秒’,我都经历了什么?
MyBatis批量插入性能优化实战:从20分钟到6秒的蜕变之旅
那天下午,系统监控突然报警,一个原本运行良好的数据导入功能竟然耗时超过20分钟。作为负责人的我,立刻意识到问题的严重性——上万条数据的批量插入操作出现了性能瓶颈。经过一系列排查和优化,最终将执行时间压缩到6秒。下面分享这段跌宕起伏的技术探索历程。
1. 问题初现:性能断崖式下跌
我们的电商促销系统需要一次性导入上万条商品数据,每条记录包含20多个字段。最初采用MyBatis默认的foreach批量插入方式:
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO product (name, price, stock, category, ..., spec_20) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.price}, ..., #{item.spec20}) </foreach> </insert>在测试环境表现尚可,但生产环境数据量增大后,出现了以下症状:
- 执行时间非线性增长:100条数据0.5秒,1000条15秒,10000条超过20分钟
- 数据库监控显示:CPU和内存使用率在操作期间飙升
- 网络抓包发现:SQL语句长度超过1MB
关键发现:当单条SQL语句过大时,数据库解析和执行效率会急剧下降
2. 第一轮优化:分批次foreach插入
基于"分而治之"的思路,首先尝试将大数据集拆分为小批次处理:
// 每批处理100条记录 int batchSize = 100; List<List<Product>> batches = ListUtils.partition(fullList, batchSize); batches.forEach(batch -> { productMapper.batchInsert(batch); });优化效果:
- 执行时间从20分钟降至约3分钟
- 数据库负载明显降低
但新问题出现:
- 每次批量插入都需单独事务提交
- 网络往返次数增加(100次 vs 原来的1次)
- 总耗时仍不理想
3. 深入MyBatis执行机制:ExecutorType的选择
MyBatis提供三种执行器类型:
| 执行器类型 | 特点 | 适用场景 |
|---|---|---|
| SIMPLE | 默认模式,每条语句单独执行 | 常规CRUD操作 |
| REUSE | 重用预处理语句 | 相同SQL频繁执行 |
| BATCH | 批量执行所有语句 | 大批量数据操作 |
启用BATCH模式的代码调整:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { ProductMapper mapper = session.getMapper(ProductMapper.class); for (Product product : productList) { mapper.insert(product); } session.flushStatements(); session.commit(); } finally { session.close(); }优化效果:
- 执行时间从3分钟降至45秒
- 数据库连接利用率显著提高
4. JDBC层优化:rewriteBatchedStatements参数
在数据库连接字符串中添加关键参数:
jdbc.url=jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true这个参数的作用机制:
- 将多个INSERT语句重写为单条多值语法
- 减少网络传输开销
- 允许驱动真正批量执行
结合BATCH模式的完整配置:
// 创建支持批量操作的SqlSession SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); // 获取Mapper接口 ProductMapper mapper = session.getMapper(ProductMapper.class); // 设置合适的批处理大小 int batchSize = 100; for (int i = 0; i < productList.size(); i++) { mapper.insert(productList.get(i)); if (i % batchSize == 0 || i == productList.size() - 1) { session.flushStatements(); } } session.commit();优化效果:
- 执行时间从45秒降至15秒
- 内存使用更加稳定
5. 终极方案:MyBatis-Plus批量操作
对于使用MyBatis-Plus的项目,其内置的批量操作方法更加简洁高效:
// 服务层直接调用批量保存方法 productService.saveBatch(productList, 100); // 或者使用Lambda方式 boolean success = SqlHelper.executeBatch(entityClass, log, productList, 100, (sqlSession, entity) -> { sqlSession.insert("com.mapper.ProductMapper.insert", entity); });MyBatis-Plus批量插入的核心优势:
- 自动事务管理:无需手动处理事务边界
- 智能批处理:自动按指定大小分批次提交
- 异常处理:完善的错误回滚机制
- 性能优化:内部采用最优执行策略
最终优化效果:
- 执行时间稳定在6秒左右
- 代码可读性和维护性大幅提升
- 系统资源占用更加合理
6. 性能对比与选型建议
不同方案的性能测试数据(10000条记录):
| 方案 | 执行时间 | 内存峰值 | CPU使用率 | 代码复杂度 |
|---|---|---|---|---|
| 原生foreach | >20min | 高 | 高 | 低 |
| 分批次foreach | ~3min | 中 | 中 | 中 |
| BATCH模式 | ~45s | 中 | 中 | 中 |
| BATCH+rewrite | ~15s | 低 | 低 | 高 |
| MyBatis-Plus | ~6s | 低 | 低 | 低 |
选型建议:
- 中小批量数据(<1000条):简单foreach即可
- 大批量简单数据:BATCH模式+rewrite参数
- 复杂业务场景:MyBatis-Plus批量操作
- 超大数据量(>10万):考虑使用LOAD DATA INFILE等特殊方式
7. 避坑指南与最佳实践
在实际项目中积累的这些经验可能对你有用:
连接池配置要点:
- 适当增大最大连接数
- 设置合理的等待超时时间
- 为批量操作单独配置连接池
# Druid连接池示例配置 spring.datasource.druid.max-active=50 spring.datasource.druid.max-wait=60000 spring.datasource.druid.batch-max-active=10事务管理技巧:
- 批量操作使用独立事务
- 考虑使用编程式事务管理
- 异常处理要确保资源释放
监控与调优:
- 记录每次批量操作的执行时间
- 动态调整batchSize参数
- 关注数据库的max_allowed_packet设置
// 简单的性能监控代码 long start = System.currentTimeMillis(); batchOperation.execute(); long duration = System.currentTimeMillis() - start; log.info("批量操作执行时间:{}ms,处理记录数:{}", duration, batchSize);经过这次优化之旅,最大的体会是:性能优化需要系统化思维,从SQL语句、框架配置、JDBC参数到数据库设置,每个环节都可能成为瓶颈。而正确的监控手段和数据驱动的决策,往往比盲目尝试更有效率。
