天天看點

Mybatis Plus 批量插入這樣操作提升性能

作者:程式猿怪咖
Mybatis Plus 批量插入這樣操作提升性能

使用的mybatisplus的批量插入方法:saveBatch(),之前就看到過網上都在說在jdbc的url路徑上加上rewriteBatchedStatements=true 參數mysql底層才能開啟真正的批量插入模式。

保證5.1.13以上版本的驅動,才能實作高性能的批量插入。MySQL JDBC驅動在預設情況下會無視executeBatch()語句,把我們期望批量執行的一組sql語句拆散,一條一條地發給MySQL資料庫,批量插入實際上是單條插入,直接造成較低的性能。隻有把rewriteBatchedStatements參數置為true, 驅動才會幫你批量執行SQL。另外這個選項對INSERT/UPDATE/DELETE都有效。

目前我的資料表目前是沒有建立索引的,即使是在1000來w的資料量下進行1500條的批量插入也不可能消耗20來秒吧,于是沖突轉移到saveBatch方法,使用版本:

檢視源碼:

public boolean saveBatch(Collection<T> entityList, int batchSize) {     String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);     return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {         sqlSession.insert(sqlStatement, entity);     }); }           
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {        return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer);    }           
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {    Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);    return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {        int size = list.size();        int i = 1;        for(Iterator var6 = list.iterator(); var6.hasNext(); ++i) {            E element = var6.next();            consumer.accept(sqlSession, element);            if (i % batchSize == 0 || i == size) {                sqlSession.flushStatements();            }        }    });}           

最終來到了executeBatch()方法,可以看到這很明顯是在一條一條循環插入,通過sqlSession.flushStatements()将一個個單條插入的insert語句分批次進行送出,而且是同一個sqlSession,這相比周遊集合循環insert來說有一定的性能提升,但是這并不是sql層面真正的批量插入。

通過查閱相關文檔後,發現mybatisPlus提供了sql注入器,我們可以自定義方法來滿足業務的實際開發需求。

sql注入器官網

https://baomidou.com/pages/42ea4a/

sql注入器官方示例

https://gitee.com/baomidou/mybatis-plus-samples/tree/master/mybatis-plus-sample-deluxe

在mybtisPlus的核心包下提供的預設可注入方法有這些:

Mybatis Plus 批量插入這樣操作提升性能

在擴充包下,mybatisPlus還為我們提供了可擴充的可注入方法:

Mybatis Plus 批量插入這樣操作提升性能
  • AlwaysUpdateSomeColumnById:根據Id更新每一個字段,全量更新不忽略null字段,解決mybatis-plus中updateById預設會自動忽略實體中null值字段不去更新的問題;
  • InsertBatchSomeColumn:真實批量插入,通過單SQL的insert語句實作批量插入;
  • Upsert:更新or插入,根據唯一限制判斷是執行更新還是删除,相當于提供insert on duplicate key update支援。

可以發現mybatisPlus已經提供好了InsertBatchSomeColumn的方法,我們隻需要把這個方法添加進我們的sql注入器即可。

public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;    SqlMethod sqlMethod = SqlMethod.INSERT_ONE;    List<TableFieldInfo> fieldList = tableInfo.getFieldList();    String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) + this.filterTableFieldInfo(fieldList, this.predicate, TableFieldInfo::getInsertSqlColumn, "");    //------------------------------------拼接批量插入語句----------------------------------------    String columnScript = "(" + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + ")";    String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, "et.", false) + this.filterTableFieldInfo(fieldList, this.predicate, (i) -> {        return i.getInsertSqlProperty("et.");    }, "");    insertSqlProperty = "(" + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + ")";    String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", (String)null, "et", ",");    //------------------------------------------------------------------------------------------    String keyProperty = null;    String keyColumn = null;    if (tableInfo.havePK()) {        if (tableInfo.getIdType() == IdType.AUTO) {            keyGenerator = Jdbc3KeyGenerator.INSTANCE;            keyProperty = tableInfo.getKeyProperty();            keyColumn = tableInfo.getKeyColumn();        } else if (null != tableInfo.getKeySequence()) {            keyGenerator = TableInfoHelper.genKeyGenerator(this.getMethod(sqlMethod), tableInfo, this.builderAssistant);            keyProperty = tableInfo.getKeyProperty();            keyColumn = tableInfo.getKeyColumn();        }    }    String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);    SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);    return this.addInsertMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource, (KeyGenerator)keyGenerator, keyProperty, keyColumn);}           

接下來就通過SQL注入器實作真正的批量插入

預設的sql注入器

public class DefaultSqlInjector extends AbstractSqlInjector {    public DefaultSqlInjector() {    }    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {        if (tableInfo.havePK()) {            return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());        } else {            this.logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType()));            return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new Update(), new SelectByMap(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());        }    }}           

