Mybatis批處理踩坑艾船,糾正網(wǎng)上的一些錯誤寫法

這篇文章會一步一步帶你從一個新手的角度慢慢揭開批處理的神秘面紗琅攘,對于初次寫Mybatis批處理的同學可能會有很大的幫助,建議收藏點贊~

處理批處理的方式有很多種爆袍,這里不分析各種方式的優(yōu)劣首繁,只是概述 ExecutorType.BATCH 這種的用法,另學藝不精陨囊,如果有錯的地方弦疮,還請大佬們指出更正。

問題原因

在公司寫項目的時候蜘醋,有一個自動對賬的需求挂捅,需要從文件中讀取幾萬條數(shù)據(jù)插入到數(shù)據(jù)庫中,后續(xù)可能跟著業(yè)務的增長,會上升到幾十萬闲先,所以對于插入需要進行批處理操作状土,下面我們就來看看我是怎么一步一步踩坑的。

簡單了解一下批處理背后的秘密伺糠,BatchExecutor

批處理是 JDBC 編程中的另一種優(yōu)化手段蒙谓。JDBC 在執(zhí)行 SQL 語句時,會將 SQL 語句以及實參通過網(wǎng)絡請求的方式發(fā)送到數(shù)據(jù)庫训桶,一次執(zhí)行一條 SQL 語句累驮,一方面會減小請求包的有效負載,另一個方面會增加耗費在網(wǎng)絡通信上的時間舵揭。通過批處理的方式谤专,我們就可以在 JDBC 客戶端緩存多條 SQL 語句,然后在 flush 或緩存滿的時候午绳,將多條 SQL 語句打包發(fā)送到數(shù)據(jù)庫執(zhí)行置侍,這樣就可以有效地降低上述兩方面的損耗,從而提高系統(tǒng)性能拦焚。

不過蜡坊,有一點需要特別注意:每次向數(shù)據(jù)庫發(fā)送的 SQL 語句的條數(shù)是有上限的,如果批量執(zhí)行的時候超過這個上限值赎败,數(shù)據(jù)庫就會拋出異常秕衙,拒絕執(zhí)行這一批 SQL 語句,所以我們需要控制批量發(fā)送 SQL 語句的條數(shù)和頻率僵刮。

引用自《深入剖析 MyBatis 核心原理》- 楊四正第18節(jié)

版本1-呱呱墜地

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


@Resource
private 某Mapper類 mapper實例對象;

private int BATCH = 1000;

  private void doUpdateBatch(Date accountDate, List<某實體類> data) {
    SqlSession batchSqlSession = null;
    try {
      if (data == null || data.size() == 0) {
        return;
      }
      batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
      for (int index = 0; index < data.size(); index++) {
        mapper實例對象.更新/插入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();
      }
    }
  }

我們先來看看上述這種寫法的幾種問題

你真的懂commit搞糕、clearCache勇吊、flushStatements嘛?

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


然后我們結合上述寫法寞宫,它會在判斷批處理條數(shù)達到1000條的時候會去手動commit,然后又手動clearCache拉鹃,我們先來看看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默認為false,調(diào)用過插入膏燕、更新钥屈、刪除之后的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();
    }
  }

我們會發(fā)現(xiàn),其實你直接調(diào)用commit的情況下坝辫,它就已經(jīng)做了clearLocalCache這件事情篷就,所以大可不必在commit后加上一句clearCache,而且clearCache是做了什么你又知道嘛近忙?就擱這調(diào)用=咭怠智润!

另外flushStatements的作用官網(wǎng)里有詳細解釋

看到這里,我們在來看點反例未辆,你就會覺得0.0 這都是啥跟啥翱弑痢!8拦瘛兼蜈!誤人子弟啊,直接在百度搜一段關鍵字:mybatis ExecutorType.BATCH 批處理拙友,反例如下:

不具備通用性

由于項目中用到批處理的地方肯定不止一個为狸,那每用一次就需要CV一下,0.0 那會不會顯得太菜了遗契?能不能一勞永逸辐棒?這個時候就得用上Java8中的接口函數(shù)了~

版本2-初具雛形

