MyBatis-Plus 批量操作与 rewriteBatchedStatements 优化
目录
① 导读卡片
② 背景与目标
为什么学?
学完能怎样?
③ 核心概念与原理
3.1 saveBatch 的两种来源
3.2 默认行为:循环单条 INSERT
3.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 IService<InspectionDetail> {} inspectionDetailService.saveBatch(details); // 方式二:ISqlRunner ISqlRunner runner = SqlRunnerFactory.getRunner(detailMapper); runner.saveBatch(details);两者底层行为完全一致。
3.2 默认行为:循环单条 INSERT
saveBatch的默认实现(简化源码):
public boolean saveBatch(Collection<T> 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 真正的"一条多值 INSERT"
INSERT INTO table (col1, col2) VALUES (v1, v2), (v3, v4), (v5, v6);一次网络往返完成所有数据插入。对比上面默认行为:
| 方式 | 7 条数据的网络往返 | MySQL 层面 |
|---|---|---|
saveBatch默认 | 7 次 | 7 条单行 INSERT |
| 真正的多值 INSERT | 1 次 | 1 条多值 INSERT |
④ 逻辑图谱与对比
4.1 四种批量插入方案对比
批量插入方案 ├─ for + insert() → ❌ 7 次网络往返,最差 ├─ saveBatch() 默认 → 7 次"批量提交"仍为单行 INSERT ├─ saveBatch() + rewriteBatchedStatements → ✅ 1 条多值 INSERT,最推荐 └─ 手写 foreach XML → ✅ 1 条多值 INSERT,最灵活
| 方式 | MySQL 层面 | 网络往返 | 代码量 | 推荐程度 |
|---|---|---|---|---|
for + insert() | 7 条 INSERT | 7 次 | 少 | ❌ 最差 |
saveBatch()默认 | 7 条 INSERT(同事务提交) | 7 次 | 极少 | ⚠️ 一般 |
saveBatch() + rewriteBatchedStatements | 1 条多值 INSERT | 1 次 | 极少(仅加个参数) | ✅首选 |
手写foreach XML | 1 条多值 INSERT | 1 次 | 多(需写 XML) | ✅ 灵活场景 |
⑤ 核心详解
5.1 方案一:开启 rewriteBatchedStatements(推荐)
原理:MySQL JDBC 驱动提供的参数,开启后会自动将 JDBC batch 提交的多个单行 INSERT 合并为一条多值 INSERT。
配置方式:
spring: datasource: druid: url: jdbc:mysql://localhost:3306/mes_db?rewriteBatchedStatements=true 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 次 INSERT,3 个值 INSERT INTO inspection_detail (...) VALUES (?), (?), (?);使用代码:
@Service @RequiredArgsConstructor public class InspectionSubmitService { private final InspectionDetailService detailService; public void submitInspection() { List<InspectionDetail> details = dto.getDetails(); // 7 条 detailService.saveBatch(details, 100); // batchSize=100 // 开启 rewriteBatchedStatements 后自动合并为一条多值 INSERT } }5.2 方案二:手写 XML 批量 INSERT(灵活场景)
当需要自定义 SQL 逻辑(如ON DUPLICATE KEY UPDATE、INSERT IGNORE)时,手动操作更可控:
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO inspection_detail (task_id, item_code, number_value, enum_value, photo_url) VALUES <foreach collection="list" item="item" separator=","> (#{item.taskId}, #{item.itemCode}, #{item.numberValue}, #{item.enumValue}, #{item.photoUrl}) </foreach> </insert> @Mapper public interface InspectionDetailMapper { void batchInsert(@Param("list") List<InspectionDetail> 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?rewriteBatchedStatements=true 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() + rewriteBatchedStatements | 500ms | <1% | 1 次 |
⑦ 常见坑与最佳实践
7.1 易错点清单
| # | 坑点 | 现象 | 原因 | ✅ 避坑方案 |
|---|---|---|---|---|
| 1 | rewriteBatchedStatements只对 INSERT 有效 | UPDATE 没合并 | 这是 MySQL JDBC 驱动的设计 | 只关心的 INSERT 场景即可 |
| 2 | 一次批量数据量过大 | 报错max_allowed_packet限制 | 单条 SQL 太长超出 MySQL 限制 | 调小 batchSize 或调大max_allowed_packet |
| 3 | 开了参数但没效果 | 抓日志仍看到多行 INSERT | 使用的不是 JDBCexecuteBatch() | saveBatch 底层恰好就是 JDBC batch |
| 4 | 不加参数直接用 saveBatch | 以为一条 SQL,实际多条 | 对 saveBatch 的误解 | 开 rewriteBatchedStatements 或手写 XML |
7.2 面试话术
Q:你们的批量插入怎么做的?
用 MyBatis-Plus 的
saveBatch,并在 JDBC URL 上加了rewriteBatchedStatements=true。这样 saveBatch 底层会把多次单行 INSERT 合并为一条多值 INSERT,7 条数据的网络往返从 7 次降到 1 次。这个参数是 MySQL JDBC 驱动提供的,只需要加个连接参数,不用改任何代码。
Q:为什么不直接手写 XML 的 foreach 批量 INSERT?
因为 saveBatch + rewriteBatchedStatements 已经能达到同样的效果,代码量更少、维护更方便。如果以后需要自定义 SQL(比如
ON DUPLICATE KEY UPDATE),再考虑手写 XML。目前简单批量插入场景,加个参数就够了。
Q:rewriteBatchedStatements 有什么副作用?
基本没有。它只影响 INSERT 语句的拼装方式,不影响其他操作。唯一要注意的是:如果一次插入几万条数据,可能超过 MySQL 的
max_allowed_packet限制,需要调大这个参数。但我们业务场景一次最多 7 条,完全没问题。
Q:saveBatch 的 batchSize 设多少合适?
设 100 比较合理。如果一次插入数据超过 100 条,MyBatis-Plus 会分批执行,每 100 条 flush 一次。我们一次最多 7 条,100 绰绰有余。如果场景是几千上万的批量导入,可以适当调大 batchSize。
7.3 最佳实践
✅首选方案:
saveBatch+rewriteBatchedStatements=true,零代码改动,效果好✅手写 XML:当需要自定义逻辑(
ON DUPLICATE KEY UPDATE、INSERT IGNORE、多表关联插入)时使用✅控制批量大小:单条 SQL 建议控制在 1000 个以内(不超过 1MB),避免
max_allowed_packet限制❌避免 for 循环 insert:除非只有 1~2 条数据,否则这是最差的写法
⑧ 总结与学习路线图
核心要点
| 维度 | 要点 | 一句话记住 |
|---|---|---|
| 默认行为 | saveBatch 是循环单行 INSERT | 不是多值 INSERT,是"批量提交" |
| 优化方案 | rewriteBatchedStatements=true | 加一个参数,效果天差地别 |
| 备选方案 | 手写 foreach XML | 灵活但有代码量 |
| 常见坑 | 误以为 saveBatch 就是多值 INSERT | 先搞清默认行为再优化 |
自检清单
- 我能说清楚 saveBatch 默认是循环单行 INSERT 还是多值 INSERT
- 我知道 rewriteBatchedStatements 是干什么的、怎么配
- 我能对比四种批量插入方案的性能差异
- 我知道什么场景用手写 XML 更合适
下一步学习
阶段一(基础):saveBatch 使用 + rewriteBatchedStatements 配置 → 完成本文 阶段二(进阶):MySQL 执行计划分析、批量插入的 InnoDB 锁机制 阶段三(源码):MyBatis-Plus 批量执行器 SqlBatch 源码、MySQL JDBC Statement.executeBatch() 源码