引言
在实际开发中,我们经常会遇到批量数据写入/更新的场景,比如初始化基础数据、日志批量入库、批量同步第三方数据等。这时候如果直接用MyBatis Plus默认的saveBatch
或saveOrUpdateBatch
,往往会遇到执行慢、内存溢出、数据库压力大等问题。今天就笔者结合项目实战,分享一套亲测有效的MyBatis Plus批量操作优化方案,帮你把百万数据插入从“分钟级”干到“秒级”!
一、先吐槽:默认批量操作的坑,你踩过几个?
MyBatis Plus的BaseMapper
提供了saveBatch
(插入)、saveOrUpdateBatch
(更新或插入)方法,看似方便,但在大数据量场景下简直是“性能杀手”。我之前踩过的坑主要有3个:
1. 逐条SQL执行,网络IO爆炸
默认情况下(没做任何配置),MyBatis Plus会用foreach
拼接成多条单条INSERT
语句,比如:
INSERT INTO user (name,age) VALUES ('张三',20);
INSERT INTO user (name,age) VALUES ('李四',22);
...
假设要插入1万条数据,就要和数据库交互1万次!每次交互都要经历TCP连接、数据传输、SQL解析,这时间浪费得让人心疼。
2. 不支持数据库原生批量语法
不同数据库有更高效的批量写入方式:
- MySQL支持
INSERT INTO t (a,b) VALUES (1,2),(3,4)...
(多值语法); - PostgreSQL支持
INSERT ... ON CONFLICT
(批量Upsert); - Oracle支持
MERGE INTO
(合并插入)。
但默认的saveBatch
根本没用这些语法,白白浪费了数据库的性能优势。
3. 大数据量直接OOM
如果要插入10万条数据,saveBatch
会把所有数据一次性加载到内存,拼接成超长的SQL。这时候要么数据库报SQL长度限制
错误,要么JVM内存直接爆掉,尤其在生产环境容易翻车。
二、实战优化方案:从“逐条插”到“批量飙车”
针对上面的痛点,我总结了4个核心优化策略,覆盖插入、更新场景,亲测百万数据量效率提升10倍+!
策略1:用数据库原生批量语法,替代逐条SQL(必做!)
原理:用数据库支持的多值插入语法,把N条INSERT
合并成1条SQL,减少网络IO次数。
适用场景:纯插入操作(无重复数据)。
步骤1:自定义Mapper方法
在你的UserMapper
接口里新增一个批量插入方法:
public interface UserMapper extends BaseMapper<User> {
// 批量插入(MySQL多值语法)
void batchInsert(@Param("list") List<User> list);
}
步骤2:写XML实现批量SQL
在UserMapper.xml
中,用<foreach>
标签拼接多值插入:
<insert id="batchInsert">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
步骤3:调用自定义方法
在Service里直接调用batchInsert
,代替原来的saveBatch
:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void batchAddUsers(List<User> users) {
userMapper.batchInsert(users); // 直接调用自定义批量方法
}
}
效果:实测1万条数据插入,耗时从默认的800ms降到80ms,直接快10倍!
策略2:大数据量分批处理,避免OOM(防崩!)
问题:即使用了多值插入,一次性插10万条数据,数据库可能超时,JVM也可能内存溢出。
解决:把数据按批次拆分,比如每批1000条,循环插入。
手动分批(简单粗暴)
用subList
切分集合,循环调用批量方法:
public void batchInsertByBatch(List<User> users, int batchSize) {
int total = users.size();
for (int i = 0; i < total; i += batchSize) {
int end = Math.min(i + batchSize, total);
List<User> subList = users.subList(i, end); // 注意:subList是视图,别修改原集合!
userMapper.batchInsert(subList);
}
}
自动分批(MyBatis Plus 3.5.3+)
MyBatis Plus内置了SqlHelper.batch()
工具,自动分批处理,更省心:
@Transactional
public void autoBatchInsert(List<User> users) {
// 每批1000条,自动循环插入
boolean success = SqlHelper.batch(() -> {
// 这里传入当前批次的数据(需要自己切分,或者用流式处理)
List<User> batchList = users.stream().limit(1000).collect(Collectors.toList());
userMapper.batchInsert(batchList);
});
if (!success) {
throw new RuntimeException("批量插入失败");
}
}
注意:分批大小建议根据数据库性能调整,MySQL推荐1000-2000条/批,Oracle可能更小(500-1000)。
策略3:Upsert操作用数据库原生语法(避坑!)
痛点:saveOrUpdateBatch
默认先查是否存在,再决定插入或更新,大数据量时“先查”操作会额外消耗大量SQL查询时间。
优化:用数据库原生的UPSERT
语法(如MySQL的ON DUPLICATE KEY UPDATE
),一条SQL搞定插入或更新。
步骤1:自定义Upsert方法
public interface UserMapper extends BaseMapper<User> {
void batchUpsert(@Param("list") List<User> list);
}
步骤2:XML实现批量Upsert(MySQL示例)
<insert id="batchUpsert">
INSERT INTO user (id, name, age)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.id}, #{user.name}, #{user.age})
</foreach>
ON DUPLICATE KEY UPDATE
name = VALUES(name), -- 存在则更新name
age = VALUES(age) -- 存在则更新age
</insert>
效果:原本需要N次查询+N次插入/更新的1万条数据,现在1条SQL搞定,耗时从500ms降到50ms!
策略4:事务+执行器优化(锦上添花)
事务包裹批量操作
批量操作一定要在事务里执行!否则每条SQL都会自动提交事务,频繁IO更慢。
Spring Boot中直接加@Transactional
即可:
@Transactional(rollbackFor = Exception.class)
public void batchInsertWithTx(List<User> users) {
userMapper.batchInsert(users);
}
配置MyBatis BATCH执行器(适合更新多)
MyBatis的BATCH
执行器会把多个SQL缓存在内存,最后一起提交,减少数据库交互。
配置方式(Spring Boot):
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 关键配置:设置批量执行器
Configuration configuration = new Configuration();
configuration.setDefaultExecutorType(ExecutorType.BATCH);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
注意:BATCH
执行器对插入优化有限(不如多值插入),但批量更新时效果明显!
三、数据库参数调优(加分项)
除了代码层面,数据库本身的参数也会影响批量操作性能。以MySQL为例:
参数 | 推荐值 | 说明 |
---|---|---|
innodb_buffer_pool_size | 内存的50%-70% | 增大缓冲池,减少磁盘IO |
innodb_flush_log_at_trx_commit | 2(非生产环境) | 每秒刷日志,比1(每次提交刷)更快,但可能丢1秒数据 |
max_allowed_packet | 1G(根据数据量调整) | 增大SQL包大小,避免“Packet too large”错误 |
四、总结:批量操作优化口诀
记住这4点,批量操作性能无忧:
- 多值插入:用数据库原生语法,减少网络IO;
- 分批处理:大数据量切分,避免OOM和超时;
- Upsert原生:替代逐条查询,一条SQL搞定;
- 事务+执行器:事务包裹减少提交,BATCH执行器优化更新。
最后提醒:优化前先用EXPLAIN
分析SQL执行计划,确认索引是否合理(批量插入时建议暂时禁用非关键索引,插入后重建)。实际效果可能因数据库版本、数据量、硬件配置不同有差异,建议先小批量测试再上线!