news 2026/9/12 18:48:54

MyBatis-Plus批量插入性能优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis-Plus批量插入性能优化实践

1. 为什么需要自己实现批量插入?

MyBatis-Plus作为MyBatis的增强工具,虽然提供了很多便捷的CRUD操作,但在批量插入方面确实存在一些局限性。官方提供的saveBatch()方法底层实际上是循环执行单条插入,当数据量达到500条时性能下降明显。我在实际项目中就遇到过这样的场景:导入Excel数据时,3000条记录用了近20秒才完成插入,这显然无法满足业务需求。

批量插入与单条循环插入的本质区别在于SQL语句的生成方式。前者使用INSERT INTO table (col1,col2) VALUES (v1,v2),(v3,v4)...的语法,一次网络往返完成多条数据插入;而后者则是多次执行INSERT INTO table (col1,col2) VALUES (v1,v2)。根据MySQL官方文档,批量插入的性能通常比单条插入快10倍以上。

2. 实现方案设计与选型

2.1 基于XML映射文件的传统方案

最直接的实现方式是在Mapper XML中编写批量插入SQL:

<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO user (name, age) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.age}) </foreach> </insert>

这种方案的优点是简单直观,但缺点也很明显:需要为每个实体单独编写XML,且当表结构变更时需要同步修改多个文件。我在早期项目中采用过这种方式,随着业务发展,维护成本越来越高。

2.2 基于注解的动态SQL方案

为了避免XML的维护问题,可以考虑使用@Insert注解配合动态SQL:

@Insert({ "<script>", "INSERT INTO user (name, age) VALUES ", "<foreach collection='list' item='item' separator=','>", "(#{item.name}, #{item.age})", "</foreach>", "</script>" }) int batchInsert(@Param("list") List<User> list);

这种方案虽然摆脱了XML文件,但SQL字符串拼接容易出错,且IDE的语法检查支持较弱。我在团队协作项目中发现,开发人员经常因为漏写script标签或参数绑定错误导致运行时异常。

2.3 基于SQL注入器的扩展方案

经过多次实践,我认为最优雅的方案是扩展MyBatis-Plus的SQL注入器机制。这种方案的核心思想是:

  1. 自定义SQL方法
  2. 注入到BaseMapper
  3. 动态生成批量插入SQL

具体实现需要以下几个组件:

  • 自定义InsertBatch方法
  • SQL注入器注册机制
  • 动态SQL生成器

3. 完整实现步骤

3.1 定义批量插入方法接口

首先在自定义Mapper接口中声明方法:

public interface CustomBaseMapper<T> extends BaseMapper<T> { /** * 批量插入(自定义实现) * @param entityList 实体列表 * @return 影响行数 */ int insertBatch(@Param("list") List<T> entityList); }

3.2 实现SQL注入器

创建自定义SQL注入器继承AbstractSqlInjector:

public class CustomSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methodList = super.getMethodList(mapperClass); methodList.add(new InsertBatch()); return methodList; } }

3.3 实现InsertBatch方法

关键部分是实现AbstractMethod的子类:

public class InsertBatch extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { String sql = "<script>INSERT INTO %s %s VALUES %s</script>"; String fieldSql = prepareFieldSql(tableInfo); String valueSql = prepareValuesSql(tableInfo); String finalSql = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql); SqlSource sqlSource = languageDriver.createSqlSource( configuration, finalSql, modelClass); return this.addInsertMappedStatement( mapperClass, modelClass, "insertBatch", sqlSource, new NoKeyGenerator(), null, null); } private String prepareFieldSql(TableInfo tableInfo) { StringBuilder fieldSql = new StringBuilder("("); tableInfo.getFieldList().forEach(field -> { fieldSql.append(field.getColumn()).append(","); }); fieldSql.deleteCharAt(fieldSql.length() - 1); fieldSql.append(")"); return fieldSql.toString(); } private String prepareValuesSql(TableInfo tableInfo) { StringBuilder valueSql = new StringBuilder("<foreach collection=\"list\" item=\"item\" separator=\",\">("); tableInfo.getFieldList().forEach(field -> { valueSql.append("#{item.").append(field.getProperty()).append("},"); }); valueSql.deleteCharAt(valueSql.length() - 1); valueSql.append(")</foreach>"); return valueSql.toString(); } }

3.4 配置Spring Bean

最后在Spring配置中注册SQL注入器:

@Bean public CustomSqlInjector customSqlInjector() { return new CustomSqlInjector(); }

4. 性能优化与注意事项

4.1 批处理大小控制

即使使用批量插入,也需要注意单次操作的数据量。根据MySQL官方建议,单个INSERT语句最好不要超过1MB。我通常设置每1000条数据执行一次批量插入:

public <T> int batchInsert(List<T> dataList, int batchSize) { int total = 0; for (int i = 0; i < dataList.size(); i += batchSize) { List<T> subList = dataList.subList(i, Math.min(i + batchSize, dataList.size())); total += mapper.insertBatch(subList); } return total; }

