🚀 MyBatis 批量操作性能优化:从龟速到极速!

107 阅读6分钟

"为什么我的 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
BatchExecutor1.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秒1000010000
foreach (10000条)10秒11
foreach (1000条/批)3秒1010
BatchExecutor (1000条/批)1.5秒1010
BatchExecutor + rewriteBatchedStatements0.8秒 ⚡⚡110

💡 面试加分回答模板

面试官:"如何优化 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. 最后调数据库参数(最快)

不要一开始就过度优化,根据数据量选择方案!🎯

祝你的批量操作快如闪电! ⚡🚀


📚 扩展阅读