- 粗略的實驗
- 最后
最近在壓測一批接口,發(fā)現(xiàn)接口處理速度慢的有點超出預(yù)期,感覺很奇怪,后面定位發(fā)現(xiàn)是數(shù)據(jù)庫批量保存這塊很慢。
這個項目用的是 mybatis-plus
,批量保存直接用的是 mybatis-plus
提供的 saveBatch。
我點進(jìn)去看了下源碼,感覺有點不太對勁:
我繼續(xù)追蹤了下,從這個代碼來看,確實是 for 循環(huán)一條一條執(zhí)行了 sqlSession.insert
,下面的 consumer 執(zhí)行的就是上面的 sqlSession.insert
:
然后累計一定數(shù)量后,一批 flush。
從這點來看,這個 saveBach 的性能肯定比直接一條一條 insert 快。
我直接進(jìn)行一個粗略的實驗,簡單創(chuàng)建了一張表來對比一波!
粗略的實驗
1000條數(shù)據(jù),一條一條插入
@Test
voidMybatisPlusSaveOne(){
SqlSessionsqlSession=sqlSessionFactory.openSession();
try{
StopWatchstopWatch=newStopWatch();
stopWatch.start("mybatisplussaveone");
for(inti=0;i1000;i++){
OpenTestopenTest=newOpenTest();
openTest.setA("a"+i);
openTest.setB("b"+i);
openTest.setC("c"+i);
openTest.setD("d"+i);
openTest.setE("e"+i);
openTest.setF("f"+i);
openTest.setG("g"+i);
openTest.setH("h"+i);
openTest.setI("i"+i);
openTest.setJ("j"+i);
openTest.setK("k"+i);
//一條一條插入
openTestService.save(openTest);
}
sqlSession.commit();
stopWatch.stop();
log.info("mybatis plus save one:"+stopWatch.getTotalTimeMillis());
}finally{
sqlSession.close();
}
}
可以看到,執(zhí)行一批 1000 條數(shù)的批量保存,耗費的時間是 121011 毫秒。
1000條數(shù)據(jù)用 mybatis-plus 自帶的 saveBatch 插入
@Test
voidMybatisPlusSaveBatch(){
SqlSessionsqlSession=sqlSessionFactory.openSession();
try{
ListopenTestList=newArrayList<>();
for(inti=0;i1000;i++){
OpenTestopenTest=newOpenTest();
openTest.setA("a"+i);
openTest.setB("b"+i);
openTest.setC("c"+i);
openTest.setD("d"+i);
openTest.setE("e"+i);
openTest.setF("f"+i);
openTest.setG("g"+i);
openTest.setH("h"+i);
openTest.setI("i"+i);
openTest.setJ("j"+i);
openTest.setK("k"+i);
openTestList.add(openTest);
}
StopWatchstopWatch=newStopWatch();
stopWatch.start("mybatisplussavebatch");
//批量插入
openTestService.saveBatch(openTestList);
sqlSession.commit();
stopWatch.stop();
log.info("mybatis plus save batch:"+stopWatch.getTotalTimeMillis());
}finally{
sqlSession.close();
}
}
耗費的時間是 59927 毫秒,比一條一條插入快了一倍,從這點來看,效率還是可以的。
然后常見的還有一種利用拼接 sql 方式來實現(xiàn)批量插入,我們也來對比試試看性能如何。
1000條數(shù)據(jù)用手動拼接 sql 方式插入
搞個手動拼接:
來跑跑下性能如何:
@Test
voidMapperSaveBatch(){
SqlSessionsqlSession=sqlSessionFactory.openSession();
try{
ListopenTestList=newArrayList<>();
for(inti=0;i1000;i++){
OpenTestopenTest=newOpenTest();
openTest.setA("a"+i);
openTest.setB("b"+i);
openTest.setC("c"+i);
openTest.setD("d"+i);
openTest.setE("e"+i);
openTest.setF("f"+i);
openTest.setG("g"+i);
openTest.setH("h"+i);
openTest.setI("i"+i);
openTest.setJ("j"+i);
openTest.setK("k"+i);
openTestList.add(openTest);
}
StopWatchstopWatch=newStopWatch();
stopWatch.start("mappersavebatch");
//手動拼接批量插入
openTestMapper.saveBatch(openTestList);
sqlSession.commit();
stopWatch.stop();
log.info("mapper save batch:"+stopWatch.getTotalTimeMillis());
}finally{
sqlSession.close();
}
}
耗時只有 2275 毫秒,性能比 mybatis-plus 自帶的 saveBatch 好了 26 倍!
這時,我又突然回想起以前直接用 JDBC 批量保存的接口,那都到這份上了,順帶也跑跑看!
1000條數(shù)據(jù)用 JDBC executeBatch 插入
@Test
voidJDBCSaveBatch()throwsSQLException{
SqlSessionsqlSession=sqlSessionFactory.openSession();
Connectionconnection=sqlSession.getConnection();
connection.setAutoCommit(false);
Stringsql="insertintoopen_test(a,b,c,d,e,f,g,h,i,j,k)values(?,?,?,?,?,?,?,?,?,?,?)";
PreparedStatementstatement=connection.prepareStatement(sql);
try{
for(inti=0;i1000;i++){
statement.setString(1,"a"+i);
statement.setString(2,"b"+i);
statement.setString(3,"c"+i);
statement.setString(4,"d"+i);
statement.setString(5,"e"+i);
statement.setString(6,"f"+i);
statement.setString(7,"g"+i);
statement.setString(8,"h"+i);
statement.setString(9,"i"+i);
statement.setString(10,"j"+i);
statement.setString(11,"k"+i);
statement.addBatch();
}
StopWatchstopWatch=newStopWatch();
stopWatch.start("JDBCsavebatch");
statement.executeBatch();
connection.commit();
stopWatch.stop();
log.info("JDBC save batch:"+stopWatch.getTotalTimeMillis());
}finally{
statement.close();
sqlSession.close();
}
}
耗時是 55663 毫秒,所以 JDBC executeBatch 的性能跟 mybatis-plus
的 saveBatch 一樣(底層一樣)。
綜上所述,拼接 sql 的方式實現(xiàn)批量保存效率最佳。
但是我又不太甘心,總感覺應(yīng)該有什么別的法子,然后我就繼續(xù)跟著 mybatis-plus 的源碼 debug 了一下,跟到了 mysql 的驅(qū)動,突然發(fā)現(xiàn)有個 if 里面的條件有點顯眼:
就是這個叫 rewriteBatchedStatements 的玩意,從名字來看是要重寫批操作的 Statement,前面batchHasPlainStatements 已經(jīng)是 false,取反肯定是 true,所以只要這參數(shù)是 true 就會進(jìn)行一波操作。
我看了下默認(rèn)是 false。
同時我也上網(wǎng)查了下 rewriteBatchedStatements 參數(shù),好家伙,好像有用!我直接將 jdbcurl 加上了這個參數(shù):
然后繼續(xù)跑了下 mybatis-plus
自帶的 saveBatch,果然性能大大提高,跟拼接 SQL 差不多!
順帶我也跑了下 JDBC 的 executeBatch ,果然也提高了。
然后我繼續(xù) debug ,來探探 rewriteBatchedStatements 究竟是怎么 rewrite 的!
如果這個參數(shù)是 true,則會執(zhí)行下面的方法且直接返回:
看下 executeBatchedInserts
究竟干了什么:
看到上面我圈出來的代碼沒,好像已經(jīng)有點感覺了,繼續(xù)往下 debug。
果然!sql 語句被 rewrite了:
對插入而言,所謂的 rewrite 其實就是將一批插入拼接成 insert into xxx values (a),(b),(c)...
這樣一條語句的形式然后執(zhí)行,這樣一來跟拼接 sql 的效果是一樣的。
那為什么默認(rèn)不給這個參數(shù)設(shè)置為 true 呢?
原來是這樣的:
- 如果批量語句中的某些語句失敗,則默認(rèn)重寫會導(dǎo)致所有語句都失敗。
- 批量語句的某些語句參數(shù)不一樣,則默認(rèn)重寫會使得查詢緩存未命中。
看起來影響不大,所以我給我的項目設(shè)置上了這個參數(shù)!
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
最后
稍微總結(jié)下我粗略的對比(雖然粗略,但實驗結(jié)果符合原理層面的理解),如果你想更準(zhǔn)確地實驗,可以使用JMH,并且測試更多組數(shù)(如 5000,10000等)的情況。
批量保存方式 | 數(shù)據(jù)量(條) | 耗時(ms) |
---|---|---|
單條循環(huán)插入 | 1000 | 121011 |
mybatis-plus saveBatch | 1000 | 59927 |
mybatis-plus saveBatch(添加rewtire參數(shù)) | 1000 | 2589 |
手動拼接sql | 1000 | 2275 |
jdbc executeBatch | 1000 | 55663 |
jdbc executeBatch(添加rewtire參數(shù)) | 1000 | 324 |
所以如果有使用 jdbc 的 Batch 性能方面的需求,要將 rewriteBatchedStatements 設(shè)置為 true,這樣能提高很多性能。
然后如果喜歡手動拼接 sql 要注意一次拼接的數(shù)量,分批處理。
-
源碼
+關(guān)注
關(guān)注
8文章
639瀏覽量
29185 -
接口處理
+關(guān)注
關(guān)注
0文章
3瀏覽量
6411 -
mybatis
+關(guān)注
關(guān)注
0文章
60瀏覽量
6709
原文標(biāo)題:調(diào)優(yōu) MyBatis 25 倍性能
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論