批量更新 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=falseuseServerPrepStmts=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 里那两个参数,这是所有批量操作的前提。
热门跟贴