"为什么我的 MyBatis 批量插入1万条数据要跑10分钟?" 😱
📖 什么是批量操作?
批量操作:一次性处理多条数据的操作(INSERT、UPDATE、DELETE)。
单条操作:
for (int i = 0; i < 10000; i++) {
insert(data[i]); // 10000次SQL
}
批量操作:
insertBatch(data); // 1次SQL
性能差异:100倍以上!⚡
🎯 MyBatis 批量操作的方式
MyBatis 提供的批量操作方式:
1️⃣ foreach 标签(最常用)
2️⃣ BatchExecutor(JDBC Batch)
3️⃣ MyBatis-Plus saveBatch
4️⃣ 原生 JDBC Batch
性能对比:
BatchExecutor > JDBC Batch > foreach > 单条插入
🔥 优化技巧一:foreach 批量插入
基础用法
<!-- Mapper XML -->
<insert id="insertBatch">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
// Service
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void batchInsert(List<User> users) {
userMapper.insertBatch(users);
}
}
生成的 SQL:
INSERT INTO user (name, age, email)
VALUES
('张三', 25, 'zhang@example.com'),
('李四', 30, 'li@example.com'),
('王五', 28, 'wang@example.com'),
...
性能问题:SQL 太长
问题:
一次插入 10000 条数据
生成的 SQL 长度 = 10000 × 100字节 ≈ 1MB
MySQL 默认 max_allowed_packet = 4MB
如果超过限制 → 报错!
而且:
- SQL 太长,解析慢
- 网络传输慢
- 事务太大,锁等待时间长
优化:分批插入
// ✅ 分批插入(推荐)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void batchInsert(List<User> users) {
int batchSize = 1000; // 每批1000条
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
userMapper.insertBatch(batch); // 分批插入
}
}
}
// 性能对比:
// 不分批(10000条):10秒
// 分批(1000条/批):3秒(快3倍)⚡
🔥 优化技巧二:BatchExecutor(最快)
什么是 BatchExecutor?
MyBatis 的三种 Executor:
1. SimpleExecutor(默认)
→ 每次执行一条 SQL
2. ReuseExecutor
→ 复用 PreparedStatement
3. BatchExecutor
→ 使用 JDBC Batch,批量执行
→ 最快!⚡⚡
开启 BatchExecutor
// 方式1:配置文件开启
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 设置 ExecutorType
Configuration configuration = new Configuration();
configuration.setDefaultExecutorType(ExecutorType.BATCH);
factory.setConfiguration(configuration);
return factory.getObject();
}
// 方式2:手动创建 BatchExecutor
@Service
public class UserService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchInsert(List<User> users) {
// 创建 Batch Session
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = session.getMapper(UserMapper.class);
int batchSize = 1000;
for (int i = 0; i < users.size(); i++) {
mapper.insert(users.get(i)); // 不会立即执行
if (i % batchSize == 0 || i == users.size() - 1) {
session.flushStatements(); // 批量执行
session.commit(); // 提交事务
session.clearCache(); // 清理缓存
}
}
}
}
}
Mapper 接口:
public interface UserMapper {
void insert(User user); // 普通的单条插入方法
}
<!-- Mapper XML -->
<insert id="insert">
INSERT INTO user (name, age, email)
VALUES (#{name}, #{age}, #{email})
</insert>
BatchExecutor 原理
内部流程:
1. insert(user1) → 加入批次,不执行
2. insert(user2) → 加入批次,不执行
3. insert(user3) → 加入批次,不执行
...
999. insert(user999) → 加入批次,不执行
1000. flushStatements() → 一次性执行所有 SQL
底层:
PreparedStatement pst = conn.prepareStatement(sql);
for (User user : users) {
pst.setString(1, user.getName());
pst.setInt(2, user.getAge());
pst.setString(3, user.getEmail());
pst.addBatch(); // 添加到批次
}
pst.executeBatch(); // 批量执行
性能对比
| 方式 | 10000条耗时 | 提升倍数 |
|---|---|---|
| 单条插入 | 60秒 | 1x |
| foreach (10000条) | 10秒 | 6x |
| foreach (1000条/批) | 3秒 | 20x |
| BatchExecutor | 1.5秒 | 40x ⚡⚡ |
🔥 优化技巧三:MyBatis-Plus saveBatch
使用方式
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
// MyBatis-Plus 提供的批量保存
public void batchInsert(List<User> users) {
this.saveBatch(users, 1000); // 每批1000条
}
}
底层实现:使用 BatchExecutor
🔥 优化技巧四:批量更新
foreach 批量更新
<!-- ❌ 不好的方式(逐条更新) -->
<update id="updateBatch">
<foreach collection="list" item="user" separator=";">
UPDATE user
SET name = #{user.name}, age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>
<!-- MySQL 默认不支持多条 SQL,需要配置:
jdbc:mysql://localhost:3306/db?allowMultiQueries=true
-->
<!-- ✅ 好的方式(CASE WHEN) -->
<update id="updateBatch">
UPDATE user
SET
name = CASE id
<foreach collection="list" item="user">
WHEN #{user.id} THEN #{user.name}
</foreach>
END,
age = CASE id
<foreach collection="list" item="user">
WHEN #{user.id} THEN #{user.age}
</foreach>
END
WHERE id IN
<foreach collection="list" item="user" open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
生成的 SQL:
UPDATE user
SET
name = CASE id
WHEN 1 THEN '张三'
WHEN 2 THEN '李四'
WHEN 3 THEN '王五'
END,
age = CASE id
WHEN 1 THEN 25
WHEN 2 THEN 30
WHEN 3 THEN 28
END
WHERE id IN (1, 2, 3)
BatchExecutor 批量更新
public void batchUpdate(List<User> users) {
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = session.getMapper(UserMapper.class);
int batchSize = 1000;
for (int i = 0; i < users.size(); i++) {
mapper.updateById(users.get(i));
if (i % batchSize == 0 || i == users.size() - 1) {
session.flushStatements();
session.commit();
}
}
}
}
🔥 优化技巧五:事务控制
问题:频繁提交事务
// ❌ 每次都提交事务(慢!)
for (User user : users) {
userMapper.insert(user);
// 每次插入后自动提交事务
}
// 10000次插入 = 10000次事务提交
// 事务提交很慢(涉及磁盘IO)
优化:批量提交
// ✅ 批量提交事务
@Transactional
public void batchInsert(List<User> users) {
for (User user : users) {
userMapper.insert(user);
}
// 方法结束时统一提交事务
}
// 10000次插入 = 1次事务提交
// 性能提升:50倍!⚡
更优:分批提交
// ✅✅ 分批提交(推荐)
public void batchInsert(List<User> users) {
int batchSize = 1000;
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
// 每批使用一个事务
insertBatchInTransaction(batch);
}
}
@Transactional
private void insertBatchInTransaction(List<User> users) {
userMapper.insertBatch(users);
}
// 优势:
// - 避免事务太大(锁等待时间长)
// - 单批失败不影响其他批
// - 可以记录失败的批次,重试
🔥 优化技巧六:禁用二级缓存
问题:批量操作 + 缓存 = 内存溢出
批量插入 10000 条数据:
1. 每条数据都会缓存
2. 10000 条 × 2KB = 20MB
3. 如果插入 100 万条 = 2GB 内存
4. OutOfMemoryError!💥
解决方案
<!-- Mapper XML -->
<insert id="insertBatch" useCache="false" flushCache="true">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
或者:
// 手动清理缓存
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.insert(users.get(i));
if (i % 1000 == 0) {
session.flushStatements();
session.commit();
session.clearCache(); // 清理缓存
}
}
}
🔥 优化技巧七:数据库连接池
配置优化
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 连接数够用
connection-timeout: 3000
# 批量操作专用配置
data-source-properties:
rewriteBatchedStatements: true # 重写批处理语句(重要!)
cachePrepStmts: true # 缓存 PreparedStatement
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
rewriteBatchedStatements 的作用:
-- 不开启:
INSERT INTO user VALUES (1, '张三', 25);
INSERT INTO user VALUES (2, '李四', 30);
INSERT INTO user VALUES (3, '王五', 28);
-- 3次网络往返
-- 开启后:
INSERT INTO user VALUES (1, '张三', 25), (2, '李四', 30), (3, '王五', 28);
-- 1次网络往返
性能提升:3倍!⚡
📊 完整性能对比
场景:插入 10000 条数据
| 方式 | 耗时 | SQL数量 | 事务数量 |
|---|---|---|---|
| 单条插入 | 60秒 | 10000 | 10000 |
| foreach (10000条) | 10秒 | 1 | 1 |
| foreach (1000条/批) | 3秒 | 10 | 10 |
| BatchExecutor (1000条/批) | 1.5秒 | 10 | 10 |
| BatchExecutor + rewriteBatchedStatements | 0.8秒 ⚡⚡ | 1 | 10 |
💡 面试加分回答模板
面试官:"如何优化 MyBatis 的批量操作性能?"
标准回答:
"我会从以下几个方面优化:
1. 使用 BatchExecutor(最重要):
- 底层使用 JDBC Batch
- 性能比 foreach 快 2-3 倍
- 需要手动创建 Batch SqlSession
2. 分批处理:
- 每批 1000-5000 条
- 避免 SQL 太长、事务太大
- 便于失败重试
3. 事务控制:
- 分批提交事务
- 避免频繁提交(慢)
- 也避免事务太大(锁等待)
4. 数据库配置:
- 开启 rewriteBatchedStatements(重要!)
- 性能可以再提升 2-3 倍
- 缓存 PreparedStatement
5. 清理缓存:
- 批量操作时禁用缓存
- 定期调用 clearCache()
- 避免内存溢出
实际案例: 我们的用户导入功能,原来插入 10万条数据要 10 分钟。通过使用 BatchExecutor + 分批处理 + 开启 rewriteBatchedStatements,优化到 8 秒,性能提升了 75 倍。"
🎉 总结
MyBatis 批量操作优化的核心:
1. 使用 BatchExecutor
→ JDBC Batch,最快
2. 分批处理
→ 每批 1000-5000 条
3. 批量提交事务
→ 减少事务提交次数
4. 开启 rewriteBatchedStatements
→ MySQL JDBC 参数,重要!
5. 清理缓存
→ 避免内存溢出
记住这个公式:
批量性能 =
(BatchExecutor) ×
(分批大小合理) ×
(事务控制得当) ×
(数据库参数优化)
最后一句话:
批量操作的优化顺序:
1. 先用 foreach(简单)
2. 再用 BatchExecutor(更快)
3. 最后调数据库参数(最快)
不要一开始就过度优化,根据数据量选择方案!🎯
祝你的批量操作快如闪电! ⚡🚀
📚 扩展阅读