✅ 一、核心概念
概念 | 说明 |
---|---|
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 中追加参数 |
✅ 四、注意事项
- 事务控制:批量方法必须加
@Transactional
,异常时整体回滚。 - 主键策略:批量插入使用
IdType.ASSIGN_ID
(雪花算法)或数据库自增,避免重复。 - 分批大小:
- 普通场景:1000 条
- 大字段/宽表:500 条
- 数据库限制:
- MySQL 单条 SQL 默认最大 16M,多值 insert 不宜超过 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
并控制批次大小,性能即可起飞。