1. 从5分钟到3秒:MyBatis批量插入的性能困境与突破
去年双十一大促前,我们的订单系统在压测时暴露了一个致命问题:当批量插入10万条订单明细数据时,MyBatis竟然需要耗时5分钟!这直接导致数据库连接池爆满,整个系统陷入瘫痪。经过三天三夜的紧急优化,最终我们将插入时间压缩到惊人的3秒。这个案例让我深刻认识到,MyBatis批量插入的性能优化不是简单的参数调整,而是需要从多个维度进行系统化改造。
批量插入性能差是MyBatis开发者最常遇到的痛点之一。当数据量达到万级时,原生MyBatis的批量插入往往会表现出以下典型症状:数据库连接长时间占用、内存消耗急剧上升、事务超时风险增加。这些问题的本质原因在于MyBatis默认的批量操作实现机制存在三个关键缺陷:
- JDBC驱动层未启用真批量模式:大多数开发者不知道,MyBatis默认配置下即使使用
<foreach>标签拼接SQL,JDBC驱动仍会逐条发送SQL语句 - 事务管理策略不当:默认情况下每个插入操作都会自动提交,导致频繁的磁盘I/O操作
- 参数重组开销巨大:MyBatis在批量处理时需要反复解析和重组参数对象,这个过程的复杂度是O(n²)
下面我将分享在这次性能攻坚战中验证有效的三个关键优化方案,这些方案在金融、电商等多个行业场景中均实现了90%以上的性能提升。特别说明:所有优化手段均基于标准MyBatis 3.5+版本,无需引入第三方组件,可直接应用于生产环境。
2. 第一关键优化:启用JDBC的真批量模式
2.1 识别伪批量操作
通过Arthas工具监控SQL执行过程,我们发现虽然Mapper接口中使用了@Insert("<foreach>...")的批量语法,但MySQL General Log中显示的却是大量单条INSERT语句。这是因为MyBatis默认的ExecutorType.SIMPLE模式实际上只是做了SQL拼接,没有利用JDBC的addBatch()机制。
关键诊断命令:
watch org.apache.ibatis.session.defaults.DefaultSqlSession update 'params[0].contains("batch")' -x 3
2.2 配置真批量执行器
在SqlSessionFactory构建时显式指定批量执行器:
java复制@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 关键配置:指定ExecutorType.BATCH
sessionFactory.setExecutorType(ExecutorType.BATCH);
return sessionFactory.getObject();
}
同时需要在JDBC连接字符串追加关键参数:
code复制jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=true
2.3 批量提交策略优化
真批量模式下需要手动控制提交频率,建议每1000-5000条提交一次:
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<dataList.size(); i++){
mapper.insert(dataList.get(i));
if(i%1000 == 0 || i == dataList.size()-1){
session.commit();
session.clearCache(); // 防止OOM
}
}
}
实测表明,仅此一项优化就能将10万条数据的插入时间从300秒降至45秒。但要注意:rewriteBatchedStatements参数在不同MySQL版本中的行为有差异,5.7.12+版本效果最佳。
3. 第二关键优化:参数重组机制改造
3.1 原生方式的性能陷阱
MyBatis默认的参数处理方式会导致三个性能瓶颈:
- 每次插入都重新解析SQL模板
- 参数对象被反复序列化/反序列化
- 大量临时对象触发频繁GC
3.2 静态SQL+批量参数方案
改造Mapper XML配置,使用单条INSERT语句配合动态参数:
xml复制<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user(name,age) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name},#{item.age})
</foreach>
</insert>
对应的Java调用代码:
java复制// 单次提交5000条数据
List<User> batchList = new ArrayList<>(5000);
for(User user : sourceList){
batchList.add(user);
if(batchList.size() >= 5000){
userMapper.batchInsert(batchList);
batchList.clear();
}
}
if(!batchList.isEmpty()){
userMapper.batchInsert(batchList);
}
3.3 参数封装技巧
对于复杂对象,建议使用@Param注解明确指定集合类型:
java复制int batchInsert(@Param("list") List<User> users);
这种方案相比默认方式可减少80%的参数处理时间。但需要注意:单条SQL的长度不能超过max_allowed_packet设置(建议调整为16M以上)。
4. 第三关键优化:多线程分片处理
4.1 单线程瓶颈分析
即使用上了前两种优化,单线程处理10万条数据仍需要约15秒。通过JProfiler分析发现,主要耗时在:
- 30% JDBC网络I/O
- 25% 对象序列化
- 20% 事务等待
4.2 安全的多线程实现
采用分库分表的思想,将数据分片后并行处理:
java复制// 创建线程池(根据CPU核数调整)
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
// 数据分片(每片5000条)
List<List<User>> shards = Lists.partition(dataList, 5000);
// 并行处理
List<Future<Integer>> futures = shards.stream()
.map(shard -> executor.submit(() -> {
try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
shard.forEach(mapper::insert);
session.commit();
return shard.size();
}
}))
.collect(Collectors.toList());
// 等待所有任务完成
int total = futures.stream().mapToInt(f -> {
try {
return f.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).sum();
4.3 事务隔离注意事项
多线程环境下需要特别关注:
- 每个线程使用独立的SqlSession
- 连接池大小要足够(建议=线程数×1.5)
- 监控死锁风险(show engine innodb status)
在我们的服务器(8核16G)上,该方案使10万条数据的插入时间从15秒进一步降至3秒。但要注意:线程数并非越多越好,超过CPU核数2倍后反而会因上下文切换导致性能下降。
5. 实战中的意外情况处理
5.1 主键冲突的优雅解决
批量插入时经常会遇到主键冲突,传统方案是逐条捕获异常,但这样会严重拖慢速度。推荐采用以下策略:
sql复制INSERT INTO user(id,name)
VALUES (1,'张三'),(2,'李四')
ON DUPLICATE KEY UPDATE name=VALUES(name);
在MyBatis中的实现:
xml复制<insert id="batchInsertOrUpdate">
INSERT INTO user(id,name) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.id},#{item.name})
</foreach>
ON DUPLICATE KEY UPDATE name=VALUES(name)
</insert>
5.2 大事务处理技巧
当必须保持原子性时,可以采用分批次提交+事务日志的方案:
java复制@Transactional
public void hugeBatchInsert(List<User> users) {
try {
List<List<User>> batches = Lists.partition(users, 1000);
for(List<User> batch : batches) {
userMapper.batchInsert(batch);
// 记录已处理位置
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 更新断点
}
});
}
} catch (Exception e) {
// 根据日志恢复
}
}
5.3 监控指标建议
优化后需要重点关注以下指标:
- 数据库服务器的CPU负载(应低于70%)
- 网络吞吐量(不超过带宽的50%)
- JDBC连接等待时间(<100ms)
- 事务平均持续时间(<1s)
我在实际生产环境中发现,当批量插入性能优化到极致后,数据库的磁盘IO往往会成为新的瓶颈。这时候可以考虑采用SSD存储或者调整InnoDB的刷盘策略(innodb_flush_log_at_trx_commit=2)。但要注意这会在断电时丢失最后1秒的数据,金融类业务需谨慎评估。
