1. 问题背景与现象分析
上周在优化一个后台数据导入功能时,遇到一个典型的性能问题:使用MyBatis-Plus批量插入10000条商品数据到MySQL数据库,耗时竟达到30秒。这个时间对于批量操作来说显然不合理,特别是当数据量进一步增大时,系统性能将难以承受。
通过JDBC Profiler工具分析发现,虽然代码中使用了MyBatis-Plus的saveBatch()方法,但实际执行时MySQL服务端接收到的仍然是单条INSERT语句的循环执行。这就解释了为什么性能如此低下——每次插入都需要经历完整的网络往返、SQL解析、索引更新等开销。
2. MySQL批处理机制深度解析
2.1 JDBC批处理的实现原理
标准JDBC批处理API的工作原理是:通过addBatch()方法将多条SQL语句缓存在客户端,最后通过executeBatch()一次性发送到数据库。但MySQL的JDBC驱动默认行为是将这些批处理语句拆分成单个语句依次执行,这导致批处理形同虚设。
2.2 rewriteBatchedStatements的作用机制
当设置rewriteBatchedStatements=true时,MySQL驱动会进行以下优化:
-
语句重写:将多个INSERT语句合并为单个多值INSERT语句。例如:
sql复制INSERT INTO products VALUES(...); INSERT INTO products VALUES(...);会被重写为:
sql复制INSERT INTO products VALUES(...),(...); -
网络优化:减少网络往返次数,原本N次请求合并为1次
-
服务器端优化:MySQL服务器只需解析一次SQL语句,索引更新也可以批量处理
2.3 性能对比测试数据
通过JMeter对不同场景进行压测(10000条记录):
| 配置方式 | 耗时(ms) | 网络请求次数 |
|---|---|---|
| 无批处理 | 28500 | 10000 |
| 默认批处理 | 27900 | 10000 |
| rewriteBatchedStatements | 1200 | 1 |
3. 完整配置方案与参数详解
3.1 推荐的生产级配置
yaml复制spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/production_db?
useUnicode=true&
characterEncoding=utf8mb4&
zeroDateTimeBehavior=CONVERT_TO_NULL&
transformedBitIsBoolean=true&
allowMultiQueries=true&
useSSL=true&
requireSSL=true&
verifyServerCertificate=false&
allowPublicKeyRetrieval=true&
rewriteBatchedStatements=true&
cachePrepStmts=true&
prepStmtCacheSize=250&
prepStmtCacheSqlLimit=2048&
useServerPrepStmts=true&
useCompression=true
username: app_user
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
3.2 关键参数解析
3.2.1 字符集与编码
useUnicode=true:启用Unicode支持characterEncoding=utf8mb4:使用完整的UTF-8编码(支持emoji等特殊字符)
3.2.2 日期处理
zeroDateTimeBehavior=CONVERT_TO_NULL:将'0000-00-00'转换为NULL避免Java异常
3.2.3 安全相关
useSSL=true:生产环境必须启用SSL加密verifyServerCertificate=false:开发环境可关闭证书验证(生产环境应配置CA证书)
3.2.4 性能优化组合
cachePrepStmts=true:缓存预处理语句prepStmtCacheSize=250:预处理语句缓存数量prepStmtCacheSqlLimit=2048:可缓存的SQL最大长度useServerPrepStmts=true:启用服务端预处理useCompression=true:启用网络传输压缩
4. 实现细节与注意事项
4.1 MyBatis-Plus批量插入的正确姿势
java复制// 错误示例:虽然使用了saveBatch但未正确配置连接参数
productService.saveBatch(productList);
// 正确做法:结合rewriteBatchedStatements和批次控制
int batchSize = 1000; // 根据数据量调整
for (int i = 0; i < productList.size(); i += batchSize) {
List<Product> subList = productList.subList(i, Math.min(i + batchSize, productList.size()));
productService.saveBatch(subList);
}
4.2 事务管理的注意事项
-
批量大小控制:过大的批次可能导致:
- 内存溢出(OOM)
- 事务超时
- 锁等待时间过长
-
事务隔离:建议在批量操作时使用REQUIRES_NEW传播行为,避免污染主事务
java复制@Transactional(propagation = Propagation.REQUIRES_NEW)
public void batchInsert(List<Product> products) {
productService.saveBatch(products, 1000);
}
4.3 监控与调优建议
-
监控指标:
- 批处理执行时间
- 网络往返次数
- MySQL的Com_insert计数器增长情况
-
服务器参数调整:
sql复制-- 增大max_allowed_packet(默认4MB) SET GLOBAL max_allowed_packet=32*1024*1024; -- 调整bulk_insert_buffer_size SET bulk_insert_buffer_size = 256M;
5. 常见问题排查指南
5.1 参数已设置但性能未改善
可能原因:
- 连接池未正确配置,实际使用的是未加参数的连接
- 批量大小设置不合理(建议500-2000条/批)
- 表设计问题(如过多索引、触发器)
检查步骤:
java复制// 获取实际使用的连接URL
DataSource ds = context.getBean(DataSource.class);
try(Connection conn = ds.getConnection()) {
System.out.println(conn.getMetaData().getURL());
}
5.2 大数据量插入时的内存问题
解决方案:
- 采用分批次提交
- 使用JDBC的流式插入:
java复制@Options(useGeneratedKeys = true, fetchSize = Integer.MIN_VALUE) @Insert("<script>INSERT INTO products(...) VALUES " + "<foreach collection='list' item='item' separator=','>(...)</foreach>" + "</script>") void bulkInsert(@Param("list") List<Product> products);
5.3 与其他ORM框架的配合
Hibernate配置示例:
properties复制# 必须同时配置以下参数
hibernate.jdbc.batch_size=1000
hibernate.order_inserts=true
hibernate.order_updates=true
hibernate.jdbc.fetch_size=100
6. 高级优化技巧
6.1 多线程批量插入
java复制// 使用并行流处理(注意线程安全和事务隔离)
int parallel = Runtime.getRuntime().availableProcessors();
List<List<Product>> partitions = Lists.partition(productList, batchSize);
ForkJoinPool customPool = new ForkJoinPool(parallel);
customPool.submit(() ->
partitions.parallelStream().forEach(subList -> {
transactionTemplate.execute(status -> {
productService.saveBatch(subList);
return null;
});
})
).get();
6.2 LOAD DATA INFILE替代方案
对于超大数据量(百万级+),考虑使用MySQL原生导入方式:
java复制String sql = "LOAD DATA LOCAL INFILE '" + csvFile +
"' INTO TABLE products FIELDS TERMINATED BY ','";
jdbcTemplate.execute(sql);
6.3 存储过程批量处理
创建存储过程:
sql复制DELIMITER //
CREATE PROCEDURE batch_insert_products(
IN batch_json JSON
)
BEGIN
INSERT INTO products(...)
SELECT ... FROM JSON_TABLE(...);
END //
DELIMITER ;
Java调用:
java复制jdbcTemplate.update("CALL batch_insert_products(?)", jsonStr);
在实际项目中,我们通过组合rewriteBatchedStatements参数、合理的批次大小控制和多线程处理,将百万级数据的导入时间从原来的30分钟缩短到2分钟以内。关键是要根据具体业务场景测试找到最优的批次大小和并发度,同时注意监控数据库负载情况。