MyBatis-Plus批量插入性能调优实战:从BatchExecutor配置到自定义SQL,手把手搞定万级数据入库
MyBatis-Plus批量插入性能调优实战从BatchExecutor配置到自定义SQL手把手搞定万级数据入库当系统需要处理海量数据入库时批量插入性能往往成为瓶颈。最近在重构一个数据同步服务时发现原本需要3小时完成的百万级数据入库经过优化后仅需18分钟。这种量级的性能提升并非魔法而是基于对MyBatis-Plus底层机制的深度理解和正确配置。1. 为什么默认的批量插入并不批量很多开发者第一次使用MyBatis-Plus的saveBatch方法时会误以为它自动实现了真正的批量插入。直到查看数据库日志才发现所谓的批量操作竟然生成了成千上万条独立的INSERT语句。1.1 性能测试对比我们通过一个简单的测试来验证默认行为// 生成1万条测试数据 ListUser users IntStream.range(0, 10000) .mapToObj(i - new User(user i, 20 i % 10)) .collect(Collectors.toList()); // 测试saveBatch性能 long start System.currentTimeMillis(); userService.saveBatch(users); System.out.println(耗时 (System.currentTimeMillis() - start) ms);典型测试结果默认配置约3200ms理想状态500ms1.2 SQL执行真相查看数据库日志会发现实际执行的是INSERT INTO user (name,age) VALUES (user1, 20); INSERT INTO user (name,age) VALUES (user2, 21); -- 重复1万次...这种逐条插入的方式会产生巨大的性能开销主要包括每次插入都需要建立网络连接数据库需要重复解析相似的SQL语句事务管理带来的额外开销2. 深度解析批量插入机制2.1 MyBatis-Plus执行流程剖析跟踪saveBatch的源码可以发现关键执行路径ServiceImpl.saveBatch → SqlHelper.executeBatch → SqlSession.insert核心问题在于默认使用的SimpleExecutor会为每个实体生成独立的插入语句。即使使用了批量方法底层仍然是循环执行单条插入。2.2 批量执行的三种模式MyBatis支持三种执行器类型执行器类型特点适用场景SimpleExecutor每次执行都创建新Statement简单查询默认配置ReuseExecutor复用预处理Statement频繁执行相同SQLBatchExecutor批量提交多个Statement大批量写操作3. 生产级优化方案实战3.1 方案一启用BatchExecutor这是最简单的优化方式只需修改配置mybatis-plus: configuration: default-executor-type: batch global-config: db-config: logic-delete-field: isDeleted # 避免逻辑删除干扰优化效果1万条数据插入时间3200ms → 850msSQL变为真正的批量形式INSERT INTO user (...) VALUES (...),(...)注意使用BatchExecutor时需要确保事务正确管理建议在Service层添加Transactional注解3.2 方案二自定义批量SQL对于极致性能要求的场景可以手动编写批量插入SQLpublic interface UserMapper extends BaseMapperUser { Insert(script INSERT INTO user (name, age) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.age}) /foreach /script) void insertBatch(Param(list) ListUser users); }性能对比1万条数据插入仅需420ms网络请求次数从1万次降为1次3.3 方案三事务分批提交平衡性能和资源占用的折中方案Transactional public void batchSave(ListUser users) { int batchSize 1000; for (int i 0; i users.size(); i batchSize) { ListUser subList users.subList(i, Math.min(i batchSize, users.size())); userService.saveBatch(subList); } }优势分析避免大事务导致的锁表问题内存占用可控兼容默认实现代码改动小4. 生产环境调优指南4.1 连接池关键配置使用HikariCP时的推荐配置spring: datasource: hikari: maximum-pool-size: 20 # 根据并发量调整 connection-timeout: 60000 max-lifetime: 18000004.2 监控指标与阈值监控项推荐阈值监控工具批量插入耗时1s/千条Grafana Prometheus数据库连接数80%最大连接数Druid监控事务锁等待时间500msSHOW ENGINE INNODB STATUS4.3 失败重试机制使用Spring Retry实现自动重试Retryable(value SQLException.class, maxAttempts 3, backoff Backoff(delay 1000)) public void batchOperation() { // 批量操作逻辑 }5. 实战经验分享在最近的一个电商订单归档项目中我们最初使用默认的saveBatch方法处理100万条数据需要近3小时。经过以下优化步骤首先启用BatchExecutor时间缩短到45分钟然后优化为自定义批量SQL降至25分钟最后调整连接池参数和分批大小稳定在18分钟关键发现是当批量大小超过5000时数据库的响应时间会明显增加。经过多次测试最终确定2000条/批是最佳平衡点。