在业务系统开发中,批量操作是一个高频且关键的技术场景。
- 批量导入用户
- 批量更新订单状态
- 批量删除无效数据
- 批量上下架商品
然而,现实项目中超过80%的批量操作实现都存在性能或稳定性问题:
- 在
for循环中执行单条insert
- 一次提交十万级数据导致数据库连接池耗尽或事务过大
- 将批量更新写成N次独立的
update语句
- 使用
DELETE ... WHERE id IN (...)处理大量数据导致SQL过长或锁表
- 操作过大的
List直接引发内存溢出(OOM)
本质上,批量操作的核心挑战并非SQL语法,而是工程化设计与性能调优能力。本文将系统性地阐述MyBatis-Plus框架下批量插入、更新、删除的标准化写法,并提供可落地的性能优化方案。
1
批量操作常见问题分析
1. 数据量过大,单次操作压力过高
一次性执行上万条的insert、update或delete语句,极易导致数据库连接卡顿、事务日志膨胀,甚至触发死锁。
2. 在循环中执行SQL语句
for (...) {
mapper.insert(item);
}
这是最典型的反模式:性能极差、无法利用数据库批处理能力、可能引发严重的锁竞争。
3. 不了解框架与驱动的批处理机制
许多开发者误以为MyBatis-Plus的saveBatch、updateBatchById等方法能处理所有场景,却忽视了其内部实现原理以及更底层的JDBC Batch模式对性能的决定性影响。
2
批量插入:三层级标准方案
批量插入是最易引发性能问题的场景。核心原则是:必须结合分批处理、JDBC批处理模式与合理的事务控制。
以下提供三种从通用到高性能的解决方案。
方案一:MyBatis-Plus 原生 saveBatch()(通用推荐)
对于大多数后台管理系统,MyBatis-Plus提供的批插入方法已足够使用:
boolean result = userService.saveBatch(list, 500);
其中,参数500代表批处理大小(batchSize)。
执行流程:
- 框架将列表按
batchSize拆分为多个子集。
- 每个子集执行一次
INSERT语句(利用foreach生成多值SQL或JDBC批处理,取决于配置)。
- 性能可满足日常业务需求。
关键提示:切勿一次性提交数万条数据,将batchSize设置在500~1000是经验证的最佳区间。
适用场景:
- 数据量在5万条以下。
- 后台管理系统的数据导入功能。
- 常规的CRUD业务。
局限性:
- 海量数据(如50万以上)迁移或导入。
- 对性能有极致要求的高频写入任务。
- 需要基于唯一键进行去重插入的场景。
方案二:Mapper XML + MyBatis foreach(SQL可控)
当需要更精细地控制生成的SQL时,可在XML映射文件中直接编写:
<insert id="batchInsert">
INSERT INTO user (username, phone)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.phone})
</foreach>
</insert>
优点:
- SQL语句完全可控,适合复杂字段映射。
- 将多条数据合并为一条
INSERT ... VALUES (...), (...), ...语句,执行效率高于循环单插。
- 性能通常优于
saveBatch()的默认行为。
缺点:
- 当列表过大时,拼接的SQL语句会非常长,可能超出数据库或驱动限制。
- 需要手动实现分批逻辑(可按
foreach的collection大小进行切分)。
方案三:JDBC Batch(极致性能)
面对数十万乃至百万级的数据写入,应直接使用JdbcTemplate或原生JDBC的批处理功能,这是数据库/中间件性能调优的重要手段。
jdbcTemplate.batchUpdate(
"INSERT INTO user (username, phone) VALUES (?, ?)",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = list.get(i);
ps.setString(1, user.getUsername());
ps.setString(2, user.getPhone());
}
public int getBatchSize() {
return list.size();
}
});
原理:JDBC批处理模式只需向数据库发送一次SQL模板,后续仅传输参数数据,极大减少了网络IO与数据库SQL解析开销。
适用场景:
- 历史数据迁移。
- 从文件(Excel/CSV)导入海量数据。
- 定时生成的报表数据入库。
3
批量更新
批量更新比插入更容易被误用,典型的低效写法是:
for (User u : list) {
updateById(u);
}
方案一:MyBatis-Plus updateBatchById()
对于按ID更新多条记录,且更新字段相同的简单场景:
userService.updateBatchById(list, 500);
适用:批量启用/禁用、批量修改状态等。
不适用:每条记录更新的字段不同、WHERE条件复杂、大表高频并发更新。
方案二:批量更新相同字段(高效推荐)
例如“批量上架商品”,只更新status字段:
<update id="batchUpdateStatus">
UPDATE product
SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
优势:一条SQL完成所有更新,网络交互少,数据库/中间件执行效率高。
场景:批量审批、批量改价(统一价格)、批量标记已读等。
方案三:批量更新不同字段(高级用法)
当每条记录需要更新的字段值都不同时,可以采用foreach生成多条UPDATE语句:
<update id="batchUpdatePrice">
<foreach collection="list" item="item" separator=";">
UPDATE product
SET price = #{item.price}
WHERE id = #{item.id}
</foreach>
</update>
关键配置:必须开启数据库连接参数,允许执行多条SQL(以MySQL为例):
jdbc:mysql://...?allowMultiQueries=true
适用:导入Excel后,每条记录有独立的新价格需要更新。
注意:此方式生成的SQL较长,不适合一次更新上万条记录。
4
批量删除
直接使用超长的IN语句进行删除是危险操作:
DELETE FROM user WHERE id IN (1,2,3,...);
风险:IN列表超过1000项某些数据库会报错;SQL超长;删除量过大直接锁表。
推荐方案一:分批删除
使用工具类将ID列表分片后,循环调用MyBatis-Plus的删除方法。
Lists.partition(ids, 500).forEach(batch -> {
userMapper.deleteBatchIds(batch);
});
此方案稳健,易于理解和维护。
推荐方案二:软删除(企业级通用)
绝大多数业务系统采用软删除设计,通过is_deleted字段标记数据状态。
UPDATE user SET is_deleted = 1 WHERE id IN (...);
优点:
- 避免硬删除导致的表锁和性能波动。
- 数据可恢复,满足审计要求。
- 对业务连续性影响最小。
推荐方案三:按范围或分页删除(处理海量数据)
对于日志、流水等大数据表,直接按条件范围删除。
-- 按ID范围删除
DELETE FROM log WHERE id BETWEEN 1 AND 50000;
-- 按时间分页删除
DELETE FROM log WHERE create_time < '2022-01-01' LIMIT 10000;
这是清理历史数据的标准做法,能有效控制单次操作的事务大小和锁范围。
5
大批量操作通用模板
一个健壮的批量操作应遵循以下模板,适用于插入、更新、删除及导入场景:
- 参数校验:检查输入列表是否为空。
- 数据分批:使用固定大小(如500)对原始列表进行分片。
- 遍历执行:对每个分片的数据执行具体的批处理SQL。
- 事务控制:确保整个批量操作在一个合理的事务内完成,避免单条失败导致部分提交。
- 异常处理:捕获批处理异常,记录失败数据,支持重试或人工干预。
此模板可直接作为团队开发规范。
6
批量操作性能优化核心建议
- 强制分批:始终设置合理的
batchSize(500~1000),这是线上大量实践得出的黄金值。
- 禁用自动提交:在批处理执行期间,务必确保处于手动事务控制下,待所有批次成功后再统一提交。
- 避免大范围锁:严禁在无
WHERE条件或条件过泛的情况下执行更新/删除,如UPDATE table SET status=1。
- 大表操作使用
LIMIT:对于海量数据删除,采用DELETE ... WHERE ... LIMIT n分多次执行,避免长事务和锁表。
- 慎用
DELETE IN:严格控制IN子句内的元素数量,优先采用分批或范围删除方案。
总结
批量操作的本质是工程化问题,它综合考验开发者对数据库性能、SQL特性、事务机制、框架批处理原理及内存管理的理解。
本文系统介绍了针对MyBatis-Plus和Java技术栈的批量操作标准化方案,并提供了从通用到高阶、从插入到删除的完整实践指南与性能调优建议。遵循此体系,无论是处理数据导入、状态同步还是历史清理,都能使你的系统在稳定性、效率与可维护性上达到企业级要求。