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秒不满意(需求是毫秒级),可以考虑:
- **调整 `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` 方法注入。
```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;
}
}
```
- **配置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**或**表索引**上了,需要从数据库硬件或表结构设计层面进行更深入的排查。