在 《MyBatis基础入门《十一》TypeHandler 详解》 中,我们打通了数据库与 Java 类型的映射通道。
但当面对 导入 10 万条用户数据 、同步大量订单状态 等场景时,逐条执行
insert或update会导致:
- 数据库连接频繁创建/销毁
- 事务提交次数过多
- 网络往返延迟累积
结果:耗时几分钟甚至超时失败!
解决方案 :使用 MyBatis 批量操作(Batch) !
本文将手把手教你实现高性能批量写入,并对比多种方案的优劣。
一、为什么普通循环插入这么慢?
// ❌ 反面教材:逐条插入(10,000 条 ≈ 10,000 次 SQL + 10,000 次网络交互)
for (User user : userList) {
userMapper.insert(user); // 每次都是一次独立 SQL
}
性能瓶颈:
- 每次
insert都是独立事务(自动提交); - JDBC 驱动与数据库多次通信;
- 数据库频繁写 WAL 日志、刷盘。
💡 实测:插入 10,000 条记录,普通方式可能耗时 30s+ ;批量方式可压至 1s 内!
二、方案一:SqlSession 的 Batch Executor(推荐)
MyBatis 提供了 ExecutorType.BATCH 模式,底层使用 JDBC 的 addBatch() + executeBatch()。
步骤 1:获取 Batch 模式的 SqlSession
@Test
public void testBatchInsert() {
// 1. 获取 BATCH 类型的 SqlSession
SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserMapper mapper = batchSqlSession.getMapper(UserMapper.class);
try {
long start = System.currentTimeMillis();
// 2. 循环添加(不立即执行)
for (int i = 1; i <= 10000; i++) {
User user = new User();
user.setUsername("user_" + i);
user.setProfile(new UserProfile("avatar.jpg", "城市" + i));
mapper.insert(user); // 仅加入批处理队列
// 3. 每 1000 条 flush 一次,防止内存溢出
if (i % 1000 == 0) {
batchSqlSession.flushStatements(); // 提交当前批次
}
}
// 4. 提交剩余数据
batchSqlSession.commit();
long time = System.currentTimeMillis() - start;
System.out.println("批量插入 10000 条耗时: " + time + " ms");
} catch (Exception e) {
batchSqlSession.rollback();
throw e;
} finally {
batchSqlSession.close(); // 必须关闭!
}
}
关键点解析:
ExecutorType.BATCH:启用批处理模式;flushStatements():手动触发executeBatch(),释放内存;commit():最终提交事务;- 必须 close():否则资源泄漏!
✅ 优势:
- 仅 1 次事务提交;
- JDBC 驱动合并 SQL,减少网络往返;
- 兼容所有数据库(MySQL、Oracle、PostgreSQL 等)。
三、方案二:XML 中使用 <foreach> 构建单条 INSERT(仅限 MySQL)
适用于 一次性插入固定数量数据(如 100~1000 条)。
Mapper XML:
<insert id="batchInsertWithForeach">
INSERT INTO tbl_user (username, profile) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.profile, typeHandler=JsonTypeHandler})
</foreach>
</insert>
调用:
userMapper.batchInsertWithForeach(userList); // 单次 SQL 插入多行
⚠️ 注意:
- MySQL 默认
max_allowed_packet限制 SQL 大小(默认 64MB);- 超过限制会报错,需分批调用;
- 不支持 Oracle(语法不兼容)。
✅ 适用场景:中小批量、简单结构、MySQL 环境。
四、方案三:Spring Boot + @Transactional 批量(谨慎使用)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void batchInsertInTransaction(List<User> users) {
for (User user : users) {
userMapper.insert(user); // 仍在同一事务中
}
}
}
❗ 问题:
- 虽然事务合并了,但 SQL 仍是逐条发送;
- 无 JDBC Batch 优化,性能提升有限;
- 大数据量易导致 事务日志过大、OOM。
❌ 不推荐用于万级数据!
五、生产环境最佳实践
✅ 1. 分批处理(防 OOM)
- 单批次建议 500~2000 条(根据字段大小调整);
- 使用
flushStatements()主动提交批次。
✅ 2. 关闭自动提交 & 合理设置事务
- Batch 模式下,整个批次为一个事务;
- 若需部分成功,可在外层控制分段提交。
✅ 3. 数据库调优(MySQL 示例)
-- 临时关闭索引更新(插入完成后再重建)
ALTER TABLE tbl_user DISABLE KEYS;
-- 批量插入...
-- 重建索引
ALTER TABLE tbl_user ENABLE KEYS;
或调整参数:
# my.cnf
innodb_flush_log_at_trx_commit = 2 # 安全性换性能
bulk_insert_buffer_size = 256M
🔔 生产环境需 DBA 配合评估风险!
✅ 4. 监控与日志
- 记录每批次耗时、条数;
- 异常时记录失败数据 ID,支持重试。
六、性能对比实测(10,000 条 User)
| 方案 | 耗时 | 事务数 | 网络交互 | 适用场景 |
|---|---|---|---|---|
| 普通循环 insert | ~32,000 ms | 10,000 | 10,000 次 | 小数据量 |
| SqlSession BATCH | ~800 ms | 1 | 1 次 | ✅ 推荐:大数据量 |
<foreach> 单条 INSERT |
~1,200 ms | 1 | 1 次 | 中小批量、MySQL |
| Spring @Transactional 循环 | ~28,000 ms | 1 | 10,000 次 | 不推荐 |
💡 测试环境:MySQL 8.0, HikariCP, 16GB RAM, SSD
七、常见问题解答
❓ Q1:Batch 模式下能获取自增主键吗?
- 不能!JDBC Batch 不支持返回生成的主键;
- 解决方案:先批量插入无主键数据,再通过其他字段查询补全(或改用
<foreach>)。
❓ Q2:如何处理部分失败?
- MyBatis Batch 是"全有或全无";
- 若需部分成功,需在外层按小批次(如 100 条)循环调用,捕获异常后跳过。
❓ Q3:与 PageHelper、插件冲突吗?
- 不冲突,但注意插件逻辑不要阻塞 Batch 执行。
八、总结
| 场景 | 推荐方案 |
|---|---|
| 万级数据导入/同步 | SqlSession(BATCH) + 分批 flush |
| 千级以内、MySQL | <foreach> 单条 INSERT |
| 需要返回主键 | 放弃 Batch,用 <foreach> 或分段普通插入 |
| 高可靠性要求 | 小批次 + 事务 + 失败重试机制 |
✨ 核心口诀 :
"大数据用 BATCH,分批 flush 防 OOM;
小批量用 foreach,主键需求要权衡!"
本文带你掌握 MyBatis 批量操作的性能优化之道,轻松应对海量数据写入挑战。
下一篇我们将深入 MyBatis 与 Lombok、MapStruct 的优雅配合,打造极简 DAO 层!
👍 如果你觉得有帮助,欢迎点赞、收藏、转发!
💬 你在项目中是如何做批量处理的?欢迎评论区分享经验!