news 2026/11/3 16:12:02

MyBatis批量插入性能优化:从5分钟到3秒的实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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语句,每次插入都包含以下开销:

  1. 应用层到数据库的网络往返
  2. SQL解析和优化
  3. 事务处理
  4. 索引维护

假设每条插入平均耗时3ms,10万条就是300秒(5分钟)。在实际测试中,这个数字还会受到数据库负载、网络延迟等因素影响而变得更糟。

2.2 性能瓶颈解析

这种方式的性能瓶颈主要来自:

  1. 网络IO:每次插入都需要完整的请求-响应过程
  2. 事务开销:默认情况下MyBatis自动提交事务,每条insert都是一个独立事务
  3. SQL解析:数据库需要重复解析结构相同的SQL语句

提示:即使将事务改为手动提交,也只是减少了部分事务开销,网络和SQL解析的开销仍然存在。

3. 第一层优化:批量SQL(30秒)

3.1 批量插入实现

MyBatis提供了foreach标签支持批量SQL生成:

<!-- Mapper.xml --> <insert id="batchInsert"> INSERT INTO user (name, age, email) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.age}, #{item.email}) </foreach> </insert>

Java调用代码:

// 分批插入,每批1000条 int batchSize = 1000; for (int i = 0; i < userList.size(); i += batchSize) { int end = Math.min(i + batchSize, userList.size()); List<User> batch = userList.subList(i, end); userMapper.batchInsert(batch); }

3.2 性能提升原理

这种方式将多条insert合并为一条SQL语句,例如:

INSERT INTO user (name, age, email) VALUES ('张三', 25, 'zhangsan@example.com'), ('李四', 30, 'lisi@example.com'), ... ('王五', 28, 'wangwu@example.com');

优势在于:

  1. 减少网络往返次数(从N次变为N/batchSize次)
  2. 数据库只需解析一次SQL
  3. 单次事务提交

实测10万条数据插入时间从300秒降至30秒,提升10倍。

3.3 注意事项

  1. 批大小选择:建议500-1000条/批,过大可能导致:

    • SQL过长超过max_allowed_packet限制
    • 内存占用过高
    • 数据库锁持有时间过长
  2. 主键返回:如果需要获取自增ID,需配置:

    <insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">

    但注意MySQL对批量插入返回自增ID的支持有限制。

4. 第二层优化:JDBC批处理(8秒)

4.1 启用批处理模式

MySQL提供了rewriteBatchedStatements参数优化批处理:

jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true

配合MyBatis的批处理执行器:

