1. 從 7 分鐘到 10 秒,Mybatis 批處理真的很強(qiáng)!

        共 1485字,需瀏覽 3分鐘

         ·

        2022-06-10 01:59

        來(lái)源:juejin.cn/post/7078237987011559460

        這篇文章會(huì)一步一步帶你從一個(gè)新手的角度慢慢揭開批處理的神秘面紗,對(duì)于初次寫Mybatis批處理的同學(xué)可能會(huì)有很大的幫助,建議收藏點(diǎn)贊~

        處理批處理的方式有很多種,這里不分析各種方式的優(yōu)劣,只是概述?ExecutorType.BATCH?這種的用法,另學(xué)藝不精,如果有錯(cuò)的地方,還請(qǐng)大佬們指出更正。

        問(wèn)題原因

        在公司寫項(xiàng)目的時(shí)候,有一個(gè)自動(dòng)對(duì)賬的需求,需要從文件中讀取幾萬(wàn)條數(shù)據(jù)插入到數(shù)據(jù)庫(kù)中,后續(xù)可能跟著業(yè)務(wù)的增長(zhǎng),會(huì)上升到幾十萬(wàn),所以對(duì)于插入需要進(jìn)行批處理操作,下面我們就來(lái)看看我是怎么一步一步踩坑的。

        簡(jiǎn)單了解一下批處理背后的秘密,BatchExecutor

        批處理是 JDBC 編程中的另一種優(yōu)化手段。JDBC 在執(zhí)行 SQL 語(yǔ)句時(shí),會(huì)將 SQL 語(yǔ)句以及實(shí)參通過(guò)網(wǎng)絡(luò)請(qǐng)求的方式發(fā)送到數(shù)據(jù)庫(kù),一次執(zhí)行一條 SQL 語(yǔ)句,一方面會(huì)減小請(qǐng)求包的有效負(fù)載,另一個(gè)方面會(huì)增加耗費(fèi)在網(wǎng)絡(luò)通信上的時(shí)間。

        通過(guò)批處理的方式,我們就可以在 JDBC 客戶端緩存多條 SQL 語(yǔ)句,然后在 flush 或緩存滿的時(shí)候,將多條 SQL 語(yǔ)句打包發(fā)送到數(shù)據(jù)庫(kù)執(zhí)行,這樣就可以有效地降低上述兩方面的損耗,從而提高系統(tǒng)性能。

        不過(guò),有一點(diǎn)需要特別注意:

        每次向數(shù)據(jù)庫(kù)發(fā)送的 SQL 語(yǔ)句的條數(shù)是有上限的,如果批量執(zhí)行的時(shí)候超過(guò)這個(gè)上限值,數(shù)據(jù)庫(kù)就會(huì)拋出異常,拒絕執(zhí)行這一批 SQL 語(yǔ)句,所以我們需要控制批量發(fā)送 SQL 語(yǔ)句的條數(shù)和頻率。

        版本1-呱呱墜地

        廢話不多說(shuō),早先時(shí)候項(xiàng)目的代碼里就已經(jīng)存在了批處理的代碼,偽代碼的樣子大概是這樣子的:

        @Resource
        private?某Mapper類?mapper實(shí)例對(duì)象;

        private?int?BATCH?=?1000;


        ??private?void?doUpdateBatch(Date?accountDate,?List<某實(shí)體類>?data)?{
        ????SqlSession?batchSqlSession?=?null;
        ????try?{
        ??????if?(data?==?null?||?data.size()?==?0)?{
        ????????return;
        ??????}
        ??????batchSqlSession?=?sqlSessionFactory.openSession(ExecutorType.BATCH,?false);
        ??????for?(int?index?=?0;?index?????????mapper實(shí)例對(duì)象.更新/插入Method(accountDate,?data.get(index).getOrderNo());
        ????????if?(index?!=?0?&&?index?%?BATCH?==?0)?{
        ??????????batchSqlSession.commit();
        ??????????batchSqlSession.clearCache();
        ????????}
        ??????}
        ??????batchSqlSession.commit();
        ????}?catch?(Exception?e)?{
        ??????batchSqlSession.rollback();
        ??????log.error(e.getMessage(),?e);
        ????}?finally?{
        ??????if?(batchSqlSession?!=?null)?{
        ????????batchSqlSession.close();
        ??????}
        ????}
        ??}

        我們先來(lái)看看上述這種寫法的幾種問(wèn)題

        你真的懂commit、clearCache、flushStatements嘛?

        我們先看看官網(wǎng)給出的解釋:

        然后我們結(jié)合上述寫法,它會(huì)在判斷批處理?xiàng)l數(shù)達(dá)到1000條的時(shí)候會(huì)去手動(dòng)commit,然后又手動(dòng)clearCache,我們先來(lái)看看commit到底都做了一些什么,以下為調(diào)用鏈

        ??@Override
        ??public?void?commit()?{
        ????commit(false);
        ??}??

        ??@Override
        ??public?void?commit(boolean?force)?{
        ????try?{
        ??????executor.commit(isCommitOrRollbackRequired(force));
        ??????dirty?=?false;
        ????}?catch?(Exception?e)?{
        ??????throw?ExceptionFactory.wrapException("Error?committing?transaction.??Cause:?"?+?e,?e);
        ????}?finally?{
        ??????ErrorContext.instance().reset();
        ????}
        ??}

        ??private?boolean?isCommitOrRollbackRequired(boolean?force)?{
        ????//?autoCommit默認(rèn)為false,調(diào)用過(guò)插入、更新、刪除之后的dirty值為true
        ????return?(!autoCommit?&&?dirty)?||?force;
        ??}

        ??@Override
        ??public?void?commit(boolean?required)?throws?SQLException?{
        ????if?(closed)?{
        ??????throw?new?ExecutorException("Cannot?commit,?transaction?is?already?closed");
        ????}
        ????clearLocalCache();
        ????flushStatements();
        ????if?(required)?{
        ??????transaction.commit();
        ????}
        ??}

        我們會(huì)發(fā)現(xiàn),其實(shí)你直接調(diào)用commit的情況下,它就已經(jīng)做了clearLocalCache這件事情,所以大可不必在commit后加上一句clearCache,而且clearCache是做了什么你又知道嘛?就擱這調(diào)用?。?/p>

        另外flushStatements的作用,官網(wǎng)里也有詳細(xì)解釋:

        此方法的作用就是將前面所有執(zhí)行過(guò)的INSERT、UPDATE、DELETE語(yǔ)句真正刷新到數(shù)據(jù)庫(kù)中。底層調(diào)用了JDBC的statement.executeBatch方法。

        這個(gè)方法的返回值通俗來(lái)說(shuō)如果執(zhí)行的是同一個(gè)方法并且執(zhí)行的是同一條SQL,注意這里的SQL還沒(méi)有設(shè)置參數(shù),也就是說(shuō)SQL里的占位符'?'還沒(méi)有被處理成真正的參數(shù),那么每次執(zhí)行的結(jié)果共用一個(gè)BatchResult,真正的結(jié)果可以通過(guò)BatchResult中的getUpdateCounts方法獲取。

        另外如果執(zhí)行了SELECT操作,那么會(huì)將先前的UPDATE、INSERT、DELETE語(yǔ)句刷新到數(shù)據(jù)庫(kù)中。這一點(diǎn)去看BatchExecutor中的doQuery方法即可。

        反例

        看到這里,我們?cè)趤?lái)看點(diǎn)反例,你就會(huì)覺(jué)得這都是啥跟啥?。。?!誤人子弟啊,直接在百度搜一段關(guān)鍵字:mybatis ExecutorType.BATCH?批處理,反例如下:

        不具備通用性

        由于項(xiàng)目中用到批處理的地方肯定不止一個(gè),那每用一次就需要CV一下,0.0 那會(huì)不會(huì)顯得太菜了?能不能一勞永逸?這個(gè)時(shí)候就得用上Java8中的接口函數(shù)了~

        版本2-初具雛形

        在解決完上述兩個(gè)問(wèn)題后,我們的代碼版本來(lái)到了第2版,你以為這就對(duì)了?這就完事了?別急,我們繼續(xù)往下看!

        import?lombok.extern.slf4j.Slf4j;
        import?org.apache.ibatis.session.ExecutorType;
        import?org.apache.ibatis.session.SqlSession;
        import?org.apache.ibatis.session.SqlSessionFactory;
        import?org.springframework.stereotype.Component;

        import?javax.annotation.Resource;
        import?java.util.List;
        import?java.util.function.ToIntFunction;

        @Slf4j
        @Component
        public?class?MybatisBatchUtils?{

        ????/**
        ?????*?每次處理1000條
        ?????*/

        ????private?static?final?int?BATCH?=?1000;

        ????@Resource
        ????private?SqlSessionFactory?sqlSessionFactory;

        ????/**
        ?????*?批量處理修改或者插入
        ?????*
        ?????*?@param?data?????需要被處理的數(shù)據(jù)
        ?????*?@param?function?自定義處理邏輯
        ?????*?@return?int?影響的總行數(shù)
        ?????*/

        ????public???int?batchUpdateOrInsert(List?data,?ToIntFunction?function)?{
        ????????int?count?=?0;
        ????????SqlSession?batchSqlSession?=?sqlSessionFactory.openSession(ExecutorType.BATCH);
        ????????try?{
        ????????????for?(int?index?=?0;?index?????????????????count?+=?function.applyAsInt(data.get(index));
        ????????????????if?(index?!=?0?&&?index?%?BATCH?==?0)?{
        ????????????????????batchSqlSession.flushStatements();
        ????????????????}
        ????????????}
        ????????????batchSqlSession.commit();
        ????????}?catch?(Exception?e)?{
        ????????????batchSqlSession.rollback();
        ????????????log.error(e.getMessage(),?e);
        ????????}?finally?{
        ????????????batchSqlSession.close();
        ????????}
        ????????return?count;
        ????}
        }

        偽代碼使用案例

        @Resource
        private?某Mapper類?mapper實(shí)例對(duì)象;

        batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合,?item?->?mapper實(shí)例對(duì)象.insert方法(item));

        這個(gè)時(shí)候我興高采烈的收工了,直到過(guò)了一兩天,導(dǎo)師問(wèn)我,考慮過(guò)這個(gè)業(yè)務(wù)的性能嘛,后續(xù)量大了可能每天有十多萬(wàn)筆數(shù)據(jù),問(wèn)我現(xiàn)在每天要多久,我才發(fā)現(xiàn) 0.0 兩三萬(wàn)條數(shù)據(jù)插入居然要7分鐘(不完全是這個(gè)問(wèn)題導(dǎo)致這么慢,還有Oracle插入語(yǔ)句的原因,下面會(huì)描述),,哈哈,笑不活了,簡(jiǎn)直就是Bug制造機(jī),我就開始思考為什么會(huì)這么慢,肯定是批處理沒(méi)生效,我就思考為什么會(huì)沒(méi)生效?

        版本3-標(biāo)準(zhǔn)寫法

        我們知道上面我們提到了BatchExecutor執(zhí)行器,我們知道每個(gè)SqlSession都會(huì)擁有一個(gè)Executor對(duì)象,這個(gè)對(duì)象才是執(zhí)行 SQL 語(yǔ)句的幕后黑手,我們也知道Spring跟Mybatis整合的時(shí)候使用的SqlSessionSqlSessionTemplate,默認(rèn)用的是ExecutorType.SIMPLE,這個(gè)時(shí)候你通過(guò)自動(dòng)注入獲得的Mapper對(duì)象其實(shí)是沒(méi)有開啟批處理的

        ??public?Executor?newExecutor(Transaction?transaction,?ExecutorType?executorType)?{
        ????executorType?=?executorType?==?null???defaultExecutorType?:?executorType;
        ????executorType?=?executorType?==?null???ExecutorType.SIMPLE?:?executorType;
        ????Executor?executor;
        ????if?(ExecutorType.BATCH?==?executorType)?{
        ??????executor?=?new?BatchExecutor(this,?transaction);
        ????}?else?if?(ExecutorType.REUSE?==?executorType)?{
        ??????executor?=?new?ReuseExecutor(this,?transaction);
        ????}?else?{
        ??????executor?=?new?SimpleExecutor(this,?transaction);
        ????}
        ????if?(cacheEnabled)?{
        ??????executor?=?new?CachingExecutor(executor);
        ????}
        ????executor?=?(Executor)?interceptorChain.pluginAll(executor);
        ????return?executor;
        ??}

        那么我們實(shí)際上是需要通過(guò)sqlSessionFactory.openSession(ExecutorType.BATCH)得到的sqlSession對(duì)象(此時(shí)里面的ExecutorBatchExecutor)去獲得一個(gè)新的Mapper對(duì)象才能生效?。?!

        所以我們更改一下這個(gè)通用的方法,把MapperClass也一塊傳遞進(jìn)來(lái)

        public?class?MybatisBatchUtils?{
        ????
        ????/**
        ????*?每次處理1000條
        ????*/

        ????private?static?final?int?BATCH_SIZE?=?1000;
        ????
        ????@Resource
        ????private?SqlSessionFactory?sqlSessionFactory;
        ????
        ????/**
        ????*?批量處理修改或者插入
        ????*
        ????*?@param?data?????需要被處理的數(shù)據(jù)
        ????*?@param?mapperClass??Mybatis的Mapper類
        ????*?@param?function?自定義處理邏輯
        ????*?@return?int?影響的總行數(shù)
        ????*/

        ????public???int?batchUpdateOrInsert(List?data,?Class?mapperClass,?BiFunction?function)?{
        ????????int?i?=?1;
        ????????SqlSession?batchSqlSession?=?sqlSessionFactory.openSession(ExecutorType.BATCH);
        ????????try?{
        ????????????U?mapper?=?batchSqlSession.getMapper(mapperClass);
        ????????????int?size?=?data.size();
        ????????????for?(T?element?:?data)?{
        ????????????????function.apply(element,mapper);
        ????????????????if?((i?%?BATCH_SIZE?==?0)?||?i?==?size)?{
        ????????????????????batchSqlSession.flushStatements();
        ????????????????}
        ????????????????i++;
        ????????????}
        ????????????//?非事務(wù)環(huán)境下強(qiáng)制commit,事務(wù)情況下該commit相當(dāng)于無(wú)效
        ????????????batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
        ????????}?catch?(Exception?e)?{
        ????????????batchSqlSession.rollback();
        ????????????throw?new?CustomException(e);
        ????????}?finally?{
        ????????????batchSqlSession.close();
        ????????}
        ????????return?i?-?1;
        ????}
        }

        這里會(huì)判斷是否是事務(wù)環(huán)境,不是的話會(huì)強(qiáng)制提交,如果是事務(wù)環(huán)境的話,這個(gè)commit設(shè)置force值是無(wú)效的,這個(gè)在前面的官網(wǎng)截圖中有提到。

        使用案例:

        batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合,?xxxxx.class,?(item,?mapper實(shí)例對(duì)象)?->?mapper實(shí)例對(duì)象.insert方法(item));

        附:Oracle批量插入優(yōu)化

        我們都知道Oracle主鍵序列生成策略跟MySQL不一樣,我們需要弄一個(gè)序列生成器,這里就不詳細(xì)展開描述了,然后Mybatis Generator生成的模板代碼中,insert的id是這樣獲取的

        <selectKey?keyProperty="id"?order="BEFORE"?resultType="java.lang.Long">
        ??select?XXX.nextval?from?dual
        selectKey>

        如此,就相當(dāng)于你插入1萬(wàn)條數(shù)據(jù),其實(shí)就是insert和查詢序列合計(jì)預(yù)計(jì)2萬(wàn)次交互,耗時(shí)竟然達(dá)到10s多。我們改為用原生的Batch插入,這樣子的話,只要500多毫秒,也就是0.5秒的樣子

        <insert?id="insert"?parameterType="user">
        ????????insert?into?table_name(id,?username,?password)
        ????????values(SEQ_USER.NEXTVAL,#{username},#{password})
        insert>

        最后這樣一頓操作,批處理 + 語(yǔ)句優(yōu)化一下,這個(gè)業(yè)務(wù)直接從7分多鐘變成10多秒,完美解決,撒花慶祝~


        瀏覽 55
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. A片性 | 美女视频h| 欧美一级二级无人区精品 | 护士做xxxxx免费看国产 | 午夜激情免费视频 |