ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

别被-saveBatch-骗了-一次-MyBatis-Plus-批量插入性能排查

别被-saveBatch-骗了-一次-MyBatis-Plus-批量插入性能排查 别被 saveBatch 骗了一次 MyBatis-Plus 批量插入性能排查摘要这次问题的表象是一个定时任务写入大量报表明细后数据库触发了长事务和 SQL 审计尖峰。第一眼看代码业务已经用了 MyBatis-PlussaveBatch(records,200);这很容易让人产生一个判断既然已经saveBatch批量插入应该没问题了。但后续 SQL 审计和 A/B 验证证明事情并没有这么简单。同样插入 5000 条数据同样使用saveBatch(list, 200)场景平均耗时未开启rewriteBatchedStatements约 44.2 秒开启rewriteBatchedStatementstrue约 2.2 秒差距大约 20 倍。最终根因不是业务代码没有调用批量 API而是 MySQL Connector/J 没有开启批量重写导致 JDBC batch 没有变成数据库侧真正高效的 multi-values INSERT。这次排查最重要的收获是框架层 batch、JDBC 层 batch、数据库侧 multi-values batch不是同一件事。1. 问题现象代码看起来批量了数据库侧却仍然很忙某天早上的报表定时任务执行后数据库监控出现长事务告警。继续看 SQL 审计可以看到同一个时间窗口内 INSERT 数量明显抬高主要集中在两类报表明细表。现象如下06:15 附近SQL 审计出现明显尖峰 一分钟内审计记录数超过 3 万条 主要 INSERT 集中在报表明细表 A 和报表明细表 B这个现象说明它不是某一条复杂 SQL 慢而是短时间内大量写入把事务时间和数据库压力一起推高了。于是第一步自然是回到代码里看是不是有人在循环里一条一条save()结果不是。业务代码里已经用了类似下面的写法reportService.saveBatch(reportList,200);也就是说从业务代码视角看开发并没有写最糟糕的循环单条插入。矛盾就出现了代码saveBatch(list, 200) 审计仍然像大量单条 INSERT 一样形成尖峰如果排查停在代码层很容易得出错误结论既然已经saveBatch了那批量插入就不是问题。但 SQL 审计不会关心我们调用了什么 API它只反映数据库最终看到了什么。2. 关键认知三层 batch 不是一回事这次排查真正有价值的地方不是发现了某个参数而是把“批量插入”拆开看清楚了。2.1 框架层 batch业务代码调用saveBatch(list,200);这是 MyBatis-Plus 提供的批量保存入口。它的价值很明确业务层不用给每张表手写循环插入也不用在每个 Service 里维护一套重复 mapper。但它只能说明我们进入了框架提供的批量 API。它不能直接证明数据库服务端收到的就是一条 multi-values INSERT。2.2 JDBC 层 batchMyBatis-Plus 底层会通过 MyBatis/JDBC 执行 batch大致可以理解为preparedStatement.addBatch();preparedStatement.addBatch();preparedStatement.addBatch();preparedStatement.executeBatch();这说明应用和 JDBC 驱动之间确实进入了 batch 语义。但 JDBC batch 仍然只是客户端和驱动之间的执行方式。驱动最终怎么把这些 batch 发给 MySQL还取决于驱动实现和连接参数。2.3 数据库侧 multi-values batch我们真正希望数据库执行的是这种形态INSERTINTOdemo_table(id,name,amount)VALUES(?,?,?),(?,?,?),(?,?,?);而不是这种形态INSERTINTOdemo_table(id,name,amount)VALUES(?,?,?);INSERTINTOdemo_table(id,name,amount)VALUES(?,?,?);INSERTINTOdemo_table(id,name,amount)VALUES(?,?,?);前者减少了 SQL 解析、网络往返和服务端处理成本后者即使来自executeBatch()在数据库侧仍然可能更接近“大量单条写入”。所以这次的核心判断是saveBatch 说明框架层用了 batch executeBatch 说明 JDBC 层用了 batch 只有最终 SQL 形态才能说明数据库侧是否真的吃到了批量收益。3. 根因少了 rewriteBatchedStatementstrue排查 JDBC URL 后发现连接参数里有常见配置例如字符集、时区等但没有rewriteBatchedStatementstrue这个参数是 MySQL Connector/J 的性能扩展配置。按官方文档说明它会在调用executeBatch()时对 INSERT / REPLACE 这类 PreparedStatement 做批量重写把它们转换成更高效的 multi-values 形式。该参数默认值是false。也就是说如果没有显式开启它不能简单假设 JDBC batch 一定会变成数据库侧 multi-values INSERT。还有一个容易混淆的参数是allowMultiQueriestrue它允许一条 Statement 中包含多条 SQL但这不是 JDBC batch rewrite 的开关。把它打开不等于打开了rewriteBatchedStatements。最终根因可以压缩成一句话MyBatis-Plus 已经做了 JDBC batch但 MySQL Connector/J 没有开启批量重写数据库侧没有获得 multi-values INSERT 的性能收益。4. A/B 验证只改变一个变量测试场景如下测试表batch_insert_test 数据量5000 条 批大小200 代码saveBatch(list, 200) 变量JDBC URL 是否增加 rewriteBatchedStatementstrue关键是只改变一个变量。这样结果才能归因到 JDBC 参数而不是机器负载、业务逻辑、索引差异或测试数据不同。4.1 未开启 rewriteBatchedStatements三次结果A47139 ms B42751 ms C42697 ms平均耗时约44.2 秒4.2 开启 rewriteBatchedStatementstrue三次结果A1945 ms B2738 ms C1807 ms平均耗时约2.2 秒4.3 结果44.2 / 2.2 ≈ 20同样的数据量同样的saveBatch(list, 200)只增加一个 JDBC 参数性能提升约 20 倍。这说明之前的瓶颈不是业务没有批量而是批量语义没有在数据库侧充分兑现。5. 为什么平时体感只是“不丝滑”不是“完全不可用”这个问题有个迷惑性它不一定每天都炸。原因是业务代码已经避免了最差的写法。它不是这样for(Recordrecord:records){service.save(record);}它至少已经通过saveBatch进入了 JDBC batch所以不会慢到特别离谱。但因为驱动没有重写成 multi-values INSERT它也没有达到理想状态。所以平时的体感可能是后端响应不是特别慢但导入、初始化、报表生成、明细落库总感觉不够顺。可以粗略理解成代码层面从 0 分优化到了 60 分 驱动参数没开所以没能到 90 分。这类问题通常会在这些场景集中暴露报表生成Excel 导入初始化数据写入订单/采购/库存明细落库财务分录、余额、流水批量生成定时任务集中写入但也要说清楚边界它不会让所有接口都快 20 倍。直接受益的是基于 JDBC batch 的批量 INSERT 场景。普通查询、单条写入、Redis 调用、远程调用、Java 计算、慢索引查询都不属于这次优化的收益范围。6. 为什么不直接给每张表写 XML foreach看到 multi-values INSERT 后很多人的第一反应是那我手写 XMLforeach不就行了吗当然可以但这不是第一选择。如果每张表都写一套批量 mapper会带来几个问题每张表都要维护一段批量 SQL 字段变更时 mapper 容易漏改 通用 CRUD 和定制 mapper 并存维护成本升高 项目里已有大量 saveBatch 调用逐个替换成本很高 问题本质在驱动层参数不在每个 Service 的写法更合理的顺序是先打开 MySQL Connector/J 官方提供的 batch rewrite 能力 让现有 saveBatch 调用自动获得收益。只有少数极端场景再考虑定制超大批量导入需要LOAD DATA INFILE特殊ON DUPLICATE KEY UPDATE语义单表瓶颈明确通用方案仍不足需要特殊分片、临时表或中间表策略绝大多数业务系统里先把 JDBC 层能力用对比到处写定制 SQL 更低成本。7. 风险边界这个参数不是不能开但也不是零风险rewriteBatchedStatementstrue是官方能力但上线前仍然要评估边界。7.1 单批 SQL 包会变大开启后多条 INSERT 会被重写成一条 multi-values INSERT。单条 SQL 包会变大。如果 batchSize 设置得过大可能碰到max_allowed_packet PacketTooBig 连接异常所以建议不要在同一次上线里同时做两件事开启 rewriteBatchedStatementstrue 同时把 batchSize 从 200 放大到 2000先保持原有批大小只改一个变量。7.2 自增主键回填要单独验证如果业务强依赖数据库自增 ID 的逐条回填需要单独验证。很多企业项目会使用雪花 ID、业务 ID 或应用侧生成主键这类场景风险相对低一些。但不能因为别的项目没问题就默认自己也没问题。7.3 ON DUPLICATE KEY UPDATE 的 update count 语义MySQL Connector/J 官方文档也提醒rewriteBatchedStatementstrue和INSERT ... ON DUPLICATE KEY UPDATE一起使用时批量重写后的 affected rows 很难精确映射回每条原始语句。如果业务依赖每一条 batch item 的更新行数就不能只看性能要补充语义验证。7.4 普通 Statement 的 SQL 注入风险官方文档还提示普通 Statement 在输入没有清洗时批量重写可能扩大 SQL 注入风险。但典型 MyBatis-PlussaveBatch使用的是 PreparedStatement 参数绑定不是字符串拼接 SQL。真正需要警惕的反而是系统里是否存在手写拼接 SQL、是否全局开启了不必要的多语句执行能力。8. 推荐上线清单我的建议是按小步验证不要把多个优化叠在一起。短期方案JDBC URL 增加 rewriteBatchedStatementstrue 保持现有 saveBatch(list, 200) 暂不放大 batchSize 暂不大面积改 XML mapper验证顺序开发环境完成 A/B 验证。测试环境回归报表、导入、初始化、单据明细等批量 INSERT 场景。检查是否有自增主键回填、ON DUPLICATE KEY UPDATE、流式字段写入等特殊场景。生产低峰发布。观察慢 SQL、SQL 审计 INSERT 数量、数据库 CPU、主从延迟、PacketTooBig、连接异常。验证口径不要只看接口耗时。更重要的是看数据库侧事实SQL 审计记录数是否下降 批量 INSERT 的最终 SQL 形态是否改变 数据库负载是否下降 定时任务事务时间是否缩短9. 这次排查给我的几个提醒9.1 不要被 API 名字骗了saveBatch这个名字很容易让人放松警惕。但排查性能问题时不能停在“我调用了批量 API”。还要继续问JDBC 驱动怎么执行 网络往返有没有减少 数据库审计里是什么形态 最终 SQL 是单条洪峰还是 multi-values9.2 性能排查要看最终事实这次真正推动问题往下走的是 SQL 审计。如果只看代码会觉得已经批量了如果只看接口耗时会觉得只是定时任务偏慢但 SQL 审计能直接告诉我们数据库最终承受了什么。9.3 A/B 实验要控制变量这次结果有说服力是因为固定了这些条件同一张表 同一数据量 同一 batchSize 同一 saveBatch 代码 只改变 rewriteBatchedStatements性能优化最怕“顺手又改了几个地方”。一旦变量混在一起就很难判断到底是谁带来了收益。9.4 好的优化不一定是多写代码一开始很容易走向“写 XML foreach”“每张表加批量 mapper”。但这次更好的方案是打开 JDBC 驱动已有能力让系统里现有的批量调用自动收益。少写代码、少改业务、收益明确、风险可控这才是更值得优先考虑的优化。10. 可以直接复用的排查模板以后再遇到“代码已经批量了但数据库还是很忙”的问题可以按这个顺序查1. 先看业务代码是不是循环单条写入。 2. 确认是否调用了框架批量 API例如 saveBatch。 3. 看 JDBC URL 是否开启 rewriteBatchedStatements。 4. 用 SQL 审计或 general log 看最终 SQL 形态。 5. 做 A/B同代码、同数据量、同 batchSize只改 JDBC 参数。 6. 评估 max_allowed_packet、自增主键、ON DUPLICATE KEY UPDATE、update count 等边界。 7. 小流量或低峰上线观察数据库侧指标。这套模板不只适用于 MyBatis-Plus。只要是 Java MySQL JDBC batch 的批量写入链路都值得检查这一层。结语这次问题表面上是一次长事务告警实际上暴露的是一个很常见的抽象泄漏框架 API 告诉你“我批量了” JDBC 驱动决定“怎么批量” 数据库审计告诉你“最终到底批没批量”。性能优化不能只相信代码意图要看最终事实。saveBatch没有错MyBatis-Plus 也没有错。错的是我们把“调用了批量 API”误认为“数据库已经按最高效方式执行”。下次再看到saveBatch(list, 200)可以多问一句数据库侧看到的真的是 multi-values INSERT 吗参考资料MySQL Connector/J Developer GuidePerformance ExtensionsrewriteBatchedStatements配置说明https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-performance-extensions.htmlMySQL Connector/J Developer GuideConfiguration Properties性能扩展属性表https://dev.mysql.com/doc/connector-j/en/connector-j-reference-configuration-properties.htmlMyBatis-Plus 官方文档持久层接口与saveBatchhttps://baomidou.com/guides/data-interface/MyBatis-Plus 官方文档批量操作https://baomidou.com/guides/batch-operation/本文为作者原创首发于掘金CSDN 为同步发布版本。
返回列表