批量更新 2000 条数据,updateBatchById 跑了 18 秒,接口直接超时。看了一眼日志,发现底层居然是一条一条发 UPDATE 语句。

MyBatis-Plus 的 updateBatchById 名字里带 "Batch",很多开发者就以为它走的是 JDBC 批量操作。实际上默认实现是逐条执行,跟 for 循环没区别。

问题出在 MyBatis-Plus 的批量方法默认走 sqlSession 的逐条模式,需要手动开启 rewriteBatchedstatements 和正确使用 SQLSession 批量提交才能真正提速。下面拆解两种正确写法。

一、默认行为的坑1. updateBatchById 的真相

看源码会发现,updateBatchById 内部调用的是 sqlSession.update(statement, entity),循环逐条执行:

// MyBatis-Plus 默认实现(简化)public boolean updateBatchById(Collection list) {for (T entity : list) {sqlsession.update(sqlStatement, entity); // 逐条执行}

2000 条数据就是 2000 次网络往返 + 2000 次事务提交,慢是必然的。

2. 实测数据对比

以 MySQL 8.0 + 本地环境测试 2000 条更新:

方式

耗时

SQL执行次数

updateBatchById(默认)

18.3s

2000次

手动批量(未开启rewrite)

4.2s

2000次(合并网络)

手动批量 + rewrite

0.8s

1次(合并SQL)

差距超过 20 倍,关键就在两个开关。

3. 两个必要配置

⚠️ 注意:JDBC URL 必须加 rewriteBatchedStatements=true,否则 JDBC 驱动不会把多条 SQL 合并成一条发送。

spring:datasource:url: jdbc:MySQL://localhost:3306/db?rewriteBatchedStatements=true&useServerPrepStmts=false

useServerPrepStmts=false 也很关键,服务端预编译模式下 rewrite 可能失效。

小结:默认 updateBatchById 没有批量能力,开启 rewrite 是第一步。

二、正确批量写法1. 手动 SqlSession 批量

最可控的方式是自己获取 SqlSession 执行批量:

@Servicepublic class UserService {@Autowiredprivate SqlSessionFactory sqlSessionFactory;public void batchUpdate(List list) {try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {UserMapper mapper = session.getMapper(UserMapper.class);for (int i = 0; i < list.size(); i++) {mapper.updateById(list.get(i));if (i % 500 == 0) {session.flushStatements();session.commit();session.flushStatements();session.commit();}

ExecutorType.BATCH 让 MyBatis 把多条同类型 SQL 合并发送,每 500 条 flush 一次避免内存溢出。

2. MyBatis-Plus 的 ServiceImpl

ServiceImpl 提供了 updateBatchById 的重载方法,传入 batchSize:

public class UserServiceImplextends ServiceImplimplements IUserService {@Overridepublic boolean updateBatch(List list) {return updateBatchById(list, 500);// ↑ 第二个参数是 batchSize}

这个方法内部也是用 ExecutorType.BATCH 的 SqlSession,不需要自己管理。

3. XML 的 foreach 方案

另一种思路是直接写批量 SQL,一次请求完成:

UPDATE userSET name = #{item.name}, age = #{item.age}WHERE id = #{item.id}

需要在 URL 加 allowMultiQueries=true,MySQL 才支持多条语句一次执行。

批量更新优先用 SqlSession(ExecutorType.BATCH) 方案,不依赖数据库特性,兼容性最好。

小结:三种批量写法各有适用场景,ExecutorType.BATCH 是最通用的选择。

三、避坑清单1. 事务范围控制

批量操作必须手动控制事务范围:

// 错误:每条自动提交SqlSession session = factory.openSession(ExecutorType.BATCH);// 正确:关闭自动提交SqlSession session = factory.openSession(ExecutorType.BATCH, false);

第二个参数 false 表示关闭自动提交,所有 SQL 在同一个事务里执行。

2. flush 时机

不 flush 会导致内存暴涨。每 500~1000 条 flush 一次是经验值:

if (i % 500 == 0) {session.flushStatements(); // 发送累积的 SQLsession.commit();          // 提交事务session.clearCache();      // 清理一级缓存}

clearCache 容易被忽略,一级缓存满了同样影响性能。

3. 与主键策略的关系

使用 IdType.ASSIGN_ID(雪花算法)批量插入没问题,但批量更新不涉及主键生成,所以主键策略对 updateBatch 无影响。需要注意的反而是乐观锁字段:

⚠️ 注意:如果实体有 @Version 乐观锁字段,批量更新时每次都会检查版本号,不能合并成一条 SQL。

小结:事务、flush 频率、缓存清理是批量操作的铁三角,缺一不可。

小结

MyBatis-Plus 批量更新慢的核心原因就两个:JDBC 没开 rewriteBatchedStatements,以及没用 ExecutorType.BATCH。两个配置加一行代码改动,性能提升 20 倍。

实际开发中,优先用 ServiceImpl 自带的 updateBatchById(list, 500),一行搞定。如果对性能有极致要求,自己管理 SqlSession 并配合 flush 策略。别忘了 URL 里那两个参数,这是所有批量操作的前提。