Пакетная обработка MyBatis-Plus имеет ямки, я научу вас преобразовывать

Java MyBatis
Пакетная обработка MyBatis-Plus имеет ямки, я научу вас преобразовывать

Учебная комната Cabbage Java охватывает основные знания

1. Проблемы с пакетной производительностью MyBatis-Plus

MyBatis-Plus (сокращенно MP) — это инструмент расширения для MyBatis, который только дополняет, а не изменяет на основе MyBatis, создан для упрощения разработки и повышения эффективности.

Хотя MyBatis-Plus упрощает разработку, когда в реальной критической точке бизнеса необходимо выбрать базовую схему реализации SQL, оказывается, что его реализация по умолчанию не самая лучшая, особенно пакетная часть, давайте посмотрим на его исходный код (версия 3.3. 2):

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

Сначала посмотримНовая часть партии:

public interface IService<T> {

    /**
     * 默认批次提交数量
     */
    int DEFAULT_BATCH_SIZE = 1000;
    
    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveBatch(Collection<T> entityList) {
        return saveBatch(entityList, DEFAULT_BATCH_SIZE);
    }
    
    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     * @param batchSize  插入批次数量
     */
    boolean saveBatch(Collection<T> entityList, int batchSize);
    
}

Рекомендуется прочитать статью автора для получения предварительных знаний, связанных с исходным кодом MyBatis:Продвинутый путь Java-инженеров MyBatis

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {

    /**
     * 批量插入
     *
     * @param entityList ignore
     * @param batchSize  ignore
     * @return ignore
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = sqlStatement(SqlMethod.INSERT_ONE);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
    }
    
    /**
     * 执行批量操作
     *
     * @param list      数据集合
     * @param batchSize 批量大小
     * @param consumer  执行方法
     * @param <E>       泛型
     * @return 操作结果
     * @since 3.3.1
     */
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
        return !CollectionUtils.isEmpty(list) && executeBatch(sqlSession -> {
            int size = list.size();
            int i = 1;
            for (E element : list) {
                consumer.accept(sqlSession, element);
                if ((i % batchSize == 0) || i == size) {
                    sqlSession.flushStatements();
                }
                i++;
            }
        });
    }
    
}

На самом деле, когда я вижу это, я обнаружил, что пакетное добавление MyBatis-Plus заключается в открытии оператора вставки транзакции нажатиемОтправка одной партииДа, давайте посмотрим поближе.

    /**
     * 执行批量操作
     *
     * @param consumer consumer
     * @since 3.3.0
     * @deprecated 3.3.1 后面我打算移除掉 {@link #executeBatch(Collection, int, BiConsumer)} }.
     */
    @Deprecated
    protected boolean executeBatch(Consumer<SqlSession> consumer) {
        SqlSessionFactory sqlSessionFactory = SqlHelper.sqlSessionFactory(entityClass);
        SqlSessionHolder sqlSessionHolder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sqlSessionFactory);
        boolean transaction = TransactionSynchronizationManager.isSynchronizationActive();
        if (sqlSessionHolder != null) {
            SqlSession sqlSession = sqlSessionHolder.getSqlSession();
            //原生无法支持执行器切换,当存在批量操作时,会嵌套两个session的,优先commit上一个session
            //按道理来说,这里的值应该一直为false。
            sqlSession.commit(!transaction);
        }
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        if (!transaction) {
            log.warn("SqlSession [" + sqlSession + "] was not registered for synchronization because DataSource is not transactional");
        }
        try {
            consumer.accept(sqlSession);
            //非事物情况下,强制commit。
            sqlSession.commit(!transaction);
            return true;
        } catch (Throwable t) {
            sqlSession.rollback();
            Throwable unwrapped = ExceptionUtil.unwrapThrowable(t);
            if (unwrapped instanceof RuntimeException) {
                MyBatisExceptionTranslator myBatisExceptionTranslator
                    = new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true);
                throw Objects.requireNonNull(myBatisExceptionTranslator.translateExceptionIfPossible((RuntimeException) unwrapped));
            }
            throw ExceptionUtils.mpe(unwrapped);
        } finally {
            sqlSession.close();
        }
    }

