最近在压测一批接口,发现接口处理速度慢的有点超出预期,感觉很奇怪,后面定位发现是数据库批量保存这块很慢。 这个项目用的是mybatisplus,批量保存直接用的是mybatisplus提供的saveBatch。 我点进去看了下源码,感觉有点不太对劲: 我继续追踪了下,从这个代码来看,确实是for循环一条一条执行了sqlSession。insert,下面的consumer执行的就是上面的sqlSession。insert: 然后累计一定数量后,一批flush。 从这点来看,这个saveBach的性能肯定比直接一条一条insert快。 我直接进行一个粗略的实验,简单创建了一张表来对比一波!粗略的实验 1000条数据,一条一条插入TestvoidMybatisPlusSaveOne(){SqlSessionsqlSessionsqlSessionFactory。openSession();try{StopWatchstopWatchnewStopWatch();stopWatch。start(mybatisplussaveone);for(inti0;i1000;i){OpenTestopenTestnewOpenTest();openTest。setA(ai);openTest。setB(bi);openTest。setC(ci);openTest。setD(di);openTest。setE(ei);openTest。setF(fi);openTest。setG(gi);openTest。setH(hi);openTest。setI(ii);openTest。setJ(ji);openTest。setK(ki);一条一条插入openTestService。save(openTest);}sqlSession。commit();stopWatch。stop();log。info(mybatisplussaveone:stopWatch。getTotalTimeMillis());}finally{sqlSession。close();}} 可以看到,执行一批1000条数的批量保存,耗费的时间是121011毫秒。 1000条数据用mybatisplus自带的saveBatch插入TestvoidMybatisPlusSaveBatch(){SqlSessionsqlSessionsqlSessionFactory。openSession();try{ListOpenTestopenTestListnewArrayList();for(inti0;i1000;i){OpenTestopenTestnewOpenTest();openTest。setA(ai);openTest。setB(bi);openTest。setC(ci);openTest。setD(di);openTest。setE(ei);openTest。setF(fi);openTest。setG(gi);openTest。setH(hi);openTest。setI(ii);openTest。setJ(ji);openTest。setK(ki);openTestList。add(openTest);}StopWatchstopWatchnewStopWatch();stopWatch。start(mybatisplussavebatch);批量插入openTestService。saveBatch(openTestList);sqlSession。commit();stopWatch。stop();log。info(mybatisplussavebatch:stopWatch。getTotalTimeMillis());}finally{sqlSession。close();}} 耗费的时间是59927毫秒,比一条一条插入快了一倍,从这点来看,效率还是可以的。 然后常见的还有一种利用拼接sql方式来实现批量插入,我们也来对比试试看性能如何。 1000条数据用手动拼接sql方式插入 搞个手动拼接: 来跑跑下性能如何:TestvoidMapperSaveBatch(){SqlSessionsqlSessionsqlSessionFactory。openSession();try{ListOpenTestopenTestListnewArrayList();for(inti0;i1000;i){OpenTestopenTestnewOpenTest();openTest。setA(ai);openTest。setB(bi);openTest。setC(ci);openTest。setD(di);openTest。setE(ei);openTest。setF(fi);openTest。setG(gi);openTest。setH(hi);openTest。setI(ii);openTest。setJ(ji);openTest。setK(ki);openTestList。add(openTest);}StopWatchstopWatchnewStopWatch();stopWatch。start(mappersavebatch);手动拼接批量插入openTestMapper。saveBatch(openTestList);sqlSession。commit();stopWatch。stop();log。info(mappersavebatch:stopWatch。getTotalTimeMillis());}finally{sqlSession。close();}} 耗时只有2275毫秒,性能比mybatisplus自带的saveBatch好了26倍! 这时,我又突然回想起以前直接用JDBC批量保存的接口,那都到这份上了,顺带也跑跑看! 1000条数据用JDBCexecuteBatch插入TestvoidJDBCSaveBatch()throwsSQLException{SqlSessionsqlSessionsqlSessionFactory。openSession();ConnectionconnectionsqlSession。getConnection();connection。setAutoCommit(false);Stringsqlinsertintoopentest(a,b,c,d,e,f,g,h,i,j,k)values(?,?,?,?,?,?,?,?,?,?,?);PreparedStatementstatementconnection。prepareStatement(sql);try{for(inti0;i1000;i){statement。setString(1,ai);statement。setString(2,bi);statement。setString(3,ci);statement。setString(4,di);statement。setString(5,ei);statement。setString(6,fi);statement。setString(7,gi);statement。setString(8,hi);statement。setString(9,ii);statement。setString(10,ji);statement。setString(11,ki);statement。addBatch();}StopWatchstopWatchnewStopWatch();stopWatch。start(JDBCsavebatch);statement。executeBatch();connection。commit();stopWatch。stop();log。info(JDBCsavebatch:stopWatch。getTotalTimeMillis());}finally{statement。close();sqlSession。close();}} 耗时是55663毫秒,所以JDBCexecuteBatch的性能跟mybatisplus的saveBatch一样(底层一样)。 综上所述,拼接sql的方式实现批量保存效率最佳。 但是我又不太甘心,总感觉应该有什么别的法子,然后我就继续跟着mybatisplus的源码debug了一下,跟到了mysql的驱动,突然发现有个if里面的条件有点显眼: 就是这个叫rewriteBatchedStatements的玩意,从名字来看是要重写批操作的Statement,前面batchHasPlainStatements已经是false,取反肯定是true,所以只要这参数是true就会进行一波操作。 我看了下默认是false。 同时我也上网查了下rewriteBatchedStatements参数,好家伙,好像有用! 我直接将jdbcurl加上了这个参数: 然后继续跑了下mybatisplus自带的saveBatch,果然性能大大提高,跟拼接SQL差不多! 顺带我也跑了下JDBC的executeBatch,果然也提高了。 然后我继续debug,来探探rewriteBatchedStatements究竟是怎么rewrite的! 如果这个参数是true,则会执行下面的方法且直接返回: 看下executeBatchedInserts究竟干了什么: 看到上面我圈出来的代码没,好像已经有点感觉了,继续往下debug。 果然!sql语句被rewrite了: 对插入而言,所谓的rewrite其实就是将一批插入拼接成insertintoxxxvalues(a),(b),(c)。。。这样一条语句的形式然后执行,这样一来跟拼接sql的效果是一样的。 那为什么默认不给这个参数设置为true呢? 我简单问了下ChatGPT:如果批量语句中的某些语句失败,则默认重写会导致所有语句都失败。批量语句的某些语句参数不一样,则默认重写会使得查询缓存未命中。 看起来影响不大,所以我给我的项目设置上了这个参数!最后 稍微总结下我粗略的对比(虽然粗略,但实验结果符合原理层面的理解),如果你想更准确地实验,可以使用JMH,并且测试更多组数(如5000,10000等)的情况。 批量保存方式 数据量(条) 耗时(ms) 单条循环插入 1000hr121011hrmybatisplussaveBatch 1000hr59927hrmybatisplussaveBatch(添加rewtire参数) 1000hr2589hr手动拼接sql 1000hr2275hrjdbcexecuteBatch 1000hr55663hrjdbcexecuteBatch(添加rewtire参数) 1000hr324hr所以如果有使用jdbc的Batch性能方面的需求,要将rewriteBatchedStatements设置为true,这样能提高很多性能。 然后如果喜欢手动拼接sql要注意一次拼接的数量,分批处理。 链接:https:juejin。cnpost7217836890120306746