大家好,我是 lucy。最近在维护一个基于 Spring Boot 3 + MyBatis-Plus 的业务系统时,遇到了一个典型的批量插入耗时从 200ms 飙升到 40s的生产故障。排查过程让我对 MySQL 的默认配置有了更深的理解,记录下来供大家参考。
📋 问题背景
系统有个定时任务,每天凌晨会从上游系统同步一批福利兑换订单,单次批量插入记录数约 5000~20000 条。业务初期运行正常,某次版本迭代后,这个定时任务的执行时间从原来的 200ms 左右突然飙升到 30s~45s,严重影响下游系统的数据同步时效。
🔍 排查过程
第一步:定位耗时环节
通过在插入逻辑前后打日志,初步确认耗时发生在 DAO 层批量插入阶段,而非上游数据拉取或业务处理环节。
@Override
public void saveBatch(List list) {
long start = System.currentTimeMillis();
baseMapper.insertBatchSomeColumn(list);
log.info("批量插入 {} 条记录,耗时 {} ms", list.size(), System.currentTimeMillis() - start);
}
日志显示,插入 10000 条数据耗时超过 40s,而数据量较小时(如 1000 条)则耗时正常。这说明耗时与数据量不是线性关系,存在某个临界点触发质变。
第二步:检查 SQL 执行计划
在测试环境模拟批量插入,并开启 MySQL 慢查询日志:
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(临时)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0;
-- 执行批量插入后查看日志
SHOW GLOBAL STATUS LIKE 'Slow_queries';
慢查询日志中捕获到的 SQL 语句本身并不慢,真正的问题出在每次插入前后的隐式操作。
第三步:检查表结构与索引
SHOW CREATE TABLE order_t\G
发现表中有一个 UNIQUE KEY 索引,字段为 order_no(订单号)。业务逻辑在插入前会先查询是否存在该订单号,但排查代码后发现插入操作本身并没有先查再插。
深入查看 Mapper XML,发现如下配置:
<insert id="insertBatchSomeColumn">
INSERT INTO order_t (
id, order_no, user_id, amount, status, create_time, update_time
) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.id}, #{item.orderNo}, #{item.userId},
#{item.amount}, #{item.status}, #{item.createTime}, #{item.updateTime})
</foreach>
ON DUPLICATE KEY UPDATE update_time = VALUES(update_time)
</insert>
这里使用了 ON DUPLICATE KEY UPDATE,在存在唯一索引冲突时执行更新,而不是直接报错。问题就藏在这里——当批量插入中大量记录存在冲突时,每次冲突都会触发一次隐藏的读+写,即先查后更新,相当于一次插入变成了两次操作。
第四步:确认根因——上游数据重复
查看上游接口变更记录发现:某次上游系统接口调整后,同一批订单数据会被重复推送多次(接口未做幂等控制)。这就导致批量插入时,5000 条数据中有大量 order_no 是重复的,每次遇到重复 key,MySQL 都要先查后更新,耗时急剧增加。
🛠️ 解决方案
方案一:上游接口幂等改造(根本解决)
这是最彻底的方案,要求上游系统在推送数据时增加幂等 key,或者在数据层面做去重。本次推动上游团队在接口层增加了 dedup_key 参数,从源头解决问题。
方案二:批量插入前本地去重(兜底防护)
在应用层做兜底,去重后再插入:
@Override
public void saveBatchSafe(List<OrderEntity> list) {
// 利用 LinkedHashSet 按插入顺序去重
Set<String> seen = new LinkedHashSet<>();
List<OrderEntity> distinctList = list.stream()
.filter(e -> seen.add(e.getOrderNo()))
.toList();
log.info("去重:原始 {} 条,去重后 {} 条", list.size(), distinctList.size());
baseMapper.insertBatchSomeColumn(distinctList);
}
方案三:分批次插入 + 批量大小调优
调整 MyBatis-Plus 的批量插入分页大小,避免单次 SQL 过长:
# application.yml
mybatis-plus:
properties:
# 每次批量插入的最大条数
max-batch-insert-size: 500
将原来一次性插入 10000 条改为每批 500 条,分 20 批插入。结合去重逻辑后,整体耗时从 40s 稳定降到 800ms 以内。
📊 优化效果
| 阶段 | 数据量 | 耗时 |
|---|---|---|
| 优化前(含冲突数据) | 10000 条 | 40s+ |
| 加去重逻辑 | 10000 条(去重后 3000 条) | 3s |
| 分批插入(每批 500) | 3000 条 | 600ms |
💡 经验总结
- 批量插入性能问题,不要只看 SQL 本身,要关注数据质量——重复数据会触发唯一索引冲突,代价远高于正常插入。
- 上游接口变更要同步评估影响,特别是幂等性、数据去重这类问题,晚了就变成生产故障。
- 批量操作一定要设置合理的分批大小,过大的单次 SQL 在 MySQL 端会产生临时表和内存压力。
- 定时任务的监控告警不能少——本次问题从发生到被发现隔了 3 天,如果接入了执行耗时告警,可以第一时间发现。
⚠️ 声明:本文相关内容仅供参考
以上内容基于真实项目经历整理,具体根因和解决方案需结合实际情况分析。如有疑问欢迎留言交流。
📡 来源:个人技术博客,2026-04-19,来自于 tech-article-publisher