MyBatisPlus批量插入性能优化实战

📅 2026/8/3 1:51:00
MyBatisPlus批量插入性能优化实战
1. 为什么需要自己实现MyBatisPlus批量插入MyBatisPlus作为MyBatis的增强工具虽然提供了很多便捷的CRUD操作但在批量插入方面确实存在一些局限性。官方提供的saveBatch方法在实际使用中会遇到几个典型问题性能瓶颈默认的saveBatch实际上是循环执行单条INSERT语句当数据量达到500条以上时性能下降明显事务管理原生批量插入在事务失败时缺乏细粒度的控制能力特殊需求无法支持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动态SQLinsert idbatchInsert INSERT INTO user(name, age) VALUES foreach collectionlist itemitem 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 ListAbstractMethod getMethodList(Class? mapperClass) { ListAbstractMethod methodList super.getMethodList(mapperClass); methodList.add(new InsertBatchSomeColumn()); return methodList; } }关键点在于InsertBatchSomeColumn这个自定义方法类public class InsertBatchSomeColumn extends AbstractMethod { Override public MappedStatement injectMappedStatement(...) { String sql scriptINSERT INTO %s %s VALUES %s/script; SqlSource sqlSource languageDriver.createSqlSource(...); return this.addInsertMappedStatement(mapperClass, modelClass, insertBatchSomeColumn, sqlSource); } }3.2 性能优化技巧分批处理即使使用批量插入单次也不宜超过1000条public static T boolean saveBatch(CollectionT entityList, int batchSize) { ListT 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; }字段过滤只插入必要的字段TableField(insertStrategy FieldStrategy.NOT_EMPTY) private String name;连接池配置适当增大连接池的maxWait和maxActive参数3.3 事务增强实现建议结合Spring的Transactional注解实现更灵活的事务控制Transactional(rollbackFor Exception.class) public void batchInsertWithTransaction(ListUser users) { // 分批插入逻辑 int batchSize 500; for (int i 0; i users.size(); i batchSize) { ListUser batchList users.subList(i, Math.min(i batchSize, users.size())); userMapper.insertBatchSomeColumn(batchList); // 业务逻辑处理 processBatch(batchList); } }4. 实战中的坑与解决方案4.1 主键冲突问题现象批量插入时遇到主键冲突导致整个批次失败解决方案使用INSERT IGNORE语法MySQLInsert(scriptINSERT IGNORE INTO user(name,age) VALUES foreach collectionlist itemitem separator, (#{item.name},#{item.age})/foreach/script) void insertBatchIgnore(ListUser users);使用ON DUPLICATE KEY UPDATEInsert(scriptINSERT INTO user(name,age) VALUES foreach collectionlist itemitem separator, (#{item.name},#{item.age})/foreach ON DUPLICATE KEY UPDATE ageVALUES(age)/script) void insertBatchOrUpdate(ListUser users);4.2 大数据量内存溢出现象导入10万条数据时出现OOM解决方案使用流式处理try (BufferedReader reader new BufferedReader(new FileReader(file))) { String line; ListUser 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); } }调整JVM参数-XX:UseG1GC -Xmx2g4.3 字段类型不匹配现象枚举类型字段插入失败解决方案实现自定义TypeHandlerMappedTypes(UserType.class) public class UserTypeHandler extends BaseTypeHandlerUserType { Override public void setNonNullParameter(PreparedStatement ps, int i, UserType parameter, JdbcType jdbcType) { ps.setInt(i, parameter.getCode()); } // 其他方法实现... }在字段上指定TypeHandlerTableField(typeHandler UserTypeHandler.class) private UserType userType;5. 性能对比测试使用JMH进行基准测试单位ops/ms数据量saveBatch改造后方案提升幅度100120450275%500253801420%1000123502816%5000228013900%测试环境MySQL 8.0连接池HikariCP机器配置4核8G6. 生产环境最佳实践监控指标执行时间阈值报警超过1秒批次大小监控建议200-500条/批失败率监控超过1%需要预警熔断机制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; } }灰度发布策略先对10%的流量启用新方案监控1小时无异常后全量数据验证public void validateBatch(ListUser users) { if (users null || users.isEmpty()) { throw new IllegalArgumentException(数据不能为空); } if (users.size() 1000) { throw new IllegalArgumentException(单批次不得超过1000条); } // 其他业务规则校验... }7. 扩展思考如何支持多数据源对于使用dynamic-datasource等多数据源场景需要特别注意在自定义方法上添加DS注解public interface UserMapper extends BaseMapperUser { DS(slave_1) int insertBatchSomeColumn(ListUser users); }事务管理器配置Bean public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) { return new DataSourceTransactionManager(dataSource); }连接泄漏防护public void batchInsertMultiDS(ListUser users) { try { // 操作逻辑 } finally { DynamicDataSourceContextHolder.clear(); } }在实际项目中我建议将批量插入操作封装成独立的Service这样既能保持代码整洁又方便统一管理事务和异常处理。一个经验法则是单批次数据量控制在300-500条总执行时间不超过1秒这样的配置在大多数场景下都能取得不错的平衡。