1. 为什么MyBatis批量插入需要优化
在数据量较大的业务场景中,批量插入操作是数据库性能优化的关键点。很多开发者习惯使用MyBatis的foreach标签来实现批量插入,这其实是一个典型的性能陷阱。我曾在电商平台的订单系统中,因为这个问题导致高峰期数据库连接池被耗尽,后来通过深入分析找到了更优方案。
1.1 foreach方式的性能瓶颈
foreach方式的典型写法是这样的:
<insert id="batchInsert"> INSERT INTO user(name,age) VALUES <foreach collection="list" item="item" separator=","> (#{item.name},#{item.age}) </foreach> </insert>这种写法会产生一个超长的SQL语句,比如插入1000条记录就会生成包含1000个VALUES子句的SQL。主要问题在于:
- SQL长度限制:某些数据库对SQL语句长度有限制,MySQL默认是4MB
- 网络传输开销:大SQL需要更多网络传输时间
- 解析成本高:数据库需要解析超长SQL,消耗CPU资源
- 事务回滚困难:如果中间某条记录出错,整个批量操作都会失败
1.2 更优的Batch模式方案
MyBatis其实内置了更高效的批量处理机制 - BatchExecutor。通过调整ExecutorType为BATCH,可以实现真正的批处理操作:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = session.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } session.commit(); } finally { session.close(); }这种方式会:
- 预编译SQL语句一次
- 多次发送参数集合
- 最后统一提交
- 显著减少网络往返和SQL解析开销
2. 三种执行器类型深度对比
MyBatis提供了三种ExecutorType,理解它们的区别对性能优化至关重要:
2.1 SIMPLE执行器
默认模式,每次执行都会创建新的PreparedStatement对象。适合常规的单条操作,但在批量场景下性能最差。
2.2 REUSE执行器
重用PreparedStatement对象,对于相同的SQL语句只会预编译一次。比SIMPLE模式节省了预编译开销,但每次执行仍然会单独发送到数据库。
2.3 BATCH执行器
真正的批处理模式,会将多个操作合并后一次性发送。实测在插入1000条记录时,性能比foreach方式提升3-5倍。
| 执行器类型 | 预编译次数 | 网络请求次数 | 适用场景 |
|---|---|---|---|
| SIMPLE | 每次执行 | 每次执行 | 常规单条操作 |
| REUSE | 首次执行 | 每次执行 | 相同SQL频繁执行 |
| BATCH | 首次执行 | 批量提交 | 大批量写操作 |
3. 实战:批量插入性能优化方案
3.1 基础Batch模式实现
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(); } catch (Exception e) { session.rollback(); throw e; } finally { session.close(); } }关键点:
- 使用ExecutorType.BATCH创建SqlSession
- 在循环中调用mapper方法
- 最后统一commit
- 必须处理异常和资源释放
3.2 分批次提交优化
对于超大数量级的批量插入(如10万+),建议分批次提交:
public void batchInsert(List<User> users, int batchSize) { 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 % batchSize == 0 || i == users.size() - 1) { session.commit(); session.clearCache(); // 防止OOM } } } finally { session.close(); } }3.3 与Spring集成方案
在Spring环境中,可以通过配置注入Batch模式的SqlSessionTemplate:
@Bean public SqlSessionTemplate batchSqlSessionTemplate() { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); } @Service public class UserService { @Autowired @Qualifier("batchSqlSessionTemplate") private SqlSessionTemplate sqlSessionTemplate; public void batchInsert(List<User> users) { UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class); // ...批量操作逻辑 } }4. 性能实测与对比数据
我在本地环境(MySQL 5.7,100M网络)进行了三种方式的性能对比测试:
| 操作方式 | 1000条耗时(ms) | 10000条耗时(ms) | 内存占用 |
|---|---|---|---|
| foreach | 1200 | 超时(>60s) | 高 |
| SIMPLE | 850 | 7800 | 中 |
| BATCH | 280 | 2100 | 低 |
关键发现:
- 小批量时(100条内)差异不大
- 超过500条时BATCH优势明显
- foreach方式在大批量时完全不可用
5. 常见问题与解决方案
5.1 批量操作获取主键ID
使用Batch模式时,无法像常规插入那样立即获取生成的主键。解决方案:
- 预先分配ID(如UUID或雪花算法)
- 批量插入后通过查询条件获取
- 使用数据库特定语法(如MySQL的LAST_INSERT_ID(expr))
5.2 事务管理注意事项
- Batch模式的事务是统一提交的,中间出错会全部回滚
- 在Spring中需要将事务管理器与Batch模式配合使用
- 考虑使用编程式事务控制更精细的提交点
5.3 内存溢出预防
长时间运行的批量操作可能导致内存溢出:
- 定期clearCache()
- 分批次提交
- 监控JVM内存使用
- 考虑使用游标方式处理超大数据集
6. 高级优化技巧
6.1 JDBC参数调优
在连接字符串中添加批处理优化参数:
rewriteBatchedStatements=true allowMultiQueries=true useServerPrepStmts=true cachePrepStmts=true6.2 批量大小选择
最优批量大小取决于:
- 数据库类型(MySQL/Oracle等)
- 网络延迟
- 记录字段大小
- 通常500-2000是不错的起点
6.3 多线程批量插入
对于超大数据集,可以结合线程池并行处理:
ExecutorService executor = Executors.newFixedThreadPool(4); List<Future<?>> futures = new ArrayList<>(); int perThread = totalSize / 4; for (int i = 0; i < 4; i++) { int from = i * perThread; int to = (i == 3) ? totalSize : (i+1)*perThread; futures.add(executor.submit(() -> batchInsert(users.subList(from, to)))); } for (Future<?> future : futures) { future.get(); }7. 不同数据库的特别优化
7.1 MySQL优化
- 使用rewriteBatchedStatements参数
- 考虑LOAD DATA INFILE替代SQL插入
- 调整max_allowed_packet参数
7.2 Oracle优化
- 使用OCI批处理接口
- 考虑数组绑定方式
- 调整SESSION_CACHED_CURSORS参数
7.3 PostgreSQL优化
- 使用COPY命令替代INSERT
- 调整work_mem参数
- 考虑使用UNNEST函数
8. 监控与问题排查
8.1 性能监控指标
- 批处理执行时间
- 批处理成功率
- 数据库服务器CPU/内存使用
- 网络IO情况
8.2 常见错误排查
- 批量过大导致内存溢出:减小batchSize
- 部分失败导致全部回滚:检查数据有效性
- 性能未达预期:检查JDBC参数和网络状况
- 主键冲突:确保ID生成策略正确
9. 替代方案评估
9.1 MyBatis-Plus的批量操作
MyBatis-Plus提供了更简洁的批量操作方法:
userService.saveBatch(userList, batchSize);9.2 原生JDBC批处理
对于极致性能需求,可以考虑直接使用JDBC批处理:
Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("INSERT..."); for (User user : users) { ps.setString(1, user.getName()); // ...其他参数 ps.addBatch(); } ps.executeBatch();9.3 数据库特有批量工具
- MySQL的LOAD DATA INFILE
- Oracle的SQL*Loader
- PostgreSQL的COPY命令
10. 最佳实践总结
经过多个项目的实践验证,我总结出以下最佳实践:
- 小于100条:可以使用foreach方式(代码简单)
- 100-5000条:使用MyBatis Batch模式
- 5000+条:分批次使用Batch模式
- 10万+条:考虑数据库特有批量工具
- 始终监控内存使用和性能指标
- 根据数据库类型调整优化参数
- 在测试环境验证批量大小阈值
- 实现优雅的失败处理和重试机制