0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

MyBatis批量插入别再乱用foreach了

jf_ro2CN3Fa 来源:CSDN 2023-03-13 09:47 次阅读

c1d45698-c02f-11ed-bfe3-dac502259ad0.jpg


近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)

<insertid="batchInsert"parameterType="java.util.List">
insertintoUSER(id,name)values
<foreachcollection="list"item="model"index="index"separator=",">
(#{model.id},#{model.name})
foreach>
insert>

这个方法提升批量插入速度的原理是,将传统的:

INSERTINTO`table1`(`field1`,`field2`)VALUES("data1","data2");
INSERTINTO`table1`(`field1`,`field2`)VALUES("data1","data2");
INSERTINTO`table1`(`field1`,`field2`)VALUES("data1","data2");
INSERTINTO`table1`(`field1`,`field2`)VALUES("data1","data2");
INSERTINTO`table1`(`field1`,`field2`)VALUES("data1","data2");

转化为:

INSERTINTO`table1`(`field1`,`field2`)
VALUES("data1","data2"),
("data1","data2"),
("data1","data2"),
("data1","data2"),
("data1","data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。

乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:

Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:

  • some database such as Oracle here does not support.
  • in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.

Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.

SqlSessionsession=sessionFactory.openSession(ExecutorType.BATCH);
for(Modelmodel:list){
session.insert("insertStatement",model);
}
session.flushStatements();

Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.

从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.

MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it contains element and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement.

And these steps are relatively costly process when the statement string is big and contains many placeholders.

[1] simply put, it is a mapping between placeholders and the parameters.

从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

c1e8ecb6-c02f-11ed-bfe3-dac502259ad0.png

所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看 http://www.mybatis.org/mybatis-dynamic-sql/docs/insert.html 中 Batch Insert Support 标题里的内容)

SqlSessionsession=sqlSessionFactory.openSession(ExecutorType.BATCH);
try{
SimpleTableMappermapper=session.getMapper(SimpleTableMapper.class);
Listrecords=getRecordsToInsert();//notshown

BatchInsertbatchInsert=insert(records)
.into(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
}finally{
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。

Connectionconnection=DriverManager.getConnection("jdbc//127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");
connection.setAutoCommit(false);
PreparedStatementps=connection.prepareStatement(
"insertintotb_user(name)values(?)");
for(inti=0;i< stuNum; i++) {
    ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。

总结一下,如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用 的插入的话,需要将每次插入的记录控制在 20~50 左右。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 参数
    +关注

    关注

    11

    文章

    1747

    浏览量

    32015
  • MySQL
    +关注

    关注

    1

    文章

    795

    浏览量

    26385

原文标题:求求你们了,MyBatis 批量插入别再乱用 foreach 了,5000 条数据花了 14 分钟。。

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    MyBatis Oracle解析Excel文件

    MyBatis Oracle批量插入数据
    发表于 09-06 09:10

    MyBatis的整合

    SpringBoot-15-之整合MyBatis-注解篇+分页
    发表于 10-28 08:09

    Mybatis是什么

    Mybatis第一讲
    发表于 06-04 15:33

    接地磁珠不要乱用

    开关电源的相关知识学习教材资料——接地磁珠不要乱用
    发表于 09-20 16:10 0次下载

    mybatis快速入门

    本文详细介绍mybatis相关知识,以及mybatis快速入门步骤详解。
    的头像 发表于 02-24 09:41 3426次阅读
    <b class='flag-5'>mybatis</b>快速入门

    MyBatis的实现原理

    本文主要详细介绍MyBatis的实现原理。mybatis底层还是采用原生jdbc来对数据库进行操作的,只是通过 SqlSessionFactory,SqlSession Executor
    的头像 发表于 02-24 11:25 6453次阅读
    <b class='flag-5'>MyBatis</b>的实现原理

    Java的iterator和foreach遍历集合源代码

    Java的iterator和foreach遍历集合源代码
    发表于 03-17 09:16 9次下载
    Java的iterator和<b class='flag-5'>foreach</b>遍历集合源代码

    MySQL 批量插入不重复数据的解决方法

    业务很简单:需要批量插入一些数据,数据来源可能是其他数据库的表,也可能是一个外部excel的导入
    的头像 发表于 07-02 15:28 2246次阅读
    MySQL <b class='flag-5'>批量</b><b class='flag-5'>插入</b>不重复数据的解决方法

    PHP教程:foreach使用引用注意的问题

    PHP教程:foreach使用引用注意的问题(电源技术期刊查询)-该文档为PHP教程:foreach使用引用注意的问题总结文档,是一份不错的参考资料,感兴趣的可以下载看看,,,,,,,,,,,,,,,,,
    发表于 09-22 12:28 9次下载
    PHP教程:<b class='flag-5'>foreach</b>使用引用注意的问题

    MyBatis批量插入数据的3种方法你知道几种

    批量插入功能是我们日常工作中比较常见的业务功能之一, 今天 来一个 MyBatis 批量插入的汇总篇,同时对 3 种实现方法做一个性能测试,
    的头像 发表于 12-08 17:56 4200次阅读
    <b class='flag-5'>MyBatis</b><b class='flag-5'>批量</b><b class='flag-5'>插入</b>数据的3种方法你知道几种

    Fluent Mybatis、原生MybatisMybatis Plus对比

    mapper中再组装参数。那对比原生Mybatis, Mybatis Plus或者其他框架,FluentMybatis提供哪些便利呢?
    的头像 发表于 09-15 15:41 1386次阅读

    for循环和forEach的差异

    for循环是js提出时就有的循环方法。forEach是ES5提出的,挂载在可迭代对象原型上的方法,例如Array Set Map。forEach是一个迭代器,负责遍历可迭代对象。那么遍历 ,迭代 ,可迭代对象 分别是什么呢。
    的头像 发表于 10-11 11:10 1272次阅读

    MyBatis、JDBC等做大数据量数据插入的案例和结果

    30万条数据插入插入数据库验证 实体类、mapper和配置文件定义 不分批次直接梭哈 循环逐条插入 MyBatis实现插入30万条数据 JD
    的头像 发表于 05-22 11:23 987次阅读
    <b class='flag-5'>MyBatis</b>、JDBC等做大数据量数据<b class='flag-5'>插入</b>的案例和结果

    如何调优MyBatis 25倍性能

    最近在压测一批接口,发现接口处理速度慢的有点超出预期,感觉很奇怪,后面定位发现是数据库批量保存这块很慢。 这个项目用的是 mybatis-plus,批量保存直接用的是 mybatis
    的头像 发表于 05-30 09:56 562次阅读
    如何调优<b class='flag-5'>MyBatis</b> 25倍性能

    mybatis框架的主要作用

    MyBatis框架的主要作用包括以下几个方面。 数据库操作的简化和标准化: MyBatis框架提供一种简单的方式来执行数据库操作,包括插入、更新、删除和查询等操作。通过使用
    的头像 发表于 12-03 14:49 1913次阅读