MYBATIS=PLUS批量插入数据提效

sql 复制代码
datasource:
  url: jdbc:mysql://localhost:13306/zzzz?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&&useServerPrepStmts=false&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048

  username: root
  password: mysql1234
  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秒不满意(需求是毫秒级),可以考虑:

  1. **调整 `saveBatch` 的分片大小**:MyBatis-Plus 默认分片是 1000。即 1万条会拆成 10 次网络请求发送。
  • 修改为:`saveTestService.saveBatch(list, 5000);` 减少网络往返次数(但也需要注意单条 SQL 长度不能超过 `max_allowed_packet`,建议 5000 左右)。
  1. **使用异步落库**:如果消费速度过快,可以使用 `@Async` 将插入任务丢给独立线程池,Kafka 消费线程直接返回,避免消费阻塞。

总结诊断步骤

请按顺序做这三件事,就能定位你的"慢"属于哪一种:

  1. **打印日志**:确认 `list.size()` 到底是 500 还是 10000。

  2. **核对表结构**:数一下 `save_test` 表除了主键外有几个索引。

  3. **检查类注解**:看 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)。

🧩 关键辅助优化

在调整批次大小的同时,可以检查以下三点,它们对性能有显著影响:

  1. **审视表索引(写入放大效应)**

* **现状分析**:每插入一条数据,不仅要写数据,还要更新所有索引。如果 `save_test` 表上存在多个二级索引(特别是唯一索引),写入开销会非常大。

* **优化建议**:如果该表主要用于日志存储,**强烈建议只保留主键索引**,删除所有非必要的二级索引,这能极大提升写入速度。

  1. **检查事务边界(长事务与锁)**

* **现状分析**:检查 `@KafkaListener` 方法或 `saveTestService` 实现类是否被 `@Transactional` 注解标记。如果有,整个批次插入会在一个长事务中完成,这会增加锁的持有时间和数据库负担。

* **优化建议**:如果业务允许,**去掉方法上的 `@Transactional`**,让 `saveBatch` 利用自动提交模式,或者使用编程式事务来精确控制边界。

  1. **补充JDBC参数(降低CPU开销)**

* 你已配置了 `rewriteBatchedStatements=true`,还可以添加以下参数来进一步优化:

```

&useServerPrepStmts=false&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048

```

* 这些参数可以开启客户端预编译缓存,复用SQL执行计划,减少数据库端的CPU开销。

🚀 进阶方案:使用 `InsertBatchSomeColumn`

这是 MyBatis-Plus 官方提供的**真正的批量插入**方法,它直接生成一条多值的 `INSERT INTO ... VALUES (...), (...)` SQL,性能比 `saveBatch` 更优。

**实现步骤**:

  1. **创建SQL注入器**:定义一个类,继承 `DefaultSqlInjector`,将 `InsertBatchSomeColumn` 方法注入。

```java

public 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;

}

}

```

  1. **配置MyBatis-Plus**:在配置类中,将自定义的注入器设置为全局配置。

```java

@Configuration

public class MybatisPlusConfig {

@Bean

public MybatisPlusSqlInjector mybatisPlusSqlInjector() {

return new BatchSqlInjector();

}

}

```

  1. **使用新方法**:在你的 Mapper 接口中添加方法,并直接调用。

```java

// 在 SaveTestMapper 中

int insertBatchSomeColumn(List<SaveTest> list);

// 在 Service 中调用

saveTestMapper.insertBatchSomeColumn(list);

```

💎 总结与性能预期

综合来看,一个比较理想的优化路径是:

  1. **立竿见影**:将 `saveBatch(list)` 改为 `saveBatch(list, 4000)`。

  2. **检查并精简**:审查并删除不必要的表索引,移除多余的 `@Transactional`。

  3. **终极方案**:如果对性能有极致要求,实现 `InsertBatchSomeColumn` 方法。

完成上述优化后,在普通云数据库上,**4000条数据的插入耗时预计可以稳定在 200ms ~ 500ms 之间**。如果仍然接近1秒,那瓶颈很可能就在**磁盘I/O**或**表索引**上了,需要从数据库硬件或表结构设计层面进行更深入的排查。

相关推荐
南城以南溫暖如初14715 小时前
从零搭建24小时自助健身系统:技术选型与核心模块实战
java·spring boot·redis·mysql·vue·mybatis
小江的记录本1 天前
【ORM 框架】MyBatis-Plus 核心特性、条件构造器、分页插件、乐观锁插件(附《思维导图》、《问题排查与实践清单》和《面试高频考点汇总》)
java·windows·spring boot·python·spring·面试·mybatis
小江的记录本1 天前
【ORM框架】MyBatis核心原理、ORM思想、MyBatis vs JPA
java·数据库·后端·spring·spring cloud·oracle·mybatis
qq_485015212 天前
MyBatis-Plus 3.x FieldStrategy 作用
java·数据库·mybatis
布谷歌2 天前
如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历
重构·mybatis
南城以南溫暖如初1472 天前
树洞交友系统架构设计与匿名聊天实战指南
java·spring boot·mysql·系统架构·vue·mybatis·交友
凯酱3 天前
MyBatis 中 OGNL 的使用:从入门到实战
java·tomcat·mybatis
蓝缘4 天前
解决main No MyBatis mapper was found in 的警告
mybatis
luteres4 天前
Mybatis+SSM整合笔记
java·开发语言·mybatis