✅ 一、核心概念

概念 说明
BatchExecutor MyBatis 内置执行器:一次网络交互发送多条 SQL,减少网络往返,提高吞吐量。
saveBatch / updateBatchById / removeByIds MyBatis-Plus 基于 BatchExecutor 封装的高阶 API,支持分批提交,屏蔽底层细节。
rewriteBatchedStatements MySQL 驱动参数:将多条 insert 重写为一条多值 insert,性能提升 3~10 倍。

✅ 二、操作步骤(3 种主流方式)

✅ 方式1:MyBatis-Plus 原生 API(最简最快)

步骤1:引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.6</version>
</dependency>

步骤2:配置数据源

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demo?rewriteBatchedStatements=true&allowMultiQueries=true
    username: root
    password: root

步骤3:编写 Service 层

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {

    @Transactional(rollbackFor = Exception.class)
    public void batchImport(List<User> list) {
        // 每 1000 条提交一次
        saveBatch(list, 1000);
    }
}

✅ 方式2:手动开启 BatchExecutor(灵活可控)

步骤1:注入 SqlSessionFactory

@Autowired
private SqlSessionFactory sqlSessionFactory;

步骤2:编程式批量插入

public void manualBatchInsert(List<User> users) {
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        int i = 0;
        for (User u : users) {
            mapper.insert(u);
            if (++i % 1000 == 0) {     // 每 1000 条 flush 一次
                session.flushStatements();
            }
        }
        session.commit();            // 手动提交
    } finally {
        session.close();
    }
}

✅ 方式3:XML foreach 批量(小数据量/动态列)

<insert id="batchInsertByForeach">
    INSERT INTO user(name, age) VALUES
    <foreach collection="list" item="u" separator=",">
        (#{u.name}, #{u.age})
    </foreach>
</insert>

✅ 三、常见错误与解决方案

错误 原因 解决
主键冲突 重复数据或主键未设置 使用 ON DUPLICATE KEY UPDATE 或数据库唯一索引
OutOfMemoryError 批次过大 每批 ≤ 1000 条,或分页读取源数据
批量更新未生效 忘记 session.commit() 手动模式需显式提交
性能未提升 未加 rewriteBatchedStatements=true 在 JDBC URL 中追加参数

✅ 四、注意事项

  1. 事务控制:批量方法必须加 @Transactional,异常时整体回滚。
  2. 主键策略:批量插入使用 IdType.ASSIGN_ID(雪花算法)或数据库自增,避免重复。
  3. 分批大小
    • 普通场景:1000 条
    • 大字段/宽表:500 条
  4. 数据库限制
    • MySQL 单条 SQL 默认最大 16M,多值 insert 不宜超过 5 万条。
  5. 不能与 Spring 声明式事务混用 手动 SqlSession 模式时,必须自己控制 commit/rollback

✅ 五、使用技巧

场景 技巧
忽略空字段 insertBatchSomeColumn + FieldFill.IGNORE
批量更新非主键 updateBatchById 只支持主键匹配,自定义 SQL:
<update id="batchUpdateAge">
  <foreach collection="list" item="u" separator=";">
    UPDATE user SET age = #{u.age} WHERE name = #{u.name}
  </foreach>
</update>
``` |
| 监控耗时 | 开启性能分析插件 `PerformanceInterceptor` |

---

### ✅ 六、最佳实践与性能优化

| 维度 | 实践 |
|------|------|
| **JDBC 参数** | `rewriteBatchedStatements=true&allowMultiQueries=true&useSSL=false` |
| **分批策略** | 动态调整:小表 1000,大字段 500,超大表按主键分页 |
| **并行处理** | 利用 `CompletableFuture` 多线程分批 + 独立事务 |
| **内存控制** | 使用 `ResultHandler` 流式读取源数据,避免一次加载全部 |
| **监控** | 开启 MyBatis-Plus SQL 日志 + AOP 统计耗时 |

---

### ✅ 七、完整示例:百万级数据导入

```java
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping("/import")
    public String importUsers(@RequestParam("file") MultipartFile file) {
        try (InputStream in = file.getInputStream()) {
            // 1. 分批读取 Excel
            EasyExcel.read(in, UserExcel.class, new PageReadListener<UserExcel>(list -> {
                List<User> users = list.stream()
                        .map(e -> new User(e.getName(), e.getAge()))
                        .collect(Collectors.toList());
                // 2. 批量插入
                userService.saveBatch(users, 1000);
            })).sheet().doRead();
            return "success";
        } catch (IOException e) {
            throw new RuntimeException("导入失败", e);
        }
    }
}

✅ 八、一句话总结

日常开发优先用 saveBatch,超大数据量或复杂场景用 ExecutorType.BATCH,记得开启 rewriteBatchedStatements=true 并控制批次大小,性能即可起飞。