ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MyBatisPlus批量插入性能优化实战

2026/8/3 2:03:16 拓冰建站 浏览量
MyBatisPlus批量插入性能优化实战

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

MyBatisPlus作为MyBatis的增强工具,虽然提供了很多便捷的CRUD操作,但在批量插入方面确实存在一些局限性。官方提供的saveBatch方法在实际使用中会遇到几个典型问题:

  1. 性能瓶颈:默认的saveBatch实际上是循环执行单条INSERT语句,当数据量达到500条以上时,性能下降明显
  2. 事务管理:原生批量插入在事务失败时缺乏细粒度的控制能力
  3. 特殊需求:无法支持INSERT IGNORE、ON DUPLICATE KEY UPDATE等特殊语法

我在电商系统开发中就遇到过这样的场景:需要一次性导入3000+商品SKU数据,使用默认的saveBatch方法耗时超过30秒,而改造后的批量插入方案仅需3秒左右。

2. 批量插入的三种实现方案对比

2.1 原生JDBC批量操作

Connection connection = dataSource.getConnection(); connection.setAutoCommit(false); PreparedStatement ps = connection.prepareStatement("INSERT INTO user(name,age) VALUES(?,?)"); for (User user : userList) { ps.setString(1, user.getName()); ps.setInt(2, user.getAge()); ps.addBatch(); } ps.executeBatch(); connection.commit();

优点

  • 执行效率最高
  • 内存占用可控

缺点

  • 需要手动管理连接和事务
  • 代码较为原始

2.2 MyBatis的foreach动态SQL

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

优点

  • 与MyBatis生态无缝集成
  • 支持MyBatis的所有特性

缺点

  • 超长SQL可能被数据库拒绝
  • 参数过多时性能下降

2.3 改造MyBatisPlus的saveBatch

这是我们重点要实现的方案,它能在保持MyBatisPlus使用习惯的同时获得更好的性能。

3. 深度改造saveBatch方法

3.1 基础实现原理

我们需要继承DefaultSqlInjector并重写inspectInject方法:

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

关键点在于InsertBatchSomeColumn这个自定义方法类:

public class InsertBatchSomeColumn extends AbstractMethod { @Override public MappedStatement injectMappedStatement(...) { String sql = "<script>INSERT INTO %s %s VALUES %s</script>"; SqlSource sqlSource = languageDriver.createSqlSource(...); return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatchSomeColumn", sqlSource); } }

3.2 性能优化技巧

  1. 分批处理:即使使用批量插入,单次也不宜超过1000条
public static <T> boolean saveBatch(Collection<T> entityList, int batchSize) { List<T> batchList = new ArrayList<>(); for (T entity : entityList) { batchList.add(entity); if (batchList.size() >= batchSize) { baseMapper.insertBatchSomeColumn(batchList); batchList.clear(); } } if (!batchList.isEmpty()) { baseMapper.insertBatchSomeColumn(batchList); } return true; }
  1. 字段过滤:只插入必要的字段
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY) private String name;
  1. 连接池配置:适当增大连接池的maxWait和maxActive参数

3.3 事务增强实现

建议结合Spring的@Transactional注解实现更灵活的事务控制:

@Transactional(rollbackFor = Exception.class) public void batchInsertWithTransaction(List<User> users) { // 分批插入逻辑 int batchSize = 500; for (int i = 0; i < users.size(); i += batchSize) { List<User> batchList = users.subList(i, Math.min(i + batchSize, users.size())); userMapper.insertBatchSomeColumn(batchList); // 业务逻辑处理 processBatch(batchList); } }

4. 实战中的坑与解决方案

4.1 主键冲突问题

现象:批量插入时遇到主键冲突导致整个批次失败

解决方案

  1. 使用INSERT IGNORE语法(MySQL)
@Insert("<script>INSERT IGNORE INTO user(name,age) VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.name},#{item.age})</foreach></script>") void insertBatchIgnore(List<User> users);
  1. 使用ON DUPLICATE KEY UPDATE
@Insert("<script>INSERT INTO user(name,age) VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.name},#{item.age})</foreach> " + "ON DUPLICATE KEY UPDATE age=VALUES(age)</script>") void insertBatchOrUpdate(List<User> users);

4.2 大数据量内存溢出

现象:导入10万条数据时出现OOM

解决方案

  1. 使用流式处理
try (BufferedReader reader = new BufferedReader(new FileReader(file))) { String line; List<User> batch = new ArrayList<>(1000); while ((line = reader.readLine()) != null) { User user = parseLine(line); batch.add(user); if (batch.size() >= 1000) { userMapper.insertBatchSomeColumn(batch); batch.clear(); } } if (!batch.isEmpty()) { userMapper.insertBatchSomeColumn(batch); } }
  1. 调整JVM参数:-XX:+UseG1GC -Xmx2g

4.3 字段类型不匹配

现象:枚举类型字段插入失败

解决方案

  1. 实现自定义TypeHandler
@MappedTypes(UserType.class) public class UserTypeHandler extends BaseTypeHandler<UserType> { @Override public void setNonNullParameter(PreparedStatement ps, int i, UserType parameter, JdbcType jdbcType) { ps.setInt(i, parameter.getCode()); } // 其他方法实现... }
  1. 在字段上指定TypeHandler
@TableField(typeHandler = UserTypeHandler.class) private UserType userType;

5. 性能对比测试

使用JMH进行基准测试(单位:ops/ms):

数据量saveBatch改造后方案提升幅度
100120450275%
500253801420%
1000123502816%
5000228013900%

测试环境:

  • MySQL 8.0
  • 连接池:HikariCP
  • 机器配置:4核8G

6. 生产环境最佳实践

  1. 监控指标

    • 执行时间阈值报警(超过1秒)
    • 批次大小监控(建议200-500条/批)
    • 失败率监控(超过1%需要预警)
  2. 熔断机制

public class BatchInsertCircuitBreaker { private static final int FAILURE_THRESHOLD = 5; private int failureCount = 0; public boolean allowExecution() { return failureCount < FAILURE_THRESHOLD; } public void recordFailure() { failureCount++; } public void recordSuccess() { failureCount = 0; } }
  1. 灰度发布策略

    • 先对10%的流量启用新方案
    • 监控1小时无异常后全量
  2. 数据验证

public void validateBatch(List<User> users) { if (users == null || users.isEmpty()) { throw new IllegalArgumentException("数据不能为空"); } if (users.size() > 1000) { throw new IllegalArgumentException("单批次不得超过1000条"); } // 其他业务规则校验... }

7. 扩展思考:如何支持多数据源

对于使用dynamic-datasource等多数据源场景,需要特别注意:

  1. 在自定义方法上添加@DS注解
public interface UserMapper extends BaseMapper<User> { @DS("slave_1") int insertBatchSomeColumn(List<User> users); }
  1. 事务管理器配置
@Bean public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
  1. 连接泄漏防护
public void batchInsertMultiDS(List<User> users) { try { // 操作逻辑 } finally { DynamicDataSourceContextHolder.clear(); } }

在实际项目中,我建议将批量插入操作封装成独立的Service,这样既能保持代码整洁,又方便统一管理事务和异常处理。一个经验法则是:单批次数据量控制在300-500条,总执行时间不超过1秒,这样的配置在大多数场景下都能取得不错的平衡。