全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

批量操作性能优化

MyBatis 批量操作有 BATCH 执行器和 foreach 拼接两种主流方案,各自适用不同场景。本文从执行效率、内存消耗、功能限制等维度进行对比分析,给出不同数据量下的最优方案选择。

BATCH 执行器 vs foreach 方式对比

维度ExecutorType.BATCHforeach 拼接 SQL
实现原理JDBC addBatch() + executeBatch()XML 循环拼接多值 INSERT 语句
SQL 条数1 条 PreparedStatement,多次传参1 条大 SQL,含多个 VALUES 子句
网络往返1 次1 次
数据库编译编译 1 次编译 1 次
获取自增主键不支持(MySQL)支持
动态 SQL不支持(SQL 已预编译)支持(运行时拼接)
单次数据量上限受内存限制,建议分批max_allowed_packet 限制
内存消耗低(不堆积 SQL 字符串)高(需拼接完整 SQL 字符串)
代码复杂度低(Mapper 写法不变)中(需编写 foreach XML)

ExecutorType.BATCH 方案

标准批量插入

Java
public void batchInsert(List<User> users) {
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (User user : users) {
            mapper.insert(user);
        }
        session.commit();
    } finally {
        session.close();
    }
}

Mapper XML 与普通单条插入完全相同:

XML
<insert id="insert" parameterType="User">
    INSERT INTO user (username, email, create_time)
    VALUES (#{username}, #{email}, #{createTime})
</insert>

分批提交控制

数据量较大时,避免 BATCH 队列在内存中堆积,需设置合理的分批大小:

Java
public void batchInsertLarge(List<User> users) {
    int batchSize = 500;
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (int i = 0; i < users.size(); i++) {
            mapper.insert(users.get(i));
            if ((i + 1) % batchSize == 0) {
                session.commit();
                session.clearCache();
            }
        }
        session.commit();
    } finally {
        session.close();
    }
}

注意:BATCH 模式下每次 commit() 后必须调用 clearCache() 清理一级缓存,否则缓存对象持续占用内存,数据量大时可能触发 OOM。

批量更新

Java
public void batchUpdate(List<User> users) {
    int batchSize = 300;
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (int i = 0; i < users.size(); i++) {
            mapper.update(users.get(i));
            if ((i + 1) % batchSize == 0) {
                session.commit();
                session.clearCache();
            }
        }
        session.commit();
    } finally {
        session.close();
    }
}

foreach 批量插入方案

多值 INSERT 拼接

XML
<insert id="batchInsertForeach" parameterType="list">
    INSERT INTO user (username, email, create_time)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.createTime})
    </foreach>
</insert>

Java 调用侧保持常规 SqlSession:

Java
public void batchInsertWithForeach(List<User> users) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        mapper.batchInsertForeach(users);
        session.commit();
    } finally {
        session.close();
    }
}

分批处理(突破 max_allowed_packet 限制)

MySQL 的 max_allowed_packet 默认 4MB,单条 SQL 过大时会报 Packet for query is too large

Java
public void batchInsertInChunks(List<User> users) {
    int chunkSize = 200; // 每 200 条拼接为一条 SQL
    SqlSession session = sqlSessionFactory.openSession();
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (int i = 0; i < users.size(); i += chunkSize) {
            int end = Math.min(i + chunkSize, users.size());
            List<User> chunk = users.subList(i, end);
            mapper.batchInsertForeach(chunk);
        }
        session.commit();
    } finally {
        session.close();
    }
}

批量大小选择策略

数据量推荐方案batchSize/chunkSize说明
< 100foreach 或 BATCH 均可-差异不明显
100 ~ 1,000BATCH 执行器500内存低,代码简洁
1,000 ~ 10,000BATCH 执行器500分批提交,避免 OOM
10,000 ~ 100,000BATCH 执行器1,000增大批次减少 commit 次数
> 100,000BATCH + 多线程2,000 ~ 5,000配合线程池并发写入

