
1. 为什么需要自己实现MyBatisPlus批量插入MyBatisPlus作为MyBatis的增强工具虽然提供了基础的CRUD操作但在批量插入场景下存在几个明显的痛点。首先是性能问题默认的saveBatch()方法实际上是通过for循环执行单条INSERT语句当处理上千条数据时与真正的批量插入相比可能有10倍以上的性能差距。其次是功能限制比如单次操作默认限制500条记录可通过调整batchSize参数修改这在ETL等大数据量场景下显得捉襟见肘。更麻烦的是事务管理——默认实现中如果某条记录插入失败已执行的插入不会回滚这与业务系统要求的原子性操作相悖。我在电商订单系统中就遇到过这样的案例夜间批量导入10万条促销活动数据时使用默认方法耗时超过30分钟而改用真正的批量插入后只需45秒。更关键的是当遇到重复主键时系统需要完整回滚整个批次而不是部分成功。2. 三种主流批量插入方案对比2.1 JDBC原生批量操作最底层的实现方式是使用JDBC的addBatch()和executeBatch()方法。这种方案性能最佳实测插入1万条数据仅需800ms左右。核心原理是将多条INSERT语句合并为一个网络请求发送到数据库。Connection conn dataSource.getConnection(); conn.setAutoCommit(false); PreparedStatement ps conn.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(); conn.commit();关键点必须关闭自动提交(setAutoCommit(false))否则每条语句都会独立提交失去批量效果2.2 MyBatis的BATCH执行器MyBatis内置了BATCH执行器配置方式如下settings setting namedefaultExecutorType valueBATCH/ /settings或者在SqlSessionFactory中指定SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH);这种方案相比JDBC原生方式稍慢1万条约1.2秒但能与MyBatis的映射体系更好结合。需要注意的是执行批量操作后必须调用session.commit()否则可能因为缓存未刷新导致操作未执行。2.3 拼接SQL语句MySQL等数据库支持多值INSERT语法INSERT INTO user(name,age) VALUES(张三,20),(李四,22),(王五,25);通过StringBuilder拼接SQL虽然性能优异1万条约600ms但要特别注意SQL注入防护。建议使用MyBatis的 标签安全生成insert idbatchInsert INSERT INTO user(name,age) VALUES foreach collectionlist itemitem separator, (#{item.name},#{item.age}) /foreach /insert3. 完整实现方案与性能优化3.1 基于MyBatisPlus的增强实现我们可以在BaseMapper基础上扩展批量插入功能。首先定义接口public interface ExpandBaseMapperT extends BaseMapperT { /** * 真正批量插入JDBC批量模式 * param entityList 实体列表 * return 影响行数 */ int insertBatchSomeColumn(Param(list) ListT entityList); }实现类需要继承AbstractMethod并重写injectMappedStatement方法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); } }注册自定义方法public class MySqlInjector extends DefaultSqlInjector { Override public ListAbstractMethod getMethodList(Class? mapperClass) { ListAbstractMethod methods super.getMethodList(mapperClass); methods.add(new InsertBatchSomeColumn()); return methods; } }3.2 动态批次分割策略处理超大批量数据时如10万需要实现智能分批public class BatchInsertHelper { private static final int MAX_BATCH_SIZE 1000; public static T int batchInsert(ExpandBaseMapperT mapper, ListT data, int batchSize) { if(batchSize 0 || batchSize MAX_BATCH_SIZE) { batchSize MAX_BATCH_SIZE; } int total 0; ListListT partitions Lists.partition(data, batchSize); for(ListT batch : partitions) { total mapper.insertBatchSomeColumn(batch); // 每批提交防止内存溢出 SqlSessionHelper.flushStatements(); } return total; } }3.3 性能对比测试使用JMH进行基准测试单位毫秒数据量saveBatchJDBC批量SQL拼接1,0001,200806010,00012,500800600100,000超时8,2006,500实测发现当单批次超过5000条时某些数据库驱动会出现内存问题建议控制在1000-3000条/批4. 生产环境注意事项4.1 事务一致性处理批量操作必须考虑事务原子性。推荐使用编程式事务Transactional public void batchInsertWithTransaction(ListUser users) { try { batchInsertHelper.batchInsert(userMapper, users, 1000); } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw new BusinessException(批量插入失败, e); } }4.2 主键冲突处理根据业务需求选择处理方式忽略重复ON DUPLICATE KEY UPDATEINSERT INTO user(id,name) VALUES(1,张三) ON DUPLICATE KEY UPDATE nameVALUES(name);跳过重复INSERT IGNOREINSERT IGNORE INTO user(id,name) VALUES(1,张三);全有或全无严格事务4.3 监控与日志优化大批量操作需要添加监控点StopWatch watch new StopWatch(); watch.start(batchInsert); try { // 执行批量插入 } finally { watch.stop(); log.info(批量插入{}条数据耗时{}ms, data.size(), watch.getTotalTimeMillis()); Metrics.counter(db.batch.insert) .increment(data.size()); }5. 高级应用场景5.1 海量数据分片导入当处理百万级数据时可采用「生产者-消费者」模式// 创建有界队列 BlockingQueueListUser queue new ArrayBlockingQueue(10); // 生产者线程读数据 executor.submit(() - { while(hasMoreData()) { ListUser batch readNextBatch(1000); queue.put(batch); } }); // 消费者线程写数据库 executor.submit(() - { while(!queue.isEmpty()) { ListUser batch queue.take(); batchInsert(batch); } });5.2 与Spring Batch集成对于需要复杂ETL的场景可以结合Spring BatchBean public Step importStep() { return stepBuilderFactory.get(userImport) .User,Userchunk(1000) .reader(flatFileItemReader()) .processor(validationProcessor()) .writer(userItemWriter()) .build(); } Bean public ItemWriterUser userItemWriter() { return items - { userMapper.insertBatchSomeColumn(items); }; }5.3 多数据源适配不同数据库需要调整批量语法public class BatchInsertStrategy { public static String generateSql(String table, ListString columns, int size) { if(dataSourceType DataSourceType.MYSQL) { // 使用VALUES(),(),()语法 } else if(dataSourceType DataSourceType.ORACLE) { // 使用INSERT ALL INTO ... SELECT语法 } else if(dataSourceType DataSourceType.POSTGRESQL) { // 支持COPY命令 } } }6. 踩坑实录与解决方案内存溢出问题未及时清理的PreparedStatement会导致内存堆积解决每批执行后调用clearBatch()事务超时大批量操作超过默认事务超时时间解决Transactional(timeout 3600)主键生成冲突批量插入时雪花ID生成器产生重复ID解决提前批量生成ID或使用数据库序列连接泄漏异常情况下未关闭Connection解决try-with-resources语法MySQL的max_allowed_packet限制解决调整批次大小或修改MySQL配置-- 查看当前包大小 SHOW VARIABLES LIKE max_allowed_packet; -- 临时修改需重启 SET GLOBAL max_allowed_packet64*1024*1024;我在金融交易系统迁移项目中就遇到过因未考虑Oracle的批量语法差异导致凌晨数据同步失败的情况。后来通过抽象BatchInsertTemplate类对不同数据库实现差异化处理才彻底解决问题。这也印证了一个经验批量操作不能假设所有数据库行为一致必须针对目标数据库进行充分测试。