结论:应使用 executortype.batch + 分批 flushstatements(),而非一次性 foreach 拼接万级 sql;因 foreach 在内存中拼接超长字符串易引发 oom、sql 解析压力大、事务粒度不可控,而 batch 复用 preparedstatement、内存稳定、可控性强。

直接说结论:用 ExecutorType.BATCH + 分批 flushStatements(),而不是把全部万级数据一次性塞进 foreach 或单次 SqlSession 提交。内存爆掉,90% 是因为没控制批次大小或没及时刷出。
为什么 foreach 批量插入容易 OOM?
MyBatis 的 <foreach></foreach> 是在 Java 内存里拼 SQL 字符串,1 万条记录、每条 200 字节,光 SQL 文本就占 2MB;字段再多、字符串更长,加上 JDBC 驱动内部缓存、MySQL 协议包封装,很容易突破 JVM 堆上限。尤其当 max_allowed_packet 设置偏小(默认 4MB),还会直接报错 Packet for query is too large。
- SQL 拼接不释放中间字符串对象,GC 压力大
- MySQL 服务端解析超长 SQL 也耗内存,可能触发
net_buffer_length重分配 - 无法控制事务粒度 —— 一条失败,整批回滚,重试成本高
用 ExecutorType.BATCH 真正节省的是什么?
它复用同一个 PreparedStatement,靠 JDBC 的 addBatch() 缓存参数,不拼完整 SQL 字符串,内存占用稳定在几百 KB 级别。关键不是“快”,而是“可控”。
- 每调一次
mapper.insert(user),只是把参数塞进本地 batch list,不发网络请求 -
flushStatements()才真正触发executeBatch(),此时才打包发给 MySQL - 必须手动
commit(),否则事务不提交;不close()会泄漏数据库连接 - 推荐每 500–1000 条 flush 一次,兼顾吞吐与内存 —— 试过 2000 条,在某些 JDK 版本下 GC pause 明显变长
rewriteBatchedStatements=true 这个 URL 参数不能少
MySQL 官方 JDBC 驱动(mysql-connector-java 8.0+)默认不启用批量优化。不加这个参数,即使你用了 ExecutorType.BATCH,驱动仍会把每条 addBatch() 拆成独立 INSERT 发送,性能退化回逐条插入。
- 配置示例:
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true&useSSL=false&serverTimezone=Asia/Shanghai - 它让驱动把同结构的多条 INSERT 合并为
INSERT ... VALUES (...), (...), (...)形式再发送 - 注意:仅对
INSERT生效,UPDATE/DELETE不合并
容易被忽略的三个细节
实际压测中,很多团队卡在这三点上:一是忘了关自动提交(autoCommit=false),导致每条 insert() 都隐式 commit;二是没设 fetchSize 或 useServerPrepStmts=true,预编译失效;三是没调 sqlSessionFactory.openSession(ExecutorType.BATCH),而是误用默认 SIMPLE 模式 Session —— 后者根本不会走 batch 流程。
最稳的写法是显式管理 SqlSession 生命周期,别依赖 Spring 的 @Transactional 自动代理,因为事务传播行为和 batch session 容易冲突。











