批量操作事务控制
批量操作涉及大量数据写入,事务控制策略直接影响数据一致性和系统稳定性。本文讲解如何在批量操作中平衡事务范围、内存占用与执行性能。
单事务 vs 分块事务
单事务(全量提交)
Java
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : allUsers) {
mapper.insertUser(user);
}
session.commit();
} finally {
session.close();
}
| 优点 | 缺点 |
|---|---|
| 原子性保证,要么全成功要么全失败 | 事务持有锁时间长,影响并发 |
| 失败可回滚 | 10 万条数据全部失败才回滚 |
| 内存中堆积全部 SQL,可能 OOM | |
| 数据库 undo log 膨胀 |
分块事务(分批提交)
Java
public void batchInsertWithChunks(List<User> users) {
int chunkSize = 500;
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.insertUser(users.get(i));
if ((i + 1) % chunkSize == 0) {
session.commit();
session.clearCache();
}
}
session.commit();
} finally {
session.close();
}
}
| 优点 | 缺点 |
|---|---|
| 事务持有锁时间短 | 部分成功部分失败,不具备全量原子性 |
| 内存可控 | 失败后已提交的无法回滚 |
| 减少 undo log 压力 |
注意:分块事务牺牲了全量原子性,换取了更好的性能和稳定性。是否接受取决于业务场景。
分块大小选择
分块大小的选择是性能与稳定性的权衡:
| 分块大小 | 适用场景 |
|---|---|
| 100-200 | 每行数据较大(含大字段)、数据库响应慢 |
| 500 | 通用推荐值,平衡性能与内存 |
| 1000-2000 | 数据行小、数据库性能好、内存充足 |
| 5000+ | 极端场景,需充分测试 |
推荐:从 500 开始,根据实际压测结果调整。
Spring 事务中的批量操作
@Transactional 分块处理
Java
@Service
public class UserBatchService {
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
public void batchInsert(List<User> users) {
int chunkSize = 500;
for (int i = 0; i < users.size(); i++) {
userMapper.insertUser(users.get(i));
if ((i + 1) % chunkSize == 0) {
// Spring 事务会在方法结束时统一提交
// 如需中途刷新,需使用自定义方式
}
}
}
}
注意:
@Transactional注解会在方法结束时统一提交,中途无法分块提交。如需分块事务,不能依赖@Transactional,需手动管理 SqlSession。
手动事务分块
Java
public void batchInsertInChunksWithManualTransaction(List<User> users) {
int chunkSize = 500;
for (int start = 0; start < users.size(); start += chunkSize) {
int end = Math.min(start + chunkSize, users.size());
List<User> chunk = users.subList(start, end);
processChunk(chunk);
}
}
private void processChunk(List<User> chunk) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : chunk) {
mapper.insertUser(user);
}
session.commit();
} catch (Exception e) {
session.rollback();
throw e;
} finally {
session.close();
}
}
异常处理策略
单条失败的处理
Java
public void batchInsertWithErrorHandling(List<User> users) {
int chunkSize = 500;
List<User> failedUsers = new ArrayList<>();
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
try {
mapper.insertUser(users.get(i));
if ((i + 1) % chunkSize == 0) {
session.commit();
session.clearCache();
}
} catch (Exception e) {
failedUsers.add(users.get(i));
// 记录失败数据,继续处理下一条
}
}
session.commit();
} finally {
session.close();
}
// 处理失败的数据
if (!failedUsers.isEmpty()) {
logFailedUsers(failedUsers);
}
}
注意:BATCH 模式下,单条 SQL 失败不会立即抛出异常,而是在
commit()时统一暴露。上述代码适用于 SIMPLE/REUSE 模式。
性能调优建议
1. 关闭不必要的日志
批量操作时,SQL 日志输出会成为性能瓶颈:
XML
<settings>
<!-- 批量操作期间可降低日志级别 -->
<setting name="logImpl" value="NO_LOGGING"/>
</settings>
2. 调整数据库参数
| 数据库 | 参数 | 建议 |
|---|---|---|
| MySQL | innodb_flush_log_at_trx_commit | 批量期间设为 2,减少磁盘 IO |
| MySQL | sync_binlog | 批量期间设为 0,减少 binlog 刷盘 |
| 通用 | 适当调大 innodb_log_file_size | 减少 redo log 切换 |
3. 使用 rewriteBatchedStatements(MySQL 专用)
text
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
开启后,MySQL 驱动会将多条 INSERT 重写为单条多值 INSERT,大幅提升 BATCH 模式性能。
要点总结
- 全量事务保证原子性但占用资源大,分块事务性能好但不具备全量原子性
- 分块大小推荐从 500 开始,根据压测调整
- Spring
@Transactional无法中途分块提交,需手动管理 SqlSession - 每批 commit 后调用 clearCache() 清理缓存
- BATCH 模式下异常在 commit() 时统一暴露
- MySQL 开启
rewriteBatchedStatements=true大幅提升 BATCH 性能 - 批量操作期间可降低日志级别,减少 IO 开销
📝 发现内容有误?点击此处直接编辑