批量操作性能优化
MyBatis 批量操作有 BATCH 执行器和 foreach 拼接两种主流方案,各自适用不同场景。本文从执行效率、内存消耗、功能限制等维度进行对比分析,给出不同数据量下的最优方案选择。
BATCH 执行器 vs foreach 方式对比
| 维度 | ExecutorType.BATCH | foreach 拼接 SQL |
|---|---|---|
| 实现原理 | JDBC addBatch() + executeBatch() | XML 循环拼接多值 INSERT 语句 |
| SQL 条数 | 1 条 PreparedStatement,多次传参 | 1 条大 SQL,含多个 VALUES 子句 |
| 网络往返 | 1 次 | 1 次 |
| 数据库编译 | 编译 1 次 | 编译 1 次 |
| 获取自增主键 | 不支持(MySQL) | 支持 |
| 动态 SQL | 不支持(SQL 已预编译) | 支持(运行时拼接) |
| 单次数据量上限 | 受内存限制,建议分批 | 受 max_allowed_packet 限制 |
| 内存消耗 | 低(不堆积 SQL 字符串) | 高(需拼接完整 SQL 字符串) |
| 代码复杂度 | 低(Mapper 写法不变) | 中(需编写 foreach XML) |
ExecutorType.BATCH 方案
标准批量插入
Java
public void batchInsert(List<User> users) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.commit();
} finally {
session.close();
}
}
Mapper XML 与普通单条插入完全相同:
XML
<insert id="insert" parameterType="User">
INSERT INTO user (username, email, create_time)
VALUES (#{username}, #{email}, #{createTime})
</insert>
分批提交控制
数据量较大时,避免 BATCH 队列在内存中堆积,需设置合理的分批大小:
Java
public void batchInsertLarge(List<User> users) {
int batchSize = 500;
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.insert(users.get(i));
if ((i + 1) % batchSize == 0) {
session.commit();
session.clearCache();
}
}
session.commit();
} finally {
session.close();
}
}
注意:BATCH 模式下每次
commit()后必须调用clearCache()清理一级缓存,否则缓存对象持续占用内存,数据量大时可能触发 OOM。
批量更新
Java
public void batchUpdate(List<User> users) {
int batchSize = 300;
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.update(users.get(i));
if ((i + 1) % batchSize == 0) {
session.commit();
session.clearCache();
}
}
session.commit();
} finally {
session.close();
}
}
foreach 批量插入方案
多值 INSERT 拼接
XML
<insert id="batchInsertForeach" parameterType="list">
INSERT INTO user (username, email, create_time)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.createTime})
</foreach>
</insert>
Java 调用侧保持常规 SqlSession:
Java
public void batchInsertWithForeach(List<User> users) {
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.batchInsertForeach(users);
session.commit();
} finally {
session.close();
}
}
分批处理(突破 max_allowed_packet 限制)
MySQL 的 max_allowed_packet 默认 4MB,单条 SQL 过大时会报 Packet for query is too large:
Java
public void batchInsertInChunks(List<User> users) {
int chunkSize = 200; // 每 200 条拼接为一条 SQL
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i += chunkSize) {
int end = Math.min(i + chunkSize, users.size());
List<User> chunk = users.subList(i, end);
mapper.batchInsertForeach(chunk);
}
session.commit();
} finally {
session.close();
}
}
批量大小选择策略
| 数据量 | 推荐方案 | batchSize/chunkSize | 说明 |
|---|---|---|---|
| < 100 | foreach 或 BATCH 均可 | - | 差异不明显 |
| 100 ~ 1,000 | BATCH 执行器 | 500 | 内存低,代码简洁 |
| 1,000 ~ 10,000 | BATCH 执行器 | 500 | 分批提交,避免 OOM |
| 10,000 ~ 100,000 | BATCH 执行器 | 1,000 | 增大批次减少 commit 次数 |
| > 100,000 | BATCH + 多线程 | 2,000 ~ 5,000 | 配合线程池并发写入 |
注意:batchSize 并非越大越好。过大会导致单次 commit 锁表时间过长,影响并发读写;过小则 commit 频繁,性能下降。建议 500 ~ 1,000 为起点,结合实际压测调整。
事务边界控制
正确做法:整个批量操作在单一事务中
Java
// 正确:一个事务覆盖整个批次
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.commit(); // 全部成功才提交
} catch (Exception e) {
session.rollback(); // 任一条失败则回滚全部
} finally {
session.close();
}
错误做法:每条数据独立提交
Java
// 错误:逐条 commit,破坏原子性
for (User user : users) {
SqlSession session = sqlSessionFactory.openSession();
try {
mapper.insert(user);
session.commit(); // 每条独立事务
} finally {
session.close();
}
}
逐条提交的问题:
- 无法保证整体一致性,中途失败导致部分写入
- 每条事务的 WAL 刷盘开销累加,总耗时远高于单次大事务
- 无法利用批量预写日志的合并写特性
半事务模式:部分成功可接受
某些场景允许部分失败(如日志采集、数据同步),可记录失败项后继续:
Java
public BatchResult batchInsertWithFailures(List<User> users) {
int batchSize = 500;
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
List<Integer> failedIndexes = new ArrayList<>();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
try {
mapper.insert(users.get(i));
if ((i + 1) % batchSize == 0) {
session.commit();
session.clearCache();
}
} catch (Exception e) {
failedIndexes.add(i);
session.rollback();
session.clearCache();
}
}
session.commit();
} finally {
session.close();
}
return new BatchResult(users.size(), failedIndexes);
}
注意:此模式下需要在每次异常后 rollback 当前批次,确保未提交数据不会残留。
性能压测对比
以 MySQL 8.0 插入 10,000 条记录为例:
| 方案 | 耗时 | 内存峰值 | 说明 |
|---|---|---|---|
| 逐条 SIMPLE 插入 | ~8.5s | 低 | 基准对照 |
| foreach 拼接(10,000 条) | 报错 | 高 | 超出 max_allowed_packet |
| foreach 分 chunk(200条/chunk) | ~1.2s | 中 | 50 次 SQL 提交 |
| BATCH(500/批) | ~0.8s | 低 | 20 次 commit |
| BATCH(1,000/批) | ~0.6s | 中 | 10 次 commit |
注意:压测数据基于本地单实例 MySQL,实际性能受网络延迟、磁盘 IO、并发连接数等影响,应以实际环境压测为准。
Spring 事务整合
使用 Spring @Transactional 时,需要注意 BATCH 模式的兼容性:
Java
@Service
public class UserService {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
@Transactional(rollbackFor = Exception.class)
public void batchInsertWithSpring(List<User> users) {
// Spring 管理的 SqlSession 需手动切换 BATCH
SqlSession session = new SqlSessionTemplate(
sqlSessionTemplate.getSqlSessionFactory(),
ExecutorType.BATCH
);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
int batchSize = 500;
for (int i = 0; i < users.size(); i++) {
mapper.insert(users.get(i));
if ((i + 1) % batchSize == 0) {
session.commit();
session.clearCache();
}
}
session.commit();
} finally {
session.close();
}
}
}
Java
@Configuration
public class MyBatisConfig {
@Bean("batchSqlSessionTemplate")
public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory factory) {
return new SqlSessionTemplate(factory, ExecutorType.BATCH);
}
}
要点总结
- BATCH 执行器内存占用低、代码简洁,是批量写入首选方案
- foreach 拼接适合需要获取自增主键或使用动态 SQL 的场景
- 批量大小建议 500 ~ 1,000 起步,结合压测数据微调
- 整个批量操作应保持在单一事务中,确保原子性
- BATCH 模式 commit 后必须
clearCache(),避免一级缓存内存泄漏 - foreach 方案需注意 MySQL
max_allowed_packet限制,合理分 chunk - 大数据量可结合多线程 + BATCH 执行器实现并发写入
📝 发现内容有误?点击此处直接编辑