三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MyBatis批量插入踩坑实录:从‘20分钟’优化到‘6秒’,我都经历了什么?

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批量插入的核心优势:

  1. 自动事务管理:无需手动处理事务边界
  2. 智能批处理:自动按指定大小分批次提交
  3. 异常处理:完善的错误回滚机制
  4. 性能优化:内部采用最优执行策略

最终优化效果:

  • 执行时间稳定在6秒左右
  • 代码可读性和维护性大幅提升
  • 系统资源占用更加合理

6. 性能对比与选型建议

不同方案的性能测试数据(10000条记录):

方案执行时间内存峰值CPU使用率代码复杂度
原生foreach>20min
分批次foreach~3min
BATCH模式~45s
BATCH+rewrite~15s
MyBatis-Plus~6s

选型建议

  1. 中小批量数据(<1000条):简单foreach即可
  2. 大批量简单数据:BATCH模式+rewrite参数
  3. 复杂业务场景:MyBatis-Plus批量操作
  4. 超大数据量(>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参数到数据库设置,每个环节都可能成为瓶颈。而正确的监控手段和数据驱动的决策,往往比盲目尝试更有效率。

← 返回列表