注意:batchSize 并非越大越好。过大会导致单次 commit 锁表时间过长,影响并发读写;过小则 commit 频繁,性能下降。建议 500 ~ 1,000 为起点,结合实际压测调整。

事务边界控制

正确做法:整个批量操作在单一事务中

Java
// 正确:一个事务覆盖整个批次
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    UserMapper mapper = session.getMapper(UserMapper.class);
    for (User user : users) {
        mapper.insert(user);
    }
    session.commit(); // 全部成功才提交
} catch (Exception e) {
    session.rollback(); // 任一条失败则回滚全部
} finally {
    session.close();
}

错误做法:每条数据独立提交

Java
// 错误:逐条 commit,破坏原子性
for (User user : users) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
        mapper.insert(user);
        session.commit(); // 每条独立事务
    } finally {
        session.close();
    }
}

逐条提交的问题:

  • 无法保证整体一致性,中途失败导致部分写入
  • 每条事务的 WAL 刷盘开销累加,总耗时远高于单次大事务
  • 无法利用批量预写日志的合并写特性

半事务模式:部分成功可接受

某些场景允许部分失败(如日志采集、数据同步),可记录失败项后继续:

Java
public BatchResult batchInsertWithFailures(List<User> users) {
    int batchSize = 500;
    SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
    List<Integer> failedIndexes = new ArrayList<>();
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (int i = 0; i < users.size(); i++) {
            try {
                mapper.insert(users.get(i));
                if ((i + 1) % batchSize == 0) {
                    session.commit();
                    session.clearCache();
                }
            } catch (Exception e) {
                failedIndexes.add(i);
                session.rollback();
                session.clearCache();
            }
        }
        session.commit();
    } finally {
        session.close();
    }
    return new BatchResult(users.size(), failedIndexes);
}

注意:此模式下需要在每次异常后 rollback 当前批次,确保未提交数据不会残留。

性能压测对比

以 MySQL 8.0 插入 10,000 条记录为例:

方案耗时内存峰值说明
逐条 SIMPLE 插入~8.5s基准对照
foreach 拼接(10,000 条)报错超出 max_allowed_packet
foreach 分 chunk(200条/chunk)~1.2s50 次 SQL 提交
BATCH(500/批)~0.8s20 次 commit
BATCH(1,000/批)~0.6s10 次 commit

注意:压测数据基于本地单实例 MySQL,实际性能受网络延迟、磁盘 IO、并发连接数等影响,应以实际环境压测为准。

Spring 事务整合

使用 Spring @Transactional 时,需要注意 BATCH 模式的兼容性:

Java
@Service
public class UserService {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void batchInsertWithSpring(List<User> users) {
        // Spring 管理的 SqlSession 需手动切换 BATCH
        SqlSession session = new SqlSessionTemplate(
            sqlSessionTemplate.getSqlSessionFactory(),
            ExecutorType.BATCH
        );
        try {
            UserMapper mapper = session.getMapper(UserMapper.class);
            int batchSize = 500;
            for (int i = 0; i < users.size(); i++) {
                mapper.insert(users.get(i));
                if ((i + 1) % batchSize == 0) {
                    session.commit();
                    session.clearCache();
                }
            }
            session.commit();
        } finally {
            session.close();
        }
    }
}
Java
@Configuration
public class MyBatisConfig {

    @Bean("batchSqlSessionTemplate")
    public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory, ExecutorType.BATCH);
    }
}

要点总结

  • BATCH 执行器内存占用低、代码简洁,是批量写入首选方案
  • foreach 拼接适合需要获取自增主键或使用动态 SQL 的场景
  • 批量大小建议 500 ~ 1,000 起步,结合压测数据微调
  • 整个批量操作应保持在单一事务中,确保原子性
  • BATCH 模式 commit 后必须 clearCache(),避免一级缓存内存泄漏
  • foreach 方案需注意 MySQL max_allowed_packet 限制,合理分 chunk
  • 大数据量可结合多线程 + BATCH 执行器实现并发写入

📝 发现内容有误?点击此处直接编辑

← 上一篇 执行计划分析
下一篇 → 缓存策略优化
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库