@TOCmybatis-plus批量插入数据优化
sql
datasource:
url: jdbc:mysql://localhost:13306/rs-db?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&&useServerPrepStmts=false&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
username: root
password: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
首先,请明确一点:在 MySQL + Spring Boot 的生产环境中,每秒插入 1 万条数据,这个速度绝对不慢,甚至属于"非常优秀"的水平。
对于单表单库的常规机械硬盘(HDD)或普通云盘,1万 TPS(事务/秒)已经接近写入瓶颈上限;即便是高性能 SSD,这个速度也属于正常范围。你感觉"慢",可能是心理预期过高(比如对标内存操作或 NoSQL),或是实际批次量远小于 1 万条。
为了帮你精准定位"能不能更快"以及"慢在哪里",我们先看下面这 4 个大概率存在的隐形瓶颈:
1. 最关键疑点:Kafka 拉取量可能不够(最常见的"伪慢")
你的代码是基于 Kafka 批量消费的,但 records.size() 真的等于 1万 吗?
Kafka 消费者默认 max.poll.records 为 500 。也就是说,你这批实际可能只插入了 500条 。
如果 500条 耗时 1秒(即 500 TPS),那确实非常慢;如果是 1万条 耗时 1秒(即 1万 TPS),那性能是正常的。
- 验证方法 :在
saveBatch前加一行日志:log.info("当前批次实际数量:{}", list.size()); - 优化 :若数量只有几百,调整 Kafka 消费参数:
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 5000);(建议结合内存和拉取超时时间调大)。
2. 事务(@Transactional)带来的提交开销
检查你的 @KafkaListener 方法或 saveTestService 实现类上是否带有 @Transactional 注解。
- 原因 :如果开启事务,
saveBatch在执行完flush后,Spring 并不会立即提交事务,而是等待方法结束才commit。而commit操作会触发磁盘fsync(刷盘)。如果默认每次只提交几百条,频繁刷盘会是极大开销。 - 优化 :如果业务允许部分失败(或无需强事务),去掉 该方法上的
@Transactional,让saveBatch内部自动提交;或者改为编程式事务,并手动控制批次提交频率。
3. 数据库表索引过多(写入放大)
插入慢最容易被忽略的原因是表的索引数量。
- 原因 :
SaveTest表如果有 4-5 个普通索引(或唯一索引),插入 1条 数据实际上要写 1 份数据 + 更新 N 份索引树。1万条数据意味着要更新几万个索引页,磁盘 IO 压力极大。 - 优化 :检查建表语句。如果该表仅用于日志归档,建议只保留主键索引,删除所有非必要二级索引,写入速度能提升 2-3 倍。
4. JDBC 连接参数配置不全(缺少辅助参数)
仅仅开启 rewriteBatchedStatements=true 已经解决了 SQL 重写问题,但还有两个参数能进一步压榨性能:
-
建议添加 :在数据库连接 URL 末尾追加:
&useServerPrepStmts=false&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048 -
解释:虽然开启了批量重写,但每次都编译 SQL 仍有解析开销。开启预编译缓存可以复用执行计划,减少 CPU 耗时(特别是在高并发下效果明显)。
如果确实只有 1万条/秒,如何硬性突破?(极致优化)
如果确认 list.size() 确实有 1万条,且你对 1秒不满意(需求是毫秒级),可以考虑:
- 调整
saveBatch的分片大小 :MyBatis-Plus 默认分片是 1000。即 1万条会拆成 10 次网络请求发送。- 修改为:
saveTestService.saveBatch(list, 5000);减少网络往返次数(但也需要注意单条 SQL 长度不能超过max_allowed_packet,建议 5000 左右)。
- 修改为:
- 使用异步落库 :如果消费速度过快,可以使用
@Async将插入任务丢给独立线程池,Kafka 消费线程直接返回,避免消费阻塞。
总结诊断步骤
请按顺序做这三件事,就能定位你的"慢"属于哪一种:
- 打印日志 :确认
list.size()到底是 500 还是 10000。 - 核对表结构 :数一下
save_test表除了主键外有几个索引。 - 检查类注解 :看 Service 或 Listener 上是否有
@Transactional。
如果 list.size() 只有几百,而耗时 1秒,那就是 Kafka 拉取量太小导致提交太频繁;调大 max.poll.records 即可显著改善。如果确实是 1万条 耗时 1秒,那这说明你的代码和数据库配置已经调优得相当不错了,无需过度焦虑。
如果查完后仍然觉得慢,可以告诉我 list.size() 的实际数量,我帮你进一步推算理论最优耗时。
在此基础上,如果还想进一步压榨性能,可以从下面几个方向入手。
🎯 首要优化:调整批次大小 (batchSize)
你当前每次拉取4000条数据,但 MyBatis-Plus 的 saveBatch 默认批次大小是 1000 。这意味着你的4000条数据会被拆成4批发送,产生了4次网络往返。
-
优化方法 :手动指定
batchSize为 4000,减少网络交互次数。java// 一次发送4000条,充分利用单次网络包 saveTestService.saveBatch(list, 4000); -
注意 :需要确保拼接后的SQL大小不超过MySQL的
max_allowed_packet限制(通常为4M或16M)。如果单条message字段很大,需适当调低该值(如2000)。
🧩 关键辅助优化
在调整批次大小的同时,可以检查以下三点,它们对性能有显著影响:
-
审视表索引(写入放大效应)
- 现状分析 :每插入一条数据,不仅要写数据,还要更新所有索引。如果
save_test表上存在多个二级索引(特别是唯一索引),写入开销会非常大。 - 优化建议 :如果该表主要用于日志存储,强烈建议只保留主键索引,删除所有非必要的二级索引,这能极大提升写入速度。
- 现状分析 :每插入一条数据,不仅要写数据,还要更新所有索引。如果
-
检查事务边界(长事务与锁)
- 现状分析 :检查
@KafkaListener方法或saveTestService实现类是否被@Transactional注解标记。如果有,整个批次插入会在一个长事务中完成,这会增加锁的持有时间和数据库负担。 - 优化建议 :如果业务允许,去掉方法上的
@Transactional,让saveBatch利用自动提交模式,或者使用编程式事务来精确控制边界。
- 现状分析 :检查
-
补充JDBC参数(降低CPU开销)
-
你已配置了
rewriteBatchedStatements=true,还可以添加以下参数来进一步优化:&useServerPrepStmts=false&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
-
这些参数可以开启客户端预编译缓存,复用SQL执行计划,减少数据库端的CPU开销。
-
🚀 进阶方案:使用 InsertBatchSomeColumn
这是 MyBatis-Plus 官方提供的真正的批量插入 方法,它直接生成一条多值的 INSERT INTO ... VALUES (...), (...) SQL,性能比 saveBatch 更优。
实现步骤:
-
创建SQL注入器 :定义一个类,继承
DefaultSqlInjector,将InsertBatchSomeColumn方法注入。javapublic class BatchSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methodList = super.getMethodList(mapperClass); // 添加 InsertBatchSomeColumn 方法,指定字段填充策略 methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); return methodList; } } -
配置MyBatis-Plus :在配置类中,将自定义的注入器设置为全局配置。
java@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusSqlInjector mybatisPlusSqlInjector() { return new BatchSqlInjector(); } } -
使用新方法 :在你的 Mapper 接口中添加方法,并直接调用。
java// 在 SaveTestMapper 中 int insertBatchSomeColumn(List<SaveTest> list); // 在 Service 中调用 saveTestMapper.insertBatchSomeColumn(list);
💎 总结与性能预期
综合来看,一个比较理想的优化路径是:
- 立竿见影 :将
saveBatch(list)改为saveBatch(list, 4000)。 - 检查并精简 :审查并删除不必要的表索引,移除多余的
@Transactional。 - 终极方案 :如果对性能有极致要求,实现
InsertBatchSomeColumn方法。
完成上述优化后,在普通云数据库上,4000条数据的插入耗时预计可以稳定在 200ms ~ 500ms 之间 。如果仍然接近1秒,那瓶颈很可能就在磁盘I/O 或表索引上了,需要从数据库硬件或表结构设计层面进行更深入的排查。