mybatis-plus 批量插入效率低的问题【重写sql注入 SqlInjector】

背景

由于项目中需要大批量将数据插入数据库,直接使用mybatis-plus中的批量插入方法,结果发现效率奇低无比,线上批量插入一千条数据居然花销八九秒的时间。而我们的目标是想要单次插入一万条数据,这样的效率完全无法接受。

问题追踪

mybatis-plus的源码IService中是有单次批量插入的大小,默认的DEFAULT_BATCH_SIZE=1000,可以看到很多批量方法里面都有设置;通过修改调用方法的入参值,可以增加单次批量插入的数据,但实际发现并没有什么提升。以下为mybatis-plus中service源码:

public interface IService {

    /**
     * 默认批次提交数量
     */
    int DEFAULT_BATCH_SIZE = 1000;

    /**
     * 插入一条记录(选择字段,策略插入)
     *
     * @param entity 实体对象
     */
    default boolean save(T entity) {
        return SqlHelper.retBool(getBaseMapper().insert(entity));
    }

    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveBatch(Collection entityList) {
        return saveBatch(entityList, DEFAULT_BATCH_SIZE);
    }

    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     * @param batchSize  插入批次数量
     */
    boolean saveBatch(Collection entityList, int batchSize);

    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveOrUpdateBatch(Collection entityList) {
        return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE);
    }

    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     * @param batchSize  每次的数量
     */
    boolean saveOrUpdateBatch(Collection entityList, int batchSize);

    ......

}

继续接着上插入的源码研究,发现底层在SqlHeper类中有个executeBatch的方法有点异常。该方法显示 sqlSession.flushStatements()的调用居然是循环的。也就是说sql层面实际上是一堆insert语句再sqlSession中循环flush,而不是一个大insert一次flush操作完。这就是效率低的本质原因。

/**
     * 执行批量操作
     *
     * @param entityClass 实体类
     * @param log         日志对象
     * @param list        数据集合
     * @param batchSize   批次大小
     * @param consumer    consumer
     * @param          T
     * @return 操作结果
     * @since 3.4.0
     */
    public static  boolean executeBatch(Class entityClass, Log log, Collection list, int batchSize, BiConsumer consumer) {
        Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
        return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, 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++;
            }
        });
    }

解决方案

因为上述原因,考虑自己写一个批量插入的sql语句,这是最简单的。

但我们此处不采取此方法,而是直接重写sql注入。以下DefaultSqlInjector为mybatis-plus默认的sql注入实现类,继承的是AbstractSqlInjector类。该默认实现类中添加的Insert、Delete、DeleteByMap等等,实际上就是对应Mapper中所调用的各种方法。

/**
 * SQL 默认注入器
 *
 * @author hubin
 * @since 2018-04-10
 */
public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List getMethodList(Class mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

 我们需要在默认实现的基础上将额外的sql注入进去,所以直接继承默认的实现类做改进。以下为源码:

/**
 * 重写DefaultSqlInjector
 */
public class SqlInjectorPlus extends DefaultSqlInjector {

    @Override
    public List getMethodList(Class mapperClass) {
        //继承原有方法
        List methodList = super.getMethodList(mapperClass);
        //注入新方法
        methodList.add(new InsertBatchSomeColumn());
        return methodList;
    }

}

/**
 * 注入
 */
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {

    /**
     * 增强sql注入的Bean
     *
     * @return
     */
    @Bean
    public SqlInjectorPlus sqlInjectorPlus() {
        return new SqlInjectorPlus();
    }
}


/**
 * 重写BaseMapper
 */
public interface BaseMapperPlus  extends BaseMapper {

    /**
     * 高效率批量插入
     * entityList数量不能太大,否则存在丢包问题;
     * 单次entityList数量务必控制在一万内,或者在service中再次封装控制数量;
     * @param entityList 数据列表
     * @return 成功标示
     */
    Integer insertBatchSomeColumn(Collection entityList);

}

SqlInjectorPlus继承 DefaultSqlInjector 进行重写,继承原有增删改查方法,加入新方法InsertBatchSomeColumn(该类为mybatis-plus源码中就有的,但并未放开来使用)。用MybatisPlusConfig 将Bean注入到spring管理,最后再重写一个BaseMapper并加入新方法。后续的mapper直接继承BaseMapperPlus 就可以调用批量插入的insertBatchSomeColumn方法了。

InsertBatchSomeColumn类

该类为mybatis-plus源码中就有的,但并未放开来使用。主要目的就是实现批量插入,生产的是一个单个大insert语句,注意如果数据量也不宜过大。因为单次flush一个大sql过去,如果数据量过大,产生丢包,则会导致该此批量插入失败。最好再service中再封装一次,做成分批循环调用。该类的源码如下:


/**
 * 批量新增数据,自选字段 insert
 * 

不同的数据库支持度不一样!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!!

*

除了主键是 数据库自增的未测试 外理论上都可以使用!!!

*

如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!!

*

* 自己的通用 mapper 如下使用: *

 * int insertBatchSomeColumn(List entityList);
 * 
*

* *
  • 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值
  • * *

    * 常用的 {@link Predicate}: *

    * *
  • 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段
  • *
  • 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段
  • *
  • 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段
  • * * @author miemie * @since 2018-11-29 */ @NoArgsConstructor @AllArgsConstructor public class InsertBatchSomeColumn extends AbstractMethod { /** * 字段筛选条件 */ @Setter @Accessors(chain = true) private Predicate predicate; @SuppressWarnings("Duplicates") @Override public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { KeyGenerator keyGenerator = new NoKeyGenerator(); SqlMethod sqlMethod = SqlMethod.INSERT_ONE; List fieldList = tableInfo.getFieldList(); String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) + this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY); String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET; String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(ENTITY_DOT, false) + this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY); insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET; String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA); String keyProperty = null; String keyColumn = null; // 表包含主键处理逻辑,如果不包含主键当普通字段处理 if (tableInfo.havePK()) { if (tableInfo.getIdType() == IdType.AUTO) { /* 自增主键 */ keyGenerator = new Jdbc3KeyGenerator(); keyProperty = tableInfo.getKeyProperty(); keyColumn = tableInfo.getKeyColumn(); } else { if (null != tableInfo.getKeySequence()) { keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant); keyProperty = tableInfo.getKeyProperty(); keyColumn = tableInfo.getKeyColumn(); } } } String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn); } @Override public String getMethod(SqlMethod sqlMethod) { // 自定义 mapper 方法名 return "insertBatchSomeColumn"; } }

     总结:

    mybatis-plus批量插入效率低的本质原因是底层代码中在sqlsession中循环flush的多条insert语句,因此改进方案有两个:1.写一个sql实现循环插入;2.重写DefaultSqlInjector类,加入自带的InsertBatchSomeColumn。

    你可能感兴趣的:(问题记录,java,batch,mybatis)