SpringBoot 批量插入/更新优化(MyBatis Batch)

张开发
2026/4/21 4:06:05 15 分钟阅读

分享文章

SpringBoot 批量插入/更新优化(MyBatis Batch)
在SpringBoot企业开发中批量插入、批量更新是高频场景——比如数据迁移、Excel导入、定时任务同步数据、日志批量归档等动辄需要处理上千条、上万条甚至百万级数据。很多同学一开始实现批量操作习惯用“循环单条插入/更新”的方式比如循环调用insert、update方法看似简单易写实则性能极差数据量稍大就会出现卡顿、超时甚至数据库崩溃。举个真实场景导入1万条用户数据用循环单条插入可能需要30秒以上甚至超时失败而用MyBatis Batch批量优化后可缩短至1秒内性能提升30倍以上差距非常明显。一、传统批量操作的3种错误方式在讲优化方案之前先盘点3种新手最常犯的错误批量方式明确为什么这些方式性能差避免大家踩坑。错误方式1循环单条插入/更新核心逻辑用for循环逐条调用Mapper的insert、update方法每条数据都单独发起一次数据库连接、执行一次SQL。// 错误示例循环单条插入千万不要这么写 Service public class UserService { Autowired private UserMapper userMapper; public void batchInsert(ListUser userList) { // 循环逐条插入每条都发起一次数据库请求 for (User user : userList) { userMapper.insert(user); } } }❌ 核心痛点• 频繁创建/释放数据库连接连接开销极大数据库连接是宝贵资源频繁切换会严重消耗性能• 每条SQL都要经过“解析-优化-执行”流程重复开销多数据量越大性能越差• 数据量达到1000条以上极易出现数据库连接超时、系统卡顿甚至触发数据库限流。错误方式2拼接SQL核心逻辑手动拼接SQL字符串将多条插入/更新语句拼接成一条SQL一次性执行比如 insert into user values (...), (...), (...)。-- 拼接SQL示例插入3条数据 insert into user (username, password, dept_id) values (zhangsan, 123456, 10), (lisi, 654321, 11), (wangwu, 111111, 10);❌ 核心痛点• SQL长度有限制拼接过多数据比如1万条会导致SQL过长超出数据库允许的最大长度直接报错• 手动拼接SQL容易出现语法错误比如逗号遗漏、引号不匹配排查困难• 存在SQL注入风险若拼接的参数未做过滤安全性低• 批量更新拼接SQLupdate ... where id in (...)无法批量更新不同字段值灵活性差。错误方式3未开启事务核心逻辑不管是循环单条操作还是批量操作未开启Spring事务每条SQL都是一个独立事务数据库需要频繁提交事务性能损耗严重。❌ 核心痛点事务提交是重量级操作频繁提交会占用大量数据库资源即使是优化后的批量方案未开启事务也会导致性能下降50%以上。二、MyBatis Batch 为什么能提升性能MyBatis Batch批量操作的本质是“预编译SQL 批量提交”核心是复用数据库连接和预编译SQL减少重复开销具体原理拆解如下底层逻辑面试高频1. 核心优化点•复用数据库连接批量操作过程中只获取一次数据库连接所有批量数据都通过这一个连接执行避免频繁创建/释放连接的开销•预编译SQL复用MyBatis会对批量SQL进行一次预编译生成预编译语句PreparedStatement后续所有批量数据都复用这个预编译语句避免重复解析、优化SQL的开销•批量提交事务开启Spring事务后所有批量操作完成后一次性提交事务减少事务提交的次数从N次提交变为1次提交大幅提升性能。2. MyBatis Batch 执行流程1. 开启Spring事务获取一个数据库连接Connection并绑定到当前线程2. MyBatis 创建批量执行器BatchExecutor复用当前数据库连接3. 开发者调用批量插入/更新方法MyBatis 将SQL预编译一次生成PreparedStatement4. 循环将批量数据的参数设置到预编译语句中暂存到批量执行器中不立即执行5. 所有数据参数设置完成后调用commit()方法一次性执行所有暂存的SQL提交事务6. 事务提交完成释放数据库连接批量操作结束。关键对比循环单条操作是“1条数据 1次连接 1次预编译 1次事务提交”MyBatis Batch是“N条数据 1次连接 1次预编译 1次事务提交”性能差距一目了然。3. 适用场景MyBatis Batch 不是万能的适合以下场景• 数据量较大≥100条比如Excel导入、数据迁移、日志批量归档• 插入/更新的SQL结构一致只是参数不同比如批量插入相同表、批量更新相同字段• 对性能要求高需要快速完成批量操作避免超时。不适用场景数据量极小100条此时批量优化的收益小于开销直接单条操作更高效每条数据的SQL结构不同比如不同字段更新无法复用预编译SQL。三、SpringBoot MyBatis Batch 完整实现下面讲解两种企业级常用的MyBatis Batch实现方式注解版简洁适合简单场景、XML版灵活适合复杂场景两种方式都支持批量插入和批量更新配置简单代码可直接复制复用。前置准备SpringBoot 2.7.x MyBatis 3.5.x MySQL 8.0核心依赖无需额外新增只需保留原有MyBatis和MySQL依赖即可。1. 基础准备实体类 Mapper接口以“用户表user”为例准备实体类和基础Mapper接口两种实现方式共用。1实体类 User.javaimport lombok.Data; import java.time.LocalDateTime; /** * 用户实体类对应user表 */ Data public class User { // 主键ID private Long id; // 用户名 private String username; // 密码实际项目需加密 private String password; // 部门ID private Long deptId; // 创建时间 private LocalDateTime createTime; // 更新时间 private LocalDateTime updateTime; }2Mapper接口 UserMapper.java定义批量插入、批量更新的方法两种实现方式的接口定义一致只是实现方式不同。import org.apache.ibatis.annotations.Mapper; import java.util.List; Mapper public interface UserMapper { // 批量插入注解版/XML版共用接口 int batchInsert(ListUser userList); // 批量更新注解版/XML版共用接口 int batchUpdate(ListUser userList); }2. 方式1注解版InsertProvider / UpdateProvider适合简单的批量插入/更新场景无需编写XML文件直接在Mapper接口用注解实现开发速度快。1批量插入使用InsertProvider注解指定SQL提供类动态生成批量插入SQL复用预编译语句。import org.apache.ibatis.annotations.InsertProvider; import org.apache.ibatis.annotations.Mapper; import java.util.List; Mapper public interface UserMapper { // 批量插入注解版 InsertProvider(type UserSqlProvider.class, method batchInsertSql) int batchInsert(ListUser userList); // SQL提供类内部类也可单独抽成独立类 class UserSqlProvider { // 动态生成批量插入SQL public String batchInsertSql(ListUser userList) { // 拼接SQLMyBatis会自动预编译复用参数占位符 StringBuilder sql new StringBuilder(); sql.append(insert into user (username, password, dept_id, create_time, update_time) values ); // 拼接占位符注意MyBatis批量插入占位符用#{item.xxx} for (int i 0; i userList.size(); i) { sql.append((#{item.username}, #{item.password}, #{item.deptId}, #{item.createTime}, #{item.updateTime})); if (i ! userList.size() - 1) { sql.append(,); } } return sql.toString(); } } // 批量更新注解版 UpdateProvider(type UserSqlProvider.class, method batchUpdateSql) int batchUpdate(ListUser userList); }2批量更新批量更新需要注意MySQL支持“case when”语法实现一条SQL批量更新多条数据避免拼接多条update语句。// 继续在UserMapper的UserSqlProvider中添加批量更新方法 public String batchUpdateSql(ListUser userList) { StringBuilder sql new StringBuilder(); sql.append(update user set ); // 批量更新的字段根据实际需求调整 sql.append(username case id ); sql.append(password case id ); sql.append(dept_id case id ); sql.append(update_time case id ); // 拼接case when 条件匹配每条数据的id和对应字段值 for (User user : userList) { sql.append(when #{item.id} then #{item.username} ); sql.append(when #{item.id} then #{item.password} ); sql.append(when #{item.id} then #{item.deptId} ); sql.append(when #{item.id} then #{item.updateTime} ); } // 拼接where条件指定需要更新的id避免更新全表 sql.append(end where id in (); for (int i 0; i userList.size(); i) { sql.append(#{item.id}); if (i ! userList.size() - 1) { sql.append(,); } } sql.append()); return sql.toString(); }3. 方式2XML版适合复杂的批量操作比如多表关联批量插入、批量更新时带复杂条件SQL编写更灵活便于维护也是企业开发中最常用的方式。1批量插入XML版创建UserMapper.xml文件使用MyBatis的标签循环拼接参数实现批量插入自动复用预编译SQL。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.xxx.mapper.UserMapper !-- 批量插入XML版 -- insert idbatchInsert parameterTypejava.util.List insert into user (username, password, dept_id, create_time, update_time) values foreach collectionlist itemitem separator, (#{item.username}, #{item.password}, #{item.deptId}, #{item.createTime}, #{item.updateTime}) /foreach /insert /mapper2批量更新XML版同样使用标签结合case when语法实现批量更新支持复杂条件比如添加where过滤。!-- 批量更新XML版 -- update idbatchUpdate parameterTypejava.util.List update user set username case id foreach collectionlist itemitem when #{item.id} then #{item.username} /foreach end, password case id foreach collectionlist itemitem when #{item.id} then #{item.password} /foreach end, dept_id case id foreach collectionlist itemitem when #{item.id} then #{item.deptId} /foreach end, update_time case id foreach collectionlist itemitem when #{item.id} then #{item.updateTime} /foreach end /set where id in foreach collectionlist itemitem open( separator, close) #{item.id} /foreach /update4. Service层实现批量操作必须开启Spring事务否则无法实现“一次性提交”性能会大打折扣同时可设置批量批次避免单次批量数据过多导致内存溢出。import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; Service public class UserService { Resource private UserMapper userMapper; /** * 批量插入核心方法 * 1. 开启事务Transactional确保一次性提交 * 2. 分批处理避免单次批量数据过多比如1万条分10批每批1000条 */ Transactional(rollbackFor Exception.class) public void batchInsertUser(ListUser userList) { // 批次大小根据数据库性能调整一般1000-5000条/批 int batchSize 1000; int totalSize userList.size(); // 分批插入 for (int i 0; i totalSize; i batchSize) { // 计算当前批次的结束索引 int endIndex Math.min(i batchSize, totalSize); ListUser batchList userList.subList(i, endIndex); // 填充公共字段可选比如创建时间、更新时间 batchList.forEach(user - { user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); }); // 调用批量插入方法 userMapper.batchInsert(batchList); } } /** * 批量更新核心方法 * 同样开启事务分批处理 */ Transactional(rollbackFor Exception.class) public void batchUpdateUser(ListUser userList) { int batchSize 1000; int totalSize userList.size(); for (int i 0; i totalSize; i batchSize) { int endIndex Math.min(i batchSize, totalSize); ListUser batchList userList.subList(i, endIndex); // 填充更新时间可选 batchList.forEach(user - user.setUpdateTime(LocalDateTime.now())); userMapper.batchUpdate(batchList); } } }5. 核心配置优化仅实现代码还不够需要在application.yml中配置MyBatis批量相关参数进一步提升性能避免出现隐藏问题。spring: # 数据库配置基础配置根据自己的数据库信息调整 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneGMT%2B8rewriteBatchedStatementstrue username: root password: 123456 # 连接池配置批量操作需足够的连接数 hikari: maximum-pool-size: 20 # 最大连接数根据服务器性能调整 minimum-idle: 5 # 最小空闲连接 connection-timeout: 30000 # 连接超时时间30秒 # MyBatis 配置批量优化核心 mybatis: mapper-locations: classpath:mapper/*.xml # Mapper XML路径XML版需配置 type-aliases-package: com.xxx.entity # 实体类包路径 configuration: map-underscore-to-camel-case: true # 开启下划线转驼峰 default-executor-type: BATCH # 核心设置默认执行器为批量执行器 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL调试用生产可关闭✅ 关键配置说明•rewriteBatchedStatementstrueMySQL驱动参数开启批量语句重写MyBatis Batch依赖该参数才能真正实现批量提交否则会被拆分为单条执行•default-executor-type: BATCHMyBatis默认执行器是SIMPLE单条执行设置为BATCH全局启用批量执行器复用预编译SQL•maximum-pool-size: 20连接池最大连接数批量操作需要足够的连接避免连接不足导致阻塞但也不要设置过大避免占用过多服务器资源。四、百万级数据批量操作不卡顿当批量数据达到10万条、100万条以上时仅靠基础的MyBatis Batch还不够需要结合以下4个进阶优化技巧避免内存溢出、超时实现高效批量操作。优化1分批处理控制批次大小一次性将100万条数据加载到内存会导致JVM内存溢出OOM必须分批处理批次大小建议设置为1000-5000条/批根据服务器内存和数据库性能调整。核心逻辑将大数据量拆分为多个小批次逐批插入/更新每批执行完成后释放当前批次的内存避免内存堆积。优化2关闭MyBatis一级缓存MyBatis一级缓存本地缓存会缓存查询结果批量操作中一级缓存会不断堆积数据占用内存甚至导致内存溢出建议关闭一级缓存。配置方式application.ymlmybatis: configuration: local-cache-scope: STATEMENT # 关闭一级缓存默认SESSION开启缓存优化3使用MyBatis BatchExecutor手动控制提交对于超大量数据比如100万条可手动获取MyBatis的BatchExecutor控制提交频率避免单次提交数据过多导致数据库压力过大。import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; Service public class UserBatchService { Resource private SqlSessionFactory sqlSessionFactory; // 手动控制批量提交百万级数据优化 public void batchInsertLargeData(ListUser userList) { // 开启批量执行器手动控制事务 try (SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH, false)) { UserMapper userMapper sqlSession.getMapper(UserMapper.class); int batchSize 1000; int count 0; for (User user : userList) { userMapper.insert(user); count; // 每1000条提交一次释放资源 if (count % batchSize 0) { sqlSession.commit(); sqlSession.clearCache(); // 清除缓存避免内存溢出 } } // 提交剩余数据 sqlSession.commit(); } } }优化4数据库层面优化批量操作的性能瓶颈最终在数据库需配合以下数据库优化进一步提升速度• 关闭数据库自动提交MySQL默认自动提交批量操作需关闭由程序手动提交• 优化表结构批量插入的表尽量减少索引索引会增加插入开销批量操作完成后再重建索引• 调整MySQL参数比如增大max_allowed_packet允许的最大数据包大小避免SQL过长报错• 使用数据库批量导入工具可选比如MySQL的LOAD DATA INFILE适合超大量数据千万级性能比MyBatis Batch更高但灵活性稍差。五、注意事项很多人实现MyBatis Batch后发现性能没有提升甚至比循环单条操作更慢大多是踩了以下坑一定要避开未开启事务批量变单条❌ 错误Service层未加Transactional注解MyBatis Batch无法实现一次性提交每条数据还是单独提交性能和循环单条一样✅ 正确所有批量操作方法必须添加Transactional(rollbackFor Exception.class)确保事务批量提交。未配置rewriteBatchedStatementstrue❌ 错误MySQL连接URL未添加该参数MyBatis Batch会将批量SQL拆分为单条SQL执行无法实现真正的批量✅ 正确必须在数据库URL中添加rewriteBatchedStatementstrue这是MyBatis Batch生效的关键。批次大小设置不合理❌ 错误批次设置过大比如10万条/批导致内存溢出设置过小比如10条/批频繁提交性能下降✅ 正确批次大小建议1000-5000条根据服务器内存、数据库性能调整优先测试不同批次的性能选择最优值。批量更新未加where条件更新全表❌ 错误批量更新时忘记添加where id in (...)条件导致全表数据被更新造成严重数据事故✅ 正确批量更新必须添加where条件明确指定需要更新的记录避免误更全表。忽略内存溢出问题❌ 错误将百万级数据一次性加载到List中导致JVM内存溢出✅ 正确分批读取数据比如从Excel、文件中分批读取再分批插入避免一次性加载大量数据到内存。六、文末小结其实MyBatis Batch 入门不难核心就是抓住“复用连接、复用预编译SQL、批量提交”三个关键点再配合正确的配置和分批处理就能轻松解决批量操作卡顿、超时问题。重点先掌握基础的XML版批量插入/更新配置好核心参数开启事务再逐步学习进阶优化技巧实际项目中根据数据量大小选择合适的方案无需过度优化比如小数据量直接单条操作更高效。收藏本文无论是日常开发中的批量场景还是面试突击都能随时查阅轻松拿捏MyBatis Batch批量优化技巧搞定亿级数据批量操作不卡顿

更多文章