MyBatis-Plus 批量操作与 rewriteBatchedStatements 优化 目录① 导读卡片② 背景与目标为什么学学完能怎样③ 核心概念与原理3.1 saveBatch 的两种来源3.2 默认行为循环单条 INSERT3.3 真正的一条多值 INSERT④ 逻辑图谱与对比4.1 四种批量插入方案对比⑤ 核心详解5.1 方案一开启 rewriteBatchedStatements推荐5.2 方案二手写 XML 批量 INSERT灵活场景5.3 方案三for 单条 insert反面教材⑥ 典型应用案例6.1 案例巡检 7 条明细的批量插入⑦ 常见坑与最佳实践7.1 易错点清单7.2 面试话术7.3 最佳实践⑧ 总结与学习路线图核心要点自检清单下一步学习① 导读卡片一句话读懂MyBatis-Plus 的saveBatch默认不是一条多值 INSERT而是循环单条 INSERT开启rewriteBatchedStatements后才能合并为一条 SQL大幅降低网络往返 适合人群Java 后端开发者、MyBatis-Plus 使用者、性能优化关注者 难度等级★★★☆☆中等 ⏱阅读时长约 10 分钟 前置知识Spring Boot MyBatis-Plus 基础、JDBC 基本概念② 背景与目标为什么学大多数人用saveBatch时以为它是这样执行的INSERT INTO table (col1, col2) VALUES (v1, v2), (v3, v4), (v5, v6);但实际上默认是这样执行的INSERT INTO table (col1, col2) VALUES (v1, v2); INSERT INTO table (col1, col2) VALUES (v3, v4); INSERT INTO table (col1, col2) VALUES (v5, v6);7 条数据就发 7 次网络往返。这个误解可能导致线上性能瓶颈。学完能怎样理解saveBatch底层的真正执行方式掌握三种批量 INSERT 方案及其性能差异知道rewriteBatchedStatements的作用和用法面试时能讲清楚批量插入怎么优化的③ 核心概念与原理3.1 saveBatch 的两种来源// 方式一IService 自带 public interface InspectionDetailService extends IServiceInspectionDetail {} inspectionDetailService.saveBatch(details); ​ // 方式二ISqlRunner ISqlRunner runner SqlRunnerFactory.getRunner(detailMapper); runner.saveBatch(details);两者底层行为完全一致。3.2 默认行为循环单条 INSERTsaveBatch的默认实现简化源码public boolean saveBatch(CollectionT entityList, int batchSize) { int i 0; String sql INSERT INTO table (...) VALUES (?); // 单条 INSERT 模板 for (T entity : entityList) { executeBatch(sql, entity); // 每条数据单独发一次 i; if (i % batchSize 0) { flushStatements(); // 每 batchSize 条提交一次 } } return true; }默认batchSize 1000虽然叫批量提交但 MySQL 层面每批仍然是多条单行 INSERT只是将多次commit合并为一次3.3 真正的一条多值 INSERTINSERT INTO table (col1, col2) VALUES (v1, v2), (v3, v4), (v5, v6);一次网络往返完成所有数据插入。对比上面默认行为方式7 条数据的网络往返MySQL 层面saveBatch默认7 次7 条单行 INSERT真正的多值 INSERT1 次1 条多值 INSERT④ 逻辑图谱与对比4.1 四种批量插入方案对比批量插入方案 ├─ for insert() → ❌ 7 次网络往返最差 ├─ saveBatch() 默认 → 7 次批量提交仍为单行 INSERT ├─ saveBatch() rewriteBatchedStatements → ✅ 1 条多值 INSERT最推荐 └─ 手写 foreach XML → ✅ 1 条多值 INSERT最灵活方式MySQL 层面网络往返代码量推荐程度for insert()7 条 INSERT7 次少❌ 最差saveBatch()默认7 条 INSERT同事务提交7 次极少⚠️ 一般saveBatch() rewriteBatchedStatements1 条多值 INSERT1 次极少仅加个参数✅首选手写foreach XML1 条多值 INSERT1 次多需写 XML✅ 灵活场景⑤ 核心详解5.1 方案一开启 rewriteBatchedStatements推荐原理MySQL JDBC 驱动提供的参数开启后会自动将 JDBC batch 提交的多个单行 INSERT 合并为一条多值 INSERT。配置方式spring: datasource: druid: url: jdbc:mysql://localhost:3306/mes_db?rewriteBatchedStatementstrue driver-class-name: com.mysql.cj.jdbc.Driver效果验证开启前后对比-- 开启前3 次 INSERT INSERT INTO inspection_detail (...) VALUES (?); INSERT INTO inspection_detail (...) VALUES (?); INSERT INTO inspection_detail (...) VALUES (?); ​ -- 开启后1 次 INSERT3 个值 INSERT INTO inspection_detail (...) VALUES (?), (?), (?);使用代码Service RequiredArgsConstructor public class InspectionSubmitService { private final InspectionDetailService detailService; public void submitInspection() { ListInspectionDetail details dto.getDetails(); // 7 条 detailService.saveBatch(details, 100); // batchSize100 // 开启 rewriteBatchedStatements 后自动合并为一条多值 INSERT } }5.2 方案二手写 XML 批量 INSERT灵活场景当需要自定义 SQL 逻辑如ON DUPLICATE KEY UPDATE、INSERT IGNORE时手动操作更可控insert idbatchInsert parameterTypejava.util.List INSERT INTO inspection_detail (task_id, item_code, number_value, enum_value, photo_url) VALUES foreach collectionlist itemitem separator, (#{item.taskId}, #{item.itemCode}, #{item.numberValue}, #{item.enumValue}, #{item.photoUrl}) /foreach /insert Mapper public interface InspectionDetailMapper { void batchInsert(Param(list) ListInspectionDetail details); } // 调用 inspectionDetailMapper.batchInsert(details); // 一条 SQL 完成5.3 方案三for 单条 insert反面教材// ❌ 不要这样做 for (InspectionDetail detail : details) { detailMapper.insert(detail); }每条数据一次网络往返、一次事务提交。7 条数据 7 次数据库交互。⑥ 典型应用案例6.1 案例巡检 7 条明细的批量插入 需求描述一次巡检提交需插入 7 条巡检明细。要求性能最优且容易维护。 推荐写法# application.yml — 仅需加一个参数 spring: datasource: druid: url: jdbc:mysql://localhost:3306/mes_db?rewriteBatchedStatementstrue driver-class-name: com.mysql.cj.jdbc.Driver Service RequiredArgsConstructor public class InspectionSubmitService { private final InspectionDetailService detailService; Transactional(rollbackFor Exception.class) public void submitInspection(InspectionSubmitDTO dto) { // ... 校验逻辑 ... // saveBatch rewriteBatchedStatements 一条多值 INSERT detailService.saveBatch(dto.toInspectionDetails(), 100); // 更新任务状态 // ... } } 性能对比300 并发压测方案平均响应超时率网络往返for insert()3 秒40%7 次saveBatch()默认2.5 秒30%7 次同事务提交saveBatch() rewriteBatchedStatements500ms1%1 次⑦ 常见坑与最佳实践7.1 易错点清单#坑点现象原因✅ 避坑方案1rewriteBatchedStatements只对 INSERT 有效UPDATE 没合并这是 MySQL JDBC 驱动的设计只关心的 INSERT 场景即可2一次批量数据量过大报错max_allowed_packet限制单条 SQL 太长超出 MySQL 限制调小 batchSize 或调大max_allowed_packet3开了参数但没效果抓日志仍看到多行 INSERT使用的不是 JDBCexecuteBatch()saveBatch 底层恰好就是 JDBC batch4不加参数直接用 saveBatch以为一条 SQL实际多条对 saveBatch 的误解开 rewriteBatchedStatements 或手写 XML7.2 面试话术Q你们的批量插入怎么做的用 MyBatis-Plus 的saveBatch并在 JDBC URL 上加了rewriteBatchedStatementstrue。这样 saveBatch 底层会把多次单行 INSERT 合并为一条多值 INSERT7 条数据的网络往返从 7 次降到 1 次。这个参数是 MySQL JDBC 驱动提供的只需要加个连接参数不用改任何代码。Q为什么不直接手写 XML 的 foreach 批量 INSERT因为 saveBatch rewriteBatchedStatements 已经能达到同样的效果代码量更少、维护更方便。如果以后需要自定义 SQL比如ON DUPLICATE KEY UPDATE再考虑手写 XML。目前简单批量插入场景加个参数就够了。QrewriteBatchedStatements 有什么副作用基本没有。它只影响 INSERT 语句的拼装方式不影响其他操作。唯一要注意的是如果一次插入几万条数据可能超过 MySQL 的max_allowed_packet限制需要调大这个参数。但我们业务场景一次最多 7 条完全没问题。QsaveBatch 的 batchSize 设多少合适设 100 比较合理。如果一次插入数据超过 100 条MyBatis-Plus 会分批执行每 100 条 flush 一次。我们一次最多 7 条100 绰绰有余。如果场景是几千上万的批量导入可以适当调大 batchSize。7.3 最佳实践✅首选方案saveBatchrewriteBatchedStatementstrue零代码改动效果好✅手写 XML当需要自定义逻辑ON DUPLICATE KEY UPDATE、INSERT IGNORE、多表关联插入时使用✅控制批量大小单条 SQL 建议控制在 1000 个以内不超过 1MB避免max_allowed_packet限制❌避免 for 循环 insert除非只有 1~2 条数据否则这是最差的写法⑧ 总结与学习路线图核心要点维度要点一句话记住默认行为saveBatch 是循环单行 INSERT不是多值 INSERT是批量提交优化方案rewriteBatchedStatementstrue加一个参数效果天差地别备选方案手写 foreach XML灵活但有代码量常见坑误以为 saveBatch 就是多值 INSERT先搞清默认行为再优化自检清单我能说清楚 saveBatch 默认是循环单行 INSERT 还是多值 INSERT我知道 rewriteBatchedStatements 是干什么的、怎么配我能对比四种批量插入方案的性能差异我知道什么场景用手写 XML 更合适下一步学习阶段一基础saveBatch 使用 rewriteBatchedStatements 配置 → 完成本文 阶段二进阶MySQL 执行计划分析、批量插入的 InnoDB 锁机制 阶段三源码MyBatis-Plus 批量执行器 SqlBatch 源码、MySQL JDBC Statement.executeBatch() 源码