Тогда мы увидимРаздел массового обновления, нашел так же:

public interface IService<T> {

    /**
     * 默认批次提交数量
     */
    int DEFAULT_BATCH_SIZE = 1000;
    
    /**
     * 根据ID 批量更新
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean updateBatchById(Collection<T> entityList) {
        return updateBatchById(entityList, DEFAULT_BATCH_SIZE);
    }

    /**
     * 根据ID 批量更新
     *
     * @param entityList 实体对象集合
     * @param batchSize  更新批次数量
     */
    boolean updateBatchById(Collection<T> entityList, int batchSize);
    
}
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateBatchById(Collection<T> entityList, int batchSize) {
        String sqlStatement = sqlStatement(SqlMethod.UPDATE_BY_ID);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, entity);
            sqlSession.update(sqlStatement, param);
        });
    }

    /**
     * 执行批量操作
     *
     * @param list      数据集合
     * @param batchSize 批量大小
     * @param consumer  执行方法
     * @param <E>       泛型
     * @return 操作结果
     * @since 3.3.1
     */
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
        return !CollectionUtils.isEmpty(list) && executeBatch(sqlSession -> {
            int size = list.size();
            int i = 1;
            for (E element : list) {
                consumer.accept(sqlSession, element);
                if ((i % batchSize == 0) || i == size) {
                    sqlSession.flushStatements();
                }
                i++;
            }
        });
    }
    
}

Будь то пакетная вставка или пакетное обновление, MyBatis-Plus в конечном итоге вызовет по умолчаниюexecuteBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer)метод и используется в рамках одной транзакцииfor (E element : list) {}Партии из 1000 заявок подаются партиями на реализацию.

2. Mybatis выбирает лучшее решение для периодического производства

Вариант 1 (то же самое для вставки и обновления):

pulic boolean bathInsert(String statementId, List<Map> params) {
    SqlSession sqlSession = null;
    try {
      sqlSession = SqlsessionUtil.getSqlSession();
      for (Map param : params) {
          sqlSession.insert(statementId, param);
      }
      sqlSession.commit();
      return true;
    } catch (Exception e) {
      sqlSession.rollback();
      e.printStackTrace();
    } finally {
      SqlsessionUtil.closeSession(sqlSession);
    }
    return false;
  }

Вариант 2 (то же самое для вставки и обновления):

