mybatis-plus 批量插入效率低的问题解决

 更新时间:2026年01月26日 09:21:55   作者:没钱的程序员  
本文揭示了Mybatis-Plus批量插入效率低的原因,并提供了解决方案,包括自定义SQL注入和重写BaseMapper实现高效批量插入,具有一定的参考价值,感兴趣的可以了解一下

背景

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

问题追踪

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

public interface IService<T> {
 
    /**
     * 默认批次提交数量
     */
    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<T> entityList) {
        return saveBatch(entityList, DEFAULT_BATCH_SIZE);
    }
 
    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     * @param batchSize  插入批次数量
     */
    boolean saveBatch(Collection<T> entityList, int batchSize);
 
    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveOrUpdateBatch(Collection<T> entityList) {
        return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE);
    }
 
    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     * @param batchSize  每次的数量
     */
    boolean saveOrUpdateBatch(Collection<T> 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 <E>         T
     * @return 操作结果
     * @since 3.4.0
     */
    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");
        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<AbstractMethod> 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<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //继承原有方法
        List<AbstractMethod> 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 <T> extends BaseMapper<T> {
 
    /**
     * 高效率批量插入
     * entityList数量不能太大,否则存在丢包问题;
     * 单次entityList数量务必控制在一万内,或者在service中再次封装控制数量;
     * @param entityList 数据列表
     * @return 成功标示
     */
    Integer insertBatchSomeColumn(Collection<T> entityList);
 
}

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

InsertBatchSomeColumn类

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

 
/**
 * 批量新增数据,自选字段 insert
 * <p> 不同的数据库支持度不一样!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!! </p>
 * <p> 除了主键是 <strong> 数据库自增的未测试 </strong> 外理论上都可以使用!!! </p>
 * <p> 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!! </p>
 * <p>
 * 自己的通用 mapper 如下使用:
 * <pre>
 * int insertBatchSomeColumn(List<T> entityList);
 * </pre>
 * </p>
 *
 * <li> 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值 </li>
 *
 * <p>
 * 常用的 {@link Predicate}:
 * </p>
 *
 * <li> 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段 </li>
 * <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段 </li>
 * <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段 </li>
 *
 * @author miemie
 * @since 2018-11-29
 */
@NoArgsConstructor
@AllArgsConstructor
public class InsertBatchSomeColumn extends AbstractMethod {
 
    /**
     * 字段筛选条件
     */
    @Setter
    @Accessors(chain = true)
    private Predicate<TableFieldInfo> predicate;
 
    @SuppressWarnings("Duplicates")
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        KeyGenerator keyGenerator = new NoKeyGenerator();
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        List<TableFieldInfo> 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。

到此这篇关于mybatis-plus 批量插入效率低的问题解决的文章就介绍到这了,更多相关mybatis-plus 批量插入效率低内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringMVC @RequestBody自动转json Http415错误的解决

    SpringMVC @RequestBody自动转json Http415错误的解决

    这篇文章主要介绍了SpringMVC @RequestBody自动转json Http415错误的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • springboot使用yml文件配置多环境方式(dev、test、prod)

    springboot使用yml文件配置多环境方式(dev、test、prod)

    这篇文章主要介绍了springboot使用yml文件配置多环境方式(dev、test、prod),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • SpringBoot如何通过Map实现天然的策略模式

    SpringBoot如何通过Map实现天然的策略模式

    文章介绍了如何在Spring框架中使用@Resource注解和Map集合来实现策略模式,并详细解释了Spring如何处理集合类型的依赖注入,通过这种方式,可以在运行时动态地选择算法的行为,使得代码更加灵活和可扩展
    2026-03-03
  • SpringBoot开发中的数据源详解

    SpringBoot开发中的数据源详解

    这篇文章主要介绍了SpringBoot开发中的数据源详解,数据源(Data Source)顾名思义,数据的来源,是提供某种所需要数据的器件或原始媒体,在数据源中存储了所有建立数据库连接的信息,需要的朋友可以参考下
    2023-09-09
  • MyBatis连接数据库配置的基本步骤和机制

    MyBatis连接数据库配置的基本步骤和机制

    MyBatis 是一个流行的持久层框架,它通过使用XML或注解的方式将SQL语句、存储过程和Java方法进行绑定,从而避免了手写大量的JDBC代码和手动设置参数与结果集,本文给大家介绍了MyBatis连接数据库配置的基本步骤和机制,需要的朋友可以参考下
    2024-05-05
  • Java使用Spire.Doc实现Word转PDF的完整方案

    Java使用Spire.Doc实现Word转PDF的完整方案

    在OA系统开发、电子合同生成等场景中,Java开发者在处理文档自动化时最常遇到的痛点:生成的Word文档通过传统POI转换PDF时格式丢失,本文介绍的Spire.Doc for Java方案能保持原版格式,实现Word转PDF,文中通过代码讲解的非常详细,需要的朋友可以参考下
    2025-08-08
  • java 生成xml并转为字符串的方法

    java 生成xml并转为字符串的方法

    今天小编就为大家分享一篇java 生成xml并转为字符串的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • Java中NIO的三大核心组件详细解析

    Java中NIO的三大核心组件详细解析

    这篇文章主要介绍了Java中NIO的三大核心组件详细解析,NIO的Buffer类是一个抽象类,位于java.nio包中,提供了一组更加有效的方法,用来进行写入和读取的交替访问,本质上是一个内存块,既可以写入数据,也可以从中读取数据,需要的朋友可以参考下
    2023-12-12
  • SpringBoot实现接口加密的五大技巧分享

    SpringBoot实现接口加密的五大技巧分享

    为了有效应对接口安全面临的挑战,接口加密作为一种至关重要的手段,能够将原始数据转化为密文进行传输,从而确保数据在传输过程中的机密性和完整性,本文将深入探讨SpringBoot接口加密的最佳实践,需要的朋友可以参考下
    2025-07-07
  • Java JTable 实现日历的示例

    Java JTable 实现日历的示例

    这篇文章主要介绍了Java JTable 实现日历的示例,帮助大家更好的理解和学习Java jtable的使用方法,感兴趣的朋友可以了解下
    2020-10-10

最新评论