当前位置: 首页 > 后端技术 > Java

MyBatis不再使用foreach来批量插入,5000条数据用了14分钟,.

时间:2023-04-02 00:39:06 Java

最近项目中的一个耗时较长的作业出现CPU占用率高的问题。经过排查,发现主要消耗时间的是批量向MyBatis插入数据。映射器配置或多或少是通过foreach循环完成的。(由于项目保密,以下代码均为自己编写的demo代码)insertintoUSER(id,name)values(#{model.id},#{model.name})这个方法改进的原理批量插入的速度是结合传统的: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");这个技巧在MySqlDocs中也有提到,如果想优化插入速度,可以将很多小操作合并成一个大操作理想情况下,这样会在一个连接中一次发送很多新的数据行,并延迟所有索引更新和一致性检查,直到结束。乍一看,这个foreach没问题,但经过项目实践,发现当表列数多(20+),一次插入的行数多(5000+),整个插入时间很长,达到14分钟,难以忍受。数据中还提到了一句话:当然不要把它们全部合并,如果数量很大。假设你有1000行你需要插入,那么不要一次一个地做。你不应该同样地尝试拥有all1000rowsinasinglequery.Insteadbreakitintosmallersizes.它强调当插入数量很大时,不能一次放在一个语句中。但是为什么不能放在同一个语句中呢?为什么这个声明需要这么长时间?查了资料发现:InsertinsideMybatisforeach不是batch的,这是一个单条的(可能会变成巨型的)SQL语句,带来的弊端是:有些数据库如Oracle这里不支持。相关情况:会有很大的要插入的记录数和数据库配置的限制(默认情况下每个语句大约2000个参数)将被命中,如果语句本身变得太大,最终可能会出现数据库堆栈错误。不能在mybatisXML中对集合进行迭代。只需在JavaForeach循环中执行一个简单的Insert语句。最重要的是sessionExecutortype.SqlSessionsession=sessionFactory.openSession(ExecutorType.BATCH);for(Modelmodel:list){session.insert("insertStatement",model);}session.flushStatements();不像defaultExecutorType.SIMPLE,该语句将准备一次并为每条要插入的记录执行。从资料中可以看出,默认的executor类型是Simple,会为每条语句创建一条语句一条新的preparedstatement,也就是在我们的项目中创建一个PreparedStatement对象,会不断的使用batchinsert方法,而由于MyBatis不能对包含的statements使用缓存,那么每次调用该方法时,sql语句都会重新解析。在内部,它仍然生成与上面的JDBC代码相同的带有许多占位符的单个插入语句。MyBatis有缓存PreparedStatement的能力,但是这条语句不能被缓存,因为它包含元素并且语句根据参数而变化。因此,MyBatis必须1)评估foreach部分和2)解析语句字符串以在每次执行此语句时构建参数映射[1]。当语句字符串很大并且包含很多占位符时,这些步骤是相对昂贵的过程。[1]简单的说,就是占位符和参数之间的映射。从以上信息来看,是耗时的。由于我在foreach之后有5000+个值,所以这个PreparedStatement很长,包含了很多占位符。字符,占位符和参数的映射尤其耗时。而且查阅相关资料发现,values的增长和需要解析的时间都是指数增长的。因此,如果非要使用foreach方式进行批量插入,可以考虑减少一次insert语句中取值的个数,最好达到上面曲线的底部值,这样速度最快。一般来说,根据经验,一次插入20-50行比较合适,时间消耗可以接受。重点来了。上面说的是,如果非要用来插入,可以提高性能。其实在MyBatis文档中写批量插入的时候,推荐使用另一种方式。(可以看http://www.mybatis.org/mybati...中批量插入支持标题里的内容)SqlSessionsession=sqlSessionFactory.openSession(ExecutorType.BATCH);try{SimpleTableMappermapper=session.getMapper(SimpleTableMapper.class);List记录=getRecordsToInsert();//未显示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();}即基础思想是将MyBatissession的executor类型设置为Batch,然后多次执行insert语句,类似于JDBC的如下语句。连接connection=DriverManager.getConnection("jdbc:mysql://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插入,需要控制每次插入的记录数在20~50条左右。参考资料https://dev.mysql.com/doc/ref...https://stackoverflow.com/que...https://stackoverflow.com/que...https://blog.csdn。net/wlwlwlw...http://blog.harawata.net/2016...https://www.red-gate.com/simp...https://stackoverflow.com/que...http://www.mybatis.org/mybatis...