<insert id="batchInsert">
        INSERT INTO table
        (
            business_id,
            element_id,
            business_value
        )
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (#{item.business_id, jdbcType=VARCHAR},
            #{item.element_id, jdbcType=VARCHAR},
            #{item.business_value, jdbcType=VARCHAR})
        </foreach>
</insert>

Вывод сравнения:

Когда количество собираемых данных относительно велико, эффективность схемы 2 значительно повышается!

Способ 50 100 500 1000 штук
Вариант первый 178ms 266ms 841ms 1863ms
Вариант 2 156ms 211ms 395ms 456ms

Причина анализ:

Основная причина высокой эффективности выполнения заключается в том, что объем журнала (транзакции MySQL binlog и innodb позволяют журнал) должен быть уменьшен после слияния, что уменьшает объем данных и частоту очистки журнала, тем самым повышая эффективность. Комбинируя операторы SQL, можно также сократить количество синтаксических анализов операторов SQL и количество операций ввода-вывода при передаче по сети.

Меры предосторожности:

  1. Заявление SQL имеет ограничение длины. При объединении данных в том же SQL он не должен превышать предел длины SQL. Он может быть изменен через конфигурацию Max_Ally_Packet. По умолчанию 1M, и он модифицируется до 8 м во время тестирования.
  2. Размер транзакций должен быть определен, а слишком большие транзакции могут повлиять на эффективность выполнения. В MySQL есть пункт конфигурации innodb_log_buffer_size, при превышении которого данные innodb будут сброшены на диск, в это время эффективность снизится. Поэтому лучше зафиксировать транзакцию до того, как данные достигнут этого значения.

3. Модернизация пакетной реализации MyBatis-Plus

Примечание. Эта реализация требуетОбратите особое внимание на ограничение длины операторов SQL базы данных., при объединении данных в один и тот же SQL он не должен превышать предел длины SQL, черезmax_allowed_packetКонфигурацию можно изменить, по умолчанию установлено значение 1M, а во время тестирования оно изменяется на 8M.

image.png

  1. Добавьте классы InsertBatchMethod и UpdateBatchMethod.:
@Slf4j
public class InsertBatchMethod extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        final String sql = "<script>insert into %s %s values %s</script>";
        final String fieldSql = prepareFieldSql(tableInfo);
        final String valueSql = prepareValuesSql(tableInfo);
        final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
        // log.debug("sqlResult----->{}", sqlResult);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatch", sqlSource, new NoKeyGenerator(), null, null);
    }

    private String prepareFieldSql(TableInfo tableInfo) {
        StringBuilder fieldSql = new StringBuilder();
        fieldSql.append(tableInfo.getKeyColumn()).append(",");
        tableInfo.getFieldList().forEach(x -> fieldSql.append(x.getColumn()).append(","));
        fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
        fieldSql.insert(0, "(");
        fieldSql.append(")");
        return fieldSql.toString();
    }

    private String prepareValuesSql(TableInfo tableInfo) {
        final StringBuilder valueSql = new StringBuilder();
        valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
        valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
        tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
        valueSql.delete(valueSql.length() - 1, valueSql.length());
        valueSql.append("</foreach>");
        return valueSql.toString();
    }

}
@Slf4j
public class UpdateBatchMethod extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql = "<script>\n<foreach collection=\"list\" item=\"item\" separator=\";\">\nupdate %s %s where %s=#{%s} %s\n</foreach>\n</script>";
        String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true);
        String setSql = sqlSet(tableInfo.isLogicDelete(), false, tableInfo, false, "item", "item.");
        String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional);
        // log.debug("sqlResult----->{}", sqlResult);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatch", sqlSource);
    }

}
  1. Добавить пользовательский метод SQL Injector CustomedSQLinjector:
public class CustomizedSqlInjector extends DefaultSqlInjector {

    /**
     * 如果只需增加方法,保留mybatis plus自带方法,
     * 可以先获取super.getMethodList(),再添加add
     */
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        methodList.add(new InsertBatchMethod());
        methodList.add(new UpdateBatchMethod());
        return methodList;
    }

}
@Configuration
@EnableTransactionManagement
@MapperScan("com.xxx.xxx.mapper")
public class MybatisPlusConfig {

    @Bean
    public CustomizedSqlInjector customizedSqlInjector() {
        return new CustomizedSqlInjector();
    }

}
  1. Добавить универсальный картограф и сервис:
public interface BasicMapper<T> extends BaseMapper<T> {

    /**
     * 自定义批量插入
     */
    int insertBatch(@Param("list") List<T> list);

    /**
     * 自定义批量更新,条件为主键
     */
    int updateBatch(@Param("list") List<T> list);

}
public interface BasicService<T> extends IService<T> {

    int insertBatch(List<T> list);

    int updateBatch(List<T> list);

}
public class BasicServiceImpl<M extends BasicMapper<T>, T> extends ServiceImpl<M, T> implements BasicService<T> {

    @Override
    public int insertBatch(List<T> list) {
        return baseMapper.insertBatch(list);
    }

    @Override
    public int updateBatch(List<T> list) {
        return baseMapper.updateBatch(list);
    }

}

Я был занят в течение длительного времени, на самом деле, чтобы добиться следующих эффектов (план 2 выше):

<insert id="batchInsert">
        INSERT INTO table
        (
            business_id,
            element_id,
            business_value
        )
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (#{item.business_id, jdbcType=VARCHAR},
            #{item.element_id, jdbcType=VARCHAR},
            #{item.business_value, jdbcType=VARCHAR})
        </foreach>
</insert>

Подводя итог, код очень грубые, он просто выражает метод. Когда реальный объем данных достигает определенного уровня (требуется более 1000), необходимо уделять большое внимание предельному вниманию предел длины отчета о выписке базы данных SQL. Решение оптимизации SQL SLICING SLICING SLICING не является панацеей. Да, производительность может быть безопасно улучшена только в определенной сумме. Выбор схемы по умолчанию MyBatis-Plus должен также иметь определенную причину.