繼承DefaultSqlInjector自定義sql注入器

/** * @author zhmsky * @date 2022/8/15 15:13 */public class MySqlInjector extends DefaultSqlInjector {    @Override    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {        List<AbstractMethod> methodList = super.getMethodList(mapperClass);        //更新時自動填充的字段,不用插入值        methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));        return methodList;    }}           

将自定義的sql注入器注入到Mybatis容器中

/** * @author zhmsky * @date 2022/8/15 15:15 */@Configurationpublic class MybatisPlusConfig {    @Bean    public MySqlInjector sqlInjector() {        return new MySqlInjector();    }}           

繼承BaseMapper添加自定義方法

/** * @author zhmsky * @date 2022/8/15 15:17 */public interface CommonMapper<T> extends BaseMapper<T> {    /**     * 真正的批量插入     * @param entityList     * @return     */    int insertBatchSomeColumn(List<T> entityList);}           

對應的mapper層接口繼承上面自定義的mapper

/* * @author zhmsky * @since 2021-12-01 */@Mapperpublic interface UserMapper extends CommonMapper<User> {}           

最後直接調用UserMapper的insertBatchSomeColumn()方法即可實作真正的批量插入。

@Testvoid contextLoads() {    for (int i = 0; i < 5; i++) {        User user = new User();        user.setAge(10);        user.setUsername("zhmsky");        user.setEmail("[email protected]");        userList.add(user);    }    long l = System.currentTimeMillis();    userMapper.insertBatchSomeColumn(userList);    long l1 = System.currentTimeMillis();    System.out.println("-------------------:"+(l1-l));    userList.clear();}           

檢視日志輸出資訊,觀察執行的sql語句;

Mybatis Plus 批量插入這樣操作提升性能

發現這才是真正意義上的sql層面的批量插入。

但是,到這裡并沒有結束,mybatisPlus官方提供的insertBatchSomeColumn方法不支援分批插入,也就是有多少直接全部一次性插入,這就可能會導緻最後的sql拼接語句特别長,超出了mysql的限制,于是我們還要實作一個類似于saveBatch的分批的批量插入方法。另外,搜尋公衆号Linux就該這樣學背景回複“猴子”,擷取一份驚喜禮包。

添加分批插入

模仿原來的saveBatch方法:

* @author zhmsky * @since 2021-12-01 */@Servicepublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {    @Override    @Transactional(rollbackFor = {Exception.class})    public boolean saveBatch(Collection<User> entityList, int batchSize) {        try {            int size = entityList.size();            int idxLimit = Math.min(batchSize, size);            int i = 1;            //儲存單批送出的資料集合            List<User> oneBatchList = new ArrayList<>();            for (Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) {                User element = var7.next();                oneBatchList.add(element);                if (i == idxLimit) {                    baseMapper.insertBatchSomeColumn(oneBatchList);                    //每次送出後需要清空集合資料                    oneBatchList.clear();                    idxLimit = Math.min(idxLimit + batchSize, size);                }            }        } catch (Exception e) {            log.error("saveBatch fail", e);            return false;        }        return true;    }}           

測試:

@Testvoid contextLoads() {    for (int i = 0; i < 20; i++) {        User user = new User();        user.setAge(10);        user.setUsername("zhmsky");        user.setEmail("[email protected]");        userList.add(user);    }    long l = System.currentTimeMillis();    userService.saveBatch(userList,10);    long l1 = System.currentTimeMillis();    System.out.println("-------------------:"+(l1-l));    userList.clear();}           

輸出結果:

Mybatis Plus 批量插入這樣操作提升性能

分批插入已滿足,到此收工結束了。

接下來最重要的測試下性能

Mybatis Plus 批量插入這樣操作提升性能

目前資料表的資料量在100w多條,在此基礎上分别拿原始的saveBatch(假的批量插入)和 insertBatchSomeColumn(真正的批量插入)進行性能對比----(jdbc均開啟rewriteBatchedStatements):

原來的假的批量插入:

@Test  void insert(){      for (int i = 0; i < 50000; i++) {          User user = new User();                 
Mybatis Plus 批量插入這樣操作提升性能

自定義的insertBatchSomeColumn:

@Testvoid contextLoads() {    for (int i = 0; i < 50000; i++) {        User user = new User           
Mybatis Plus 批量插入這樣操作提升性能

分批插入5w條資料,自定義的真正意義上的批量插入耗時減少了3秒左右,用insertBatchSomeColum分批插入1500條資料耗時650毫秒,這速度已經挺快了

Mybatis Plus 批量插入這樣操作提升性能