MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn)
最近在壓測(cè)一批接口,發(fā)現(xiàn)接口處理速度慢的有點(diǎn)超出預(yù)期,感覺(jué)很奇怪,后面定位發(fā)現(xiàn)是數(shù)據(jù)庫(kù)批量保存這塊很慢。
這個(gè)項(xiàng)目用的是 mybatis-plus,批量保存直接用的是 mybatis-plus 提供的 saveBatch。 我點(diǎn)進(jìn)去看了下源碼,感覺(jué)有點(diǎn)不太對(duì)勁:
繼續(xù)追蹤了下,從這個(gè)代碼來(lái)看,確實(shí)是 for 循環(huán)一條一條執(zhí)行了 sqlSession.insert,下面的 consumer 執(zhí)行的就是上面的 sqlSession.insert:
然后累計(jì)一定數(shù)量后,一批 flush。從這點(diǎn)來(lái)看,這個(gè) saveBach 的性能肯定比直接一條一條 insert 快。
我直接進(jìn)行一個(gè)粗略的實(shí)驗(yàn),簡(jiǎn)單創(chuàng)建了一張表來(lái)對(duì)比一波!
1、1000條數(shù)據(jù),一條一條插入
@Test void MybatisPlusSaveOne() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { StopWatch stopWatch = new StopWatch(); stopWatch.start("mybatis plus save one"); for (int i = 0; i < 1000; i++) { OpenTest openTest = new OpenTest(); 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ù)的批量保存,耗費(fèi)的時(shí)間是 121011 毫秒。
2、1000條數(shù)據(jù)用 mybatis-plus 自帶的 saveBatch 插入
@Test void MybatisPlusSaveBatch() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { List<OpenTest> openTestList = new ArrayList<>(); for (int i = 0; i < 1000; i++) { OpenTest openTest = new OpenTest(); 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); } StopWatch stopWatch = new StopWatch(); stopWatch.start("mybatis plus save batch"); //批量插入 openTestService.saveBatch(openTestList); sqlSession.commit(); stopWatch.stop(); log.info("mybatis plus save batch:" + stopWatch.getTotalTimeMillis()); } finally { sqlSession.close(); } }
耗費(fèi)的時(shí)間是 59927 毫秒,比一條一條插入快了一倍,從這點(diǎn)來(lái)看,效率還是可以的。
然后常見(jiàn)的還有一種利用拼接 SQL 方式來(lái)實(shí)現(xiàn)批量插入,我們也來(lái)對(duì)比試試看性能如何。
3、1000 條數(shù)據(jù)用手動(dòng)拼接 SQL 方式插入, 搞個(gè)手動(dòng)拼接:
來(lái)跑跑下性能如何:
@Test void MapperSaveBatch() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { List<OpenTest> openTestList = new ArrayList<>(); for (int i = 0; i < 1000; i++) { OpenTest openTest = new OpenTest(); 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); } StopWatch stopWatch = new StopWatch(); stopWatch.start("mapper save batch"); //手動(dòng)拼接批量插入 openTestMapper.saveBatch(openTestList); sqlSession.commit(); stopWatch.stop(); log.info("mapper save batch:" + stopWatch.getTotalTimeMillis()); } finally { sqlSession.close(); } }
耗時(shí)只有 2275 毫秒,性能比 mybatis-plus 自帶的 saveBatch 好了 26 倍!
這時(shí),我又突然回想起以前直接用 JDBC 批量保存的接口,那都到這份上了,順帶也跑跑看!
4、1000 條數(shù)據(jù)用 JDBC executeBatch 插入
@Test void JDBCSaveBatch() throws SQLException { SqlSession sqlSession = sqlSessionFactory.openSession(); Connection connection = sqlSession.getConnection(); connection.setAutoCommit(false); String sql = "insert into open_test(a,b,c,d,e,f,g,h,i,j,k) values(?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement statement = connection.prepareStatement(sql); try { for (int i = 0; i < 1000; 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(); } StopWatch stopWatch = new StopWatch(); stopWatch.start("JDBC save batch"); statement.executeBatch(); connection.commit(); stopWatch.stop(); log.info("JDBC save batch:" + stopWatch.getTotalTimeMillis()); } finally { statement.close(); sqlSession.close(); } }
耗時(shí)是 55663 毫秒,所以 JDBC executeBatch 的性能跟 mybatis-plus 的 saveBatch 一樣(底層一樣)。
綜上所述,拼接 SQL 的方式實(shí)現(xiàn)批量保存效率最佳。
但是我又不太甘心,總感覺(jué)應(yīng)該有什么別的法子,然后我就繼續(xù)跟著 mybatis-plus 的源碼 debug 了一下,跟到了 MySQL 的驅(qū)動(dòng),突然發(fā)現(xiàn)有個(gè) if 里面的條件有點(diǎn)顯眼:
就是這個(gè)叫 rewriteBatchedStatements 的玩意,從名字來(lái)看是要重寫(xiě)批操作的 Statement,前面batchHasPlainStatements 已經(jīng)是 false,取反肯定是 true,所以只要這參數(shù)是 true 就會(huì)進(jìn)行一波操作。
我看了下默認(rèn)是 false。
同時(shí)我也上網(wǎng)查了下 rewriteBatchedStatements 參數(shù),好家伙,好像有用!
直接將 jdbcurl 加上了這個(gè)參數(shù):
然后繼續(xù)跑了下 mybatis-plus 自帶的 saveBatch,果然性能大大提高,跟拼接 SQL 差不多!
順帶我也跑了下 JDBC 的 executeBatch ,果然也提高了。
然后我繼續(xù) debug ,來(lái)探探 rewriteBatchedStatements 究竟是怎么 rewrite 的! 如果這個(gè)參數(shù)是 true,則會(huì)執(zhí)行下面的方法且直接返回:
看下 executeBatchedInserts 究竟干了什么:
看到上面我圈出來(lái)的代碼沒(méi),好像已經(jīng)有點(diǎn)感覺(jué)了,繼續(xù)往下 debug。
果然!SQL 語(yǔ)句被 rewrite了:
對(duì)插入而言,所謂的 rewrite 其實(shí)就是將一批插入拼接成 insert into xxx values (a),(b),(c)...這樣一條語(yǔ)句的形式然后執(zhí)行,這樣一來(lái)跟拼接 SQL 的效果是一樣的。
那為什么默認(rèn)不給這個(gè)參數(shù)設(shè)置為 true 呢?主要有以下兩點(diǎn):
如果批量語(yǔ)句中的某些語(yǔ)句失敗,則默認(rèn)重寫(xiě)會(huì)導(dǎo)致所有語(yǔ)句都失敗。
批量語(yǔ)句的某些語(yǔ)句參數(shù)不一樣,則默認(rèn)重寫(xiě)會(huì)使得查詢緩存未命中。
看起來(lái)影響不大,所以我給我的項(xiàng)目設(shè)置上了這個(gè)參數(shù)!
最后
稍微總結(jié)下我粗略的對(duì)比(雖然粗略,但實(shí)驗(yàn)結(jié)果符合原理層面的理解),如果你想更準(zhǔn)確地做實(shí)驗(yàn),可以使用 JMH,并且測(cè)試更多組數(shù)(如 5000,10000等)的情況。
所以如果有使用 JDBC 的 Batch 性能方面的需求,要將 rewriteBatchedStatements 設(shè)置為 true,這樣能提高很多性能。
然后如果喜歡手動(dòng)拼接 SQL 要注意一次拼接的數(shù)量,分批處理。
到此這篇關(guān)于MyBatis saveBatch 性能調(diào)優(yōu)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)MyBatis saveBatch 性能調(diào)優(yōu)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea 安裝 Mybatis 開(kāi)發(fā)幫助插件 MyBatisCodeHelper-Pro 插件破解版的方法
MyBatisCodeHelper-Pro 插件可以幫助我們快速的開(kāi)發(fā) mybatis,這篇文章給大家介紹idea 安裝 Mybatis 開(kāi)發(fā)幫助插件 MyBatisCodeHelper-Pro 插件破解版的相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧2020-09-09LambdaQueryWrapper與QueryWrapper的使用方式
這篇文章主要介紹了LambdaQueryWrapper與QueryWrapper的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05springboot項(xiàng)目中PropertySource如何讀取yaml配置文件
這篇文章主要介紹了springboot項(xiàng)目中PropertySource如何讀取yaml配置文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01詳解Java 自動(dòng)裝箱與拆箱的實(shí)現(xiàn)原理
本篇文章主要介紹了詳解Java 自動(dòng)裝箱與拆箱的實(shí)現(xiàn)原理,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04MyBatis批量查詢、插入、更新、刪除的實(shí)現(xiàn)示例
由于需要處理短時(shí)間內(nèi)大量數(shù)據(jù)入庫(kù)的問(wèn)題,想到了Mybatis的批量操作,本文主要介紹了MyBatis批量查詢、插入、更新、刪除的實(shí)現(xiàn)示例,感興趣的可以了解一下2023-05-05SpringBoot集成Druid連接池進(jìn)行SQL監(jiān)控的問(wèn)題解析
這篇文章主要介紹了SpringBoot集成Druid連接池進(jìn)行SQL監(jiān)控的問(wèn)題解析,在SpringBoot工程中引入Druid連接池非常簡(jiǎn)單,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-07-07Java實(shí)例講解枚舉enum的實(shí)現(xiàn)
枚舉法的本質(zhì)就是從所有候選答案中去搜索正確的解,枚舉算法簡(jiǎn)單粗暴,他暴力的枚舉所有可能,盡可能地嘗試所有的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06Mybatis動(dòng)態(tài)SQL之if、choose、where、set、trim、foreach標(biāo)記實(shí)例詳解
動(dòng)態(tài)SQL就是動(dòng)態(tài)的生成SQL。接下來(lái)通過(guò)本文給大家介紹Mybatis動(dòng)態(tài)SQL之if、choose、where、set、trim、foreach標(biāo)記實(shí)例詳解的相關(guān)知識(shí),感興趣的朋友一起看看吧2016-09-09使用遞歸刪除樹(shù)形結(jié)構(gòu)的所有子節(jié)點(diǎn)(java和mysql實(shí)現(xiàn))
下面小編就為大家?guī)?lái)一篇使用遞歸刪除樹(shù)形結(jié)構(gòu)的所有子節(jié)點(diǎn)(java和mysql實(shí)現(xiàn))。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10java?list和map切割分段的實(shí)現(xiàn)及多線程應(yīng)用案例
這篇文章主要為大家介紹了java?list和map切割分段的實(shí)現(xiàn)及多線程應(yīng)用案例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12