在解決完上述兩個問題后,我們的代碼版本來到了第2版姊途,你以為這就對了涉瘾?這就完事了?別急捷兰,我們繼續(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  <T> int batchUpdateOrInsert(List<T> data, ToIntFunction<T> function) {
        int count = 0;
        SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            for (int index = 0; index < data.size(); 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實例對象;

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

這個時候我興高采烈的收工了,直到過了一兩天贡茅,導師問我秘蛇,考慮過這個業(yè)務的性能嘛,后續(xù)量大了可能每天有十多萬筆數(shù)據(jù)顶考,問我現(xiàn)在每天要多久赁还,我才發(fā)現(xiàn) 0.0 兩三萬條數(shù)據(jù)插入居然要7分鐘(不完全是這個問題導致這么慢,還有Oracle插入語句的原因驹沿,下面會描述)艘策,,哈哈渊季,笑不活了朋蔫,簡直就是Bug制造機,我就開始思考為什么會這么慢却汉,肯定是批處理沒生效驯妄,我就思考為什么會沒生效?

版本3-標準寫法

我們知道上面我們提到了BatchExecutor執(zhí)行器合砂,我們知道每個SqlSession都會擁有一個Executor對象青扔,這個對象才是執(zhí)行 SQL 語句的幕后黑手,我們也知道Spring跟Mybatis整合的時候使用的SqlSessionSqlSessionTemplate,默認用的是ExecutorType.SIMPLE微猖,這個時候你通過自動注入獲得的Mapper對象其實是沒有開啟批處理的

  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;
  }

那么我們實際上是需要通過sqlSessionFactory.openSession(ExecutorType.BATCH)得到的sqlSession對象(此時里面的Executor是BatchExecutor)去獲得一個新的Mapper對象才能生效L赶ⅰ!励两!

所以我們更改一下這個通用的方法黎茎,把MapperClass也一塊傳遞進來

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  <T,U,R> int batchUpdateOrInsert(List<T> data, Class<U> mapperClass, BiFunction<T,U,R> 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++;
            }
            // 非事務環(huán)境下強制commit,事務情況下該commit相當于無效
            batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
        } catch (Exception e) {
            batchSqlSession.rollback();
            log.error(e.getMessage(), e);
        } finally {
            batchSqlSession.close();
        }
        return i - 1;
    }
}

這里會判斷是否是事務環(huán)境当悔,不是的話會強制提交傅瞻,如果是事務環(huán)境的話,這個commit設置force值是無效的盲憎,這個在前面的官網(wǎng)截圖中有提到嗅骄。

使用案例:

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

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

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

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

如此窑眯,就相當于你插入1萬條數(shù)據(jù)屏积,其實就是insert和查詢序列合計預計2萬次交互,耗時竟然達到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ōu)化一下,這個業(yè)務直接從7分多鐘變成10多秒僧叉,完美解決奕枝,撒花慶祝~

作者:Linn
鏈接:https://juejin.cn/post/7078237987011559460

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瓶堕,隨后出現(xiàn)的幾起案子隘道,更是在濱河造成了極大的恐慌,老刑警劉巖郎笆,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谭梗,死亡現(xiàn)場離奇詭異,居然都是意外死亡题画,警方通過查閱死者的電腦和手機默辨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門德频,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苍息,“玉大人,你說我怎么就攤上這事【核迹” “怎么了表谊?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盖喷。 經(jīng)常有香客問我爆办,道長,這世上最難降的妖魔是什么课梳? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任距辆,我火速辦了婚禮,結果婚禮上暮刃,老公的妹妹穿的比我還像新娘跨算。我一直安慰自己,他們只是感情好椭懊,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布诸蚕。 她就那樣靜靜地躺著,像睡著了一般氧猬。 火紅的嫁衣襯著肌膚如雪背犯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天盅抚,我揣著相機與錄音漠魏,去河邊找鬼。 笑死泉哈,一個胖子當著我的面吹牛蛉幸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丛晦,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼奕纫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烫沙?” 一聲冷哼從身側響起匹层,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锌蓄,沒想到半個月后升筏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡瘸爽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年您访,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剪决。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡灵汪,死狀恐怖檀训,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情享言,我是刑警寧澤峻凫,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站览露,受9級特大地震影響荧琼,放射性物質發(fā)生泄漏。R本人自食惡果不足惜差牛,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一命锄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偏化,春花似錦累舷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搭伤,卻和暖如春只怎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怜俐。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工身堡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拍鲤。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓贴谎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親季稳。 傳聞我的和親對象是個殘疾皇子擅这,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容