7人参与 • 2026-01-31 • Java
上周接了个数据迁移的活,要把10万条数据从老系统导入新系统。
写了个简单的批量插入,跑起来一看——5分钟。
领导说太慢了,能不能快点?
折腾了一下午,最后优化到3秒,记录一下过程。
最开始写的很简单,foreach循环插入:
// 方式1:循环单条插入(最慢)
for (user user : userlist) {
usermapper.insert(user);
}
10万条数据,每条都要走一次网络请求、一次sql解析、一次事务提交。
算一下:假设每条插入需要3ms,10万条就是300秒 = 5分钟。
这是最蠢的写法,但我见过很多项目都这么写。
把循环插入改成批量sql:
<!-- mapper.xml -->
<insert id="batchinsert">
insert into user (name, age, email) values
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.age}, #{item.email})
</foreach>
</insert>
// 分批插入,每批1000条
int batchsize = 1000;
for (int i = 0; i < userlist.size(); i += batchsize) {
int end = math.min(i + batchsize, userlist.size());
list<user> batch = userlist.sublist(i, end);
usermapper.batchinsert(batch);
}
从5分钟降到30秒,提升10倍。
原理:一条sql插入多条数据,减少网络往返次数。
但还有问题:30秒还是太慢。
mysql有个参数叫rewritebatchedstatements,开启后可以把多条insert合并成一条。
jdbc:mysql://localhost:3306/test?rewritebatchedstatements=true
@autowired
private sqlsessionfactory sqlsessionfactory;
public void batchinsertwithexecutor(list<user> userlist) {
try (sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch)) {
usermapper mapper = sqlsession.getmapper(usermapper.class);
int batchsize = 1000;
for (int i = 0; i < userlist.size(); i++) {
mapper.insert(userlist.get(i));
if ((i + 1) % batchsize == 0) {
sqlsession.flushstatements();
sqlsession.clearcache();
}
}
sqlsession.flushstatements();
sqlsession.commit();
}
}
从30秒降到8秒。
原理:executortype.batch模式下,mybatis会缓存sql,最后一次性发送给数据库执行。配合rewritebatchedstatements=true,mysql驱动会把多条insert合并。
8秒还是不够快,上多线程:
public void parallelbatchinsert(list<user> userlist) {
int threadcount = 4; // 根据数据库连接池大小调整
int batchsize = userlist.size() / threadcount;
executorservice executor = executors.newfixedthreadpool(threadcount);
list<future<?>> futures = new arraylist<>();
for (int i = 0; i < threadcount; i++) {
int start = i * batchsize;
int end = (i == threadcount - 1) ? userlist.size() : (i + 1) * batchsize;
list<user> sublist = userlist.sublist(start, end);
futures.add(executor.submit(() -> {
batchinsertwithexecutor(sublist);
}));
}
// 等待所有任务完成
for (future<?> future : futures) {
try {
future.get();
} catch (exception e) {
thrownew runtimeexception(e);
}
}
executor.shutdown();
}
从8秒降到3秒。
注意事项:
| 方案 | 耗时 | 提升倍数 |
|---|---|---|
| 循环单条插入 | 300秒 | 基准 |
| 批量sql | 30秒 | 10倍 |
| jdbc批处理 | 8秒 | 37倍 |
| 多线程并行 | 3秒 | 100倍 |
<foreach collection="list" item="item" separator=",">
如果一次插入太多条,sql会非常长,可能超过max_allowed_packet限制。
解决: 分批插入,每批500-1000条。
检查几个点:
rewritebatchedstatements=trueexecutortype.batch批量插入时想获取自增主键:
<insert id="batchinsert" usegeneratedkeys="true" keyproperty="id">
注意:rewritebatchedstatements=true时,自增主键返回可能有问题,需要升级mysql驱动到8.0.17+。
10万条数据一次性加载到内存,可能oom。
解决:分页读取 + 分批插入。
int pagesize = 10000;
int total = counttotal();
for (int i = 0; i < total; i += pagesize) {
list<user> page = selectbypage(i, pagesize);
batchinsertwithexecutor(page);
}
@service
publicclass batchinsertservice {
@autowired
private sqlsessionfactory sqlsessionfactory;
/**
* 高性能批量插入
* 10万条数据约3秒
*/
public void highperformancebatchinsert(list<user> userlist) {
if (userlist == null || userlist.isempty()) {
return;
}
int threadcount = math.min(4, runtime.getruntime().availableprocessors());
int batchsize = (int) math.ceil((double) userlist.size() / threadcount);
executorservice executor = executors.newfixedthreadpool(threadcount);
countdownlatch latch = new countdownlatch(threadcount);
for (int i = 0; i < threadcount; i++) {
int start = i * batchsize;
int end = math.min((i + 1) * batchsize, userlist.size());
if (start >= userlist.size()) {
latch.countdown();
continue;
}
list<user> sublist = new arraylist<>(userlist.sublist(start, end));
executor.submit(() -> {
try {
dobatchinsert(sublist);
} finally {
latch.countdown();
}
});
}
try {
latch.await();
} catch (interruptedexception e) {
thread.currentthread().interrupt();
}
executor.shutdown();
}
private void dobatchinsert(list<user> userlist) {
try (sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch, false)) {
usermapper mapper = sqlsession.getmapper(usermapper.class);
for (int i = 0; i < userlist.size(); i++) {
mapper.insert(userlist.get(i));
if ((i + 1) % 1000 == 0) {
sqlsession.flushstatements();
sqlsession.clearcache();
}
}
sqlsession.flushstatements();
sqlsession.commit();
}
}
}
| 优化点 | 关键配置 |
|---|---|
| 批量sql | foreach拼接,分批1000条 |
| jdbc批处理 | rewritebatchedstatements=true+executortype.batch |
| 多线程 | 线程数 ≤ 连接池大小 |
核心原则: 减少网络往返 + 减少事务次数 + 并行处理。
到此这篇关于mybatis中批量插入的三个关键优化技巧与避坑指南的文章就介绍到这了,更多相关mybatis批量插入优化内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论