4.2 事务管理

批量插入必须放在事务中执行,否则部分失败会导致数据不一致:

@Transactional public void batchInsertUsers(List<User> users) { userMapper.insertBatch(users); }

重要提示:Spring的默认事务超时时间是-1(无限制),对于大数据量插入建议设置合理超时时间:@Transactional(timeout = 30)

4.3 主键生成策略

如果使用自增ID,需要注意批量插入后无法获取所有记录的ID。解决方案包括:

  1. 使用UUID等客户端生成的主键
  2. 插入后执行查询获取ID范围
  3. 使用JDBC的getGeneratedKeys方法(需要额外处理)

5. 实际效果对比测试

为了验证优化效果,我进行了以下测试(环境:MySQL 5.7,10000条记录):

方式耗时(ms)内存占用(MB)
saveBatch452185
自定义批量插入62332
原生JDBC批量58728

测试结果表明:

  1. 自定义批量插入比saveBatch快7倍以上
  2. 内存占用减少约60%
  3. 与原生JDBC方案性能接近,但开发效率更高

6. 常见问题排查

6.1 SQL语法错误

错误现象:SQLSyntaxErrorException: You have an error in your SQL syntax

可能原因:

  1. 表名或字段名包含MySQL关键字未转义
  2. VALUES列表末尾有多余逗号
  3. 字段数与值数不匹配

解决方案:

// 在prepareFieldSql和prepareValuesSql方法中添加日志 log.debug("Generated SQL: {}", finalSql);

6.2 数据类型不匹配

错误现象:DataIntegrityViolationException或类型转换异常

处理建议:

  1. 检查实体类字段类型与数据库类型
  2. 特别处理LocalDateTime等时间类型
  3. 使用@TableField注解指定jdbcType

6.3 批量大小限制

错误现象:PacketTooBigException

解决方案:

  1. 调整MySQL的max_allowed_packet参数
  2. 减小单次批量插入的数据量
  3. 在连接字符串中添加参数:jdbc:mysql://...?maxAllowedPacket=16777216

7. 高级扩展方案

对于更复杂的场景,可以考虑以下优化:

7.1 多线程批量插入

// 使用线程池并行处理 ExecutorService executor = Executors.newFixedThreadPool(4); List<Future<Integer>> futures = new ArrayList<>(); for (int i = 0; i < 4; i++) { final int start = i * 2500; futures.add(executor.submit(() -> { return batchInsert(dataList.subList(start, start + 2500), 500); })); } int total = futures.stream().mapToInt(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } }).sum();

7.2 使用LOAD DATA INFILE

对于超大数据量(百万级),可以考虑生成CSV文件后使用MySQL的LOAD DATA命令:

String sql = "LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE " + tableName + " FIELDS TERMINATED BY ','"; jdbcTemplate.execute(sql);

7.3 与MyBatis-Plus租户插件集成

如果项目使用了多租户插件,需要在SQL生成时考虑tenant_id字段:

private String prepareFieldSql(TableInfo tableInfo) { StringBuilder fieldSql = new StringBuilder("("); // 添加租户字段 if (tableInfo.isWithTenantId()) { fieldSql.append(tableInfo.getTenantIdColumn()).append(","); } // ...原有字段处理 }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 18:48:30

Python异常处理系统化实践与架构设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 18:48:18

CMSIS-6:从寄存器映射到计算资源编排的嵌入式范式革命

1. 项目概述&#xff1a;CMSIS‑6不是升级补丁&#xff0c;而是嵌入式开发范式的重写CMSIS‑6这个标题里藏着一个被多数工程师低估的信号——它不是CMSIS‑5的简单迭代&#xff0c;而是一次针对Cortex系列芯片全栈开发流程的底层重构。我从2014年用Keil MDK跑第一个Cortex‑M3裸…

作者头像 李华
网站建设 2026/9/12 18:46:51

BI选型六大核心能力:数据接入韧性到运维轻量级

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 18:44:26

品牌发稿怎样告别无效曝光?传播易的交付保障靠谱吗

对于企业市场负责人来说&#xff0c;品牌新闻发稿最棘手的困境&#xff0c;往往并非预算不足&#xff0c;而是历经多轮沟通、走完层层内部审批之后&#xff0c;最终投放效果和平台宣传承诺严重不符&#xff0c;投入付诸东流&#xff0c;传播成果难以兑现。 当前企业传播发稿赛道…

作者头像 李华
网站建设 2026/9/12 18:43:45

智能家居与物联网实战:改个阈值不用再改YAML:用Helpers把自动化参数交还给家人

智能家居与物联网实战:改个阈值不用再改YAML:用Helpers把自动化参数交还给家人 [!NOTE] 每次想调整延时或温度阈值,都要打开YAML改代码,自动化很难真正融入家庭。Helpers让开关、数字和模式成为界面可操作的实体,同时保留范围和类型。本课用人工保持、提示阈值和房间模式建…

作者头像 李华