1. 项目背景与核心挑战最近在开发一个用户行为分析系统时遇到了一个棘手的问题需要将每天产生的数十万条用户行为记录高效地存入MySQL数据库。最初尝试了最简单的单条插入方式结果发现完成10万条数据插入需要近5分钟这显然无法满足业务需求。于是我开始系统性地研究Spring Boot环境下各种批量插入方案的性能差异。批量数据插入是后端开发中的常见需求特别是在大数据处理、日志收集、报表生成等场景下。传统单条插入的方式会产生大量网络往返和事务开销导致性能瓶颈。Spring Boot生态提供了多种批量插入方案每种方案在易用性、性能和适用场景上各有特点。2. 环境准备与基础配置2.1 项目依赖配置首先创建一个Spring Boot 3.3项目在pom.xml中添加必要依赖dependencies !-- Spring Boot Starter Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 其他工具类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.2 数据库配置在application.yml中配置数据库连接和MyBatis-Plus相关参数spring: datasource: url: jdbc:mysql://localhost:3306/demo?useSSLfalserewriteBatchedStatementstrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl特别注意rewriteBatchedStatementstrue这个参数它能让MySQL服务器真正执行批量操作而不是将批量语句拆分成单条执行。这个参数对JDBC批处理性能影响巨大。3. 六种批量插入方案详解3.1 方案一JDBC原生批处理3.1.1 实现原理JDBC批处理是最底层的批量插入方式它通过PreparedStatement的addBatch()方法将多条SQL语句打包然后通过executeBatch()一次性发送到数据库执行。public long jdbcBatchInsert(ListUser users) throws SQLException { long start System.currentTimeMillis(); String sql INSERT INTO user (name, age) VALUES (?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (User user : users) { ps.setString(1, user.getName()); ps.setInt(2, user.getAge()); ps.addBatch(); // 每1000条执行一次批处理避免内存溢出 if (i % 1000 0) { ps.executeBatch(); } } ps.executeBatch(); // 执行剩余批次 } return System.currentTimeMillis() - start; }3.1.2 性能优化要点批处理大小建议每1000-5000条执行一次批处理太小会增加网络往返太大会占用过多内存事务控制整个批处理应该在一个事务中完成避免自动提交连接池配置适当增大连接池大小避免批处理占用连接时间过长3.1.3 实测数据数据量批处理大小执行时间(ms)10,0001,0001,20050,0005,0003,800100,00010,0007,500注意实际性能会受网络延迟、数据库负载等因素影响3.2 方案二MyBatis-Plus的saveBatch3.2.1 实现方式MyBatis-Plus提供的saveBatch是对JDBC批处理的封装使用起来更加简单public long mybatisPlusBatchInsert(ListUser users) { long start System.currentTimeMillis(); userService.saveBatch(users, 5000); // 第二个参数是批处理大小 return System.currentTimeMillis() - start; }3.2.2 底层原理saveBatch方法内部实现有几个关键点默认使用ExecutorType.BATCH执行器自动管理批处理提交间隔支持事务的自动回滚3.2.3 性能对比与原生JDBC批处理相比saveBatch会有约10-15%的性能损耗主要来自对象到SQL参数的转换开销MyBatis的拦截器链执行额外的类型检查和安全验证3.3 方案三SQL拼接批量插入3.3.1 实现代码public long sqlConcatInsert(ListUser users) { long start System.currentTimeMillis(); StringBuilder sql new StringBuilder(INSERT INTO user (name, age) VALUES ); for (int i 0; i users.size(); i) { User user users.get(i); sql.append(().append(user.getName()).append(,) .append(user.getAge()).append()); if (i ! users.size() - 1) { sql.append(,); } } jdbcTemplate.execute(sql.toString()); return System.currentTimeMillis() - start; }3.3.2 注意事项SQL长度限制MySQL默认最大包大小为4MB超过会报错SQL注入风险必须确保数据已经正确转义性能瓶颈超长SQL解析会消耗数据库资源3.3.3 适用场景适合一次性插入1000-5000条数据的场景超过这个量级建议分批次拼接。3.4 方案四MyBatis批处理模式3.4.1 实现方式通过SqlSession手动开启批处理模式public long mybatisBatchInsert(ListUser users) { long start System.currentTimeMillis(); SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper session.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); } session.commit(); } finally { session.close(); } return System.currentTimeMillis() - start; }3.4.2 优化建议适当调整batchSize参数在循环中定期flush批处理语句考虑使用二级缓存减少重复SQL解析3.5 方案五JdbcTemplate批处理3.5.1 实现代码public long jdbcTemplateBatchInsert(ListUser users) { long start System.currentTimeMillis(); jdbcTemplate.batchUpdate(INSERT INTO user (name, age) VALUES (?, ?), new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int i) throws SQLException { User user users.get(i); ps.setString(1, user.getName()); ps.setInt(2, user.getAge()); } Override public int getBatchSize() { return users.size(); } }); return System.currentTimeMillis() - start; }3.5.2 特点分析比原生JDBC批处理更安全自动管理资源释放与Spring事务管理无缝集成3.6 方案六多线程并行插入3.6.1 实现思路将大数据集分割成多个子集使用线程池并行处理public long parallelInsert(ListUser users, int threadCount) throws InterruptedException { long start System.currentTimeMillis(); ExecutorService executor Executors.newFixedThreadPool(threadCount); int batchSize users.size() / threadCount; ListCallableVoid tasks new ArrayList(); for (int i 0; i threadCount; i) { int from i * batchSize; int to (i threadCount - 1) ? users.size() : (i 1) * batchSize; ListUser subList users.subList(from, to); tasks.add(() - { jdbcBatchInsert(subList); // 使用前面介绍的JDBC批处理 return null; }); } executor.invokeAll(tasks); executor.shutdown(); return System.currentTimeMillis() - start; }3.6.2 注意事项线程数不宜超过数据库连接池大小需要考虑数据一致性和事务隔离表设计避免热点更新问题4. 性能对比与选型建议4.1 实测数据对比在相同环境下测试10万条数据插入方案执行时间(ms)CPU占用内存占用单条插入185,000中低JDBC批处理1,200高中MyBatis-Plus saveBatch1,800中中SQL拼接850低高MyBatis批处理模式2,100中中JdbcTemplate批处理1,500中中多线程并行(4线程)650极高高4.2 方案选型指南数据量1万MyBatis-Plus的saveBatch最简单1万-10万条JDBC批处理或SQL拼接10万条考虑多线程并行JDBC批处理需要事务支持避免使用SQL拼接方式内存敏感场景优先考虑JDBC批处理4.3 数据库参数优化为了获得最佳性能还需要调整数据库参数-- 增大最大允许包大小 SET GLOBAL max_allowed_packet256*1024*1024; -- 调整批量插入缓存 SET GLOBAL bulk_insert_buffer_size256*1024*1024; -- 临时关闭索引更新(大数据量插入时) ALTER TABLE user DISABLE KEYS; -- 插入完成后 ALTER TABLE user ENABLE KEYS;5. 常见问题与解决方案5.1 内存溢出问题问题现象插入大量数据时出现OOM解决方案分批次处理数据每批5000-10000条增加JVM堆内存-Xmx2g使用流式处理避免全量加载5.2 性能突然下降可能原因数据库连接池耗尽数据库临时表空间不足索引碎片化严重排查步骤监控数据库连接数检查数据库慢查询日志分析执行计划5.3 事务超时问题解决方案Transactional(timeout 1200) // 设置足够长的事务超时 public void batchInsert(ListUser users) { // 批处理逻辑 }或者将大事务拆分为多个小事务。6. 高级优化技巧6.1 使用LOAD DATA INFILE对于超大数据量(百万级以上)可以考虑使用MySQL的LOAD DATA INFILE命令public long loadDataInfile(File csvFile) throws SQLException { long start System.currentTimeMillis(); try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement()) { String sql String.format( LOAD DATA LOCAL INFILE %s INTO TABLE user FIELDS TERMINATED BY ,, csvFile.getAbsolutePath()); stmt.execute(sql); } return System.currentTimeMillis() - start; }这种方法比任何批处理方式都快但需要先将数据写入临时文件。6.2 存储过程批量插入对于频繁的批量插入场景可以考虑使用存储过程DELIMITER // CREATE PROCEDURE batch_insert_users(IN users JSON) BEGIN DECLARE i INT DEFAULT 0; DECLARE user_count INT; SET user_count JSON_LENGTH(users); WHILE i user_count DO INSERT INTO user (name, age) VALUES ( JSON_UNQUOTE(JSON_EXTRACT(users, CONCAT($[, i, ].name))), JSON_EXTRACT(users, CONCAT($[, i, ].age)) ); SET i i 1; END WHILE; END // DELIMITER ;6.3 使用Spring Batch框架对于需要复杂ETL流程的批量操作可以使用Spring BatchBean public Job importUserJob(JobRepository jobRepository, Step step1) { return new JobBuilder(importUserJob, jobRepository) .start(step1) .build(); } Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager txManager) { return new StepBuilder(step1, jobRepository) .User, Userchunk(5000, txManager) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) .build(); }7. 实际项目中的经验分享在电商订单系统中我们最终采用的方案是多线程分片JDBC批处理动态批次调整。核心优化点包括根据服务器CPU核心数动态设置线程数根据当前系统负载自动调整批处理大小在低峰期执行大数据量导入实现断点续传功能关键代码片段public class DynamicBatchInserter { private static final int MAX_THREADS Runtime.getRuntime().availableProcessors() * 2; private static final int BASE_BATCH_SIZE 5000; public void dynamicBatchInsert(ListOrder orders) { // 根据系统负载计算实际线程数 int actualThreads Math.min( MAX_THREADS, (int) (MAX_THREADS * (1 - SystemLoadAverage.get()))); // 根据数据量调整批次大小 int dynamicBatchSize BASE_BATCH_SIZE; if (orders.size() 100_000) { dynamicBatchSize 10_000; } // 执行分片批处理 // ... } }这种方案在我们的生产环境中实现了每分钟处理50万条订单记录的吞吐量。