【无标题】

@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.records500 。也就是说,你这批实际可能只插入了 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 左右)。
  2. 使用异步落库 :如果消费速度过快,可以使用 @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 表上存在多个二级索引(特别是唯一索引),写入开销会非常大。
    • 优化建议 :如果该表主要用于日志存储,强烈建议只保留主键索引,删除所有非必要的二级索引,这能极大提升写入速度。
  2. 检查事务边界(长事务与锁)

    • 现状分析 :检查 @KafkaListener 方法或 saveTestService 实现类是否被 @Transactional 注解标记。如果有,整个批次插入会在一个长事务中完成,这会增加锁的持有时间和数据库负担。
    • 优化建议 :如果业务允许,去掉方法上的 @Transactional ,让 saveBatch 利用自动提交模式,或者使用编程式事务来精确控制边界。
  3. 补充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;
        }
    }
  2. 配置MyBatis-Plus :在配置类中,将自定义的注入器设置为全局配置。

    java 复制代码
    @Configuration
    public class MybatisPlusConfig {
        @Bean
        public MybatisPlusSqlInjector mybatisPlusSqlInjector() {
            return new BatchSqlInjector();
        }
    }
  3. 使用新方法 :在你的 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表索引上了,需要从数据库硬件或表结构设计层面进行更深入的排查。

相关推荐
FlYFlOWERANDLEAF2 小时前
微软CommunityToolkit.Mvvm和DevExpress中GenerateViewModel使用
c#
Xin_ye100863 小时前
第七章:进阶篇——事件监听、外部服务集成与性能优化
c#
z落落3 小时前
C# WinForm NModbus 串口通信+WinForm Modbus4 串口通信
开发语言·单片机·c#
CoderIsArt3 小时前
C#环境中部署MQTT客户端
开发语言·c#
CSDN_RTKLIB4 小时前
模型空间、布局控件、自定义块
c#
z落落4 小时前
C# WinForm TCP 服务器+WinForm TCP 客户端
服务器·tcp/ip·c#
csdn_aspnet5 小时前
C# 多个串口多个线程发送数据和接收数据
c#·串口·thread·com·serialport
吴可可12317 小时前
多段线局部约束缩放实现
c#
棉晗榜21 小时前
C# HttpClient配置具有tls指纹发起跟浏览器一样的请求解决反爬虫防护
开发语言·爬虫·c#