public void batchInsertWithExecutor(List<User> 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 技术原理

  1. ExecutorType.BATCH:MyBatis会缓存预处理语句,而不是立即执行
  2. rewriteBatchedStatements:MySQL驱动会将多个INSERT重写为更高效的形式
  3. 分批刷新:定期清空缓存防止内存溢出

这种组合使得实际发送到数据库的SQL数量大幅减少,10万条数据降至8秒完成。

4.3 常见问题排查

  1. 参数不生效检查:

    • 确认连接URL包含rewriteBatchedStatements=true
    • 确认使用了ExecutorType.BATCH
    • MySQL驱动版本建议5.1.13+(8.0+最佳)
  2. 主键返回异常:

    • 批量模式下自增ID可能不连续
    • 解决方案:升级MySQL驱动到8.0.17+
  3. 内存管理:

    • 大批量操作需定期flushStatements
    • 考虑使用clearCache防止二级缓存膨胀

5. 第三层优化:多线程并行(3秒)

5.1 并行处理实现

public void parallelBatchInsert(List<User> userList) { int threadCount = 4; // 根据CPU核心数和连接池大小调整 int batchSize = userList.size() / threadCount; ExecutorService executor = Executors.newFixedThreadPool(threadCount); List<Future<?>> futures = new ArrayList<>(); for (int i = 0; i < threadCount; i++) { int start = i * batchSize; int end = (i == threadCount - 1) ? userList.size() : (i + 1) * batchSize; List<User> 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 关键配置要点

  1. 线程数选择:

    • 不超过数据库连接池最大连接数
    • 建议CPU核心数×1.5
    • 可通过压测找到最优值
  2. 数据分片:

    • 均匀分配数据量到各线程
    • 使用subList避免数据拷贝
  3. 资源管理:

    • 使用CountDownLatch确保所有线程完成
    • 正确关闭线程池

5.3 适用场景与限制

适用场景:

  • 数据量大且可并行处理
  • 对实时性要求高的迁移任务
  • 非事务性操作(或可接受最终一致性)

限制:

  • 需要足够数据库连接资源
  • 可能引发主键冲突(需确保数据分区不重叠)
  • 错误处理更复杂

6. 优化效果对比

优化阶段耗时提升倍数核心优化点
循环单条插入300s1x-
批量SQL30s10x减少网络往返
JDBC批处理8s37x减少SQL解析和事务次数
多线程并行3s100x并行利用多核CPU

7. 完整解决方案代码

@Service public class BatchInsertService { @Autowired private SqlSessionFactory sqlSessionFactory; /** * 高性能批量插入 * 10万条数据约3秒 */ public void highPerformanceBatchInsert(List<User> 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; } List<User> 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(List<User> 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

解决方案:

  1. 减小批处理大小(建议500-1000条/批)
  2. 调整MySQL配置:
    SET GLOBAL max_allowed_packet=256*1024*1024;

8.2 内存溢出问题

现象:

  • OOM异常
  • 应用内存持续增长

解决方案:

  1. 分页读取源数据:
    int pageSize = 10000; int total = countTotal(); for (int i = 0; i < total; i += pageSize) { List<User> page = selectByPage(i, pageSize); batchInsertWithExecutor(page); }
  2. 定期清空MyBatis缓存
  3. 增加JVM堆内存

8.3 主键冲突问题

现象:

  • 多线程插入时出现主键重复
  • 自增ID不连续

解决方案:

  1. 使用UUID或其他分布式ID方案
  2. 确保数据分片不重叠
  3. 使用数据库序列(Oracle)或调整自增步长(MySQL)

9. 优化原则总结

  1. 减少网络往返:批量操作优于单条操作
  2. 降低事务开销:合并事务提交次数
  3. 并行处理:充分利用多核CPU
  4. 资源控制:合理设置批大小和线程数
  5. 监控调整:根据实际运行情况持续优化

在实际项目中,建议从简单的批量SQL开始,逐步应用更高级的优化技术。同时要注意,并非所有场景都适合多线程并行,需要根据具体业务需求和系统资源情况选择合适的方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 16:11:47

LNA噪声最佳点Γₒₚₜ原理与工程设计

简介&#xff1a;本资源是一份面向射频与微波电路设计初学者及工程师的低噪声放大器&#xff08;LNA&#xff09;系统化教学课件&#xff0c;聚焦接收链路前端关键模块的设计原理与工程权衡。内容深入解析噪声系数、等效噪声温度、相关增益、增益平坦度、动态范围及端口匹配等核…

作者头像 李华
网站建设 2026/11/3 16:09:57

机器学习中的微积分基础与不定积分应用

1. 机器学习中的微积分基础微积分作为机器学习的数学基石&#xff0c;其重要性怎么强调都不为过。在深度学习领域&#xff0c;我们几乎每天都在与导数和积分打交道。特别是在训练神经网络时&#xff0c;反向传播算法的核心就是链式求导法则的应用。而不定积分作为微积分的另一大…

作者头像 李华
网站建设 2026/11/3 16:09:11

金融AI数据管道设计:核心架构与6大避坑技巧

1. 项目背景与核心挑战 金融市场的预测分析一直是量化投资领域的圣杯。过去五年间&#xff0c;我们看到越来越多的对冲基金和资产管理公司开始将AI/ML模型纳入他们的交易决策流程。但有意思的是&#xff0c;这些机构在模型开发上投入巨大资源后&#xff0c;往往发现实际效果与预…

作者头像 李华
网站建设 2026/11/3 16:08:10

AI编程时代,为什么扎实编程基础仍是不可替代的核心能力

1. 这不是“替代”&#xff0c;而是“新编程范式”的入场券最近在技术圈刷屏的那句“Elvis Saravia&#xff1a;用 Claude Code 不等于不用学编程”&#xff0c;我第一次看到时就在咖啡机旁停了三秒——不是因为话多&#xff0c;而是因为它精准戳破了一个正在快速膨胀的认知泡沫…

作者头像 李华
网站建设 2026/11/3 16:08:02

大数据预处理技术:从原理到工程实践

1. 数据预处理的核心价值 大数据时代最讽刺的现象莫过于&#xff1a;我们拥有前所未有的海量数据&#xff0c;却常常陷入"数据丰富&#xff0c;信息贫乏"的困境。三年前我参与的一个电商用户行为分析项目就遭遇了典型的数据困境——原始日志中78%的字段存在格式混乱或…

作者头像 李华
网站建设 2026/11/3 16:07:49

纯前端 ORK 目标管理工具:IndexedDB 本地存储与进度计算

目标管理这件事&#xff0c;卡住大多数人的从来不是工具&#xff0c;而是写下第一个目标那一刻的犹豫。我见过的失败案例里&#xff0c;十有八九不是"软件不好用"&#xff0c;而是"软件太重了"——要先注册、建空间、邀请成员、配置权限&#xff0c;折腾半…

作者头像 李华