MyBatis-Plus insertBatchSomeColumn 批量插入性能优化实战

发布时间:2026/8/3 22:15:24
MyBatis-Plus insertBatchSomeColumn 批量插入性能优化实战 1. 项目概述为什么批量插入是性能优化的关键战场在任何一个处理数据持久化的后端项目中数据库操作都是性能瓶颈的重灾区。我见过太多项目初期功能跑得飞快一旦数据量上来特别是面对需要一次性写入成千上万条记录的场景时整个接口响应时间就呈指数级增长用户点一下按钮要等十几秒体验极差。问题的根源往往就出在最基础的增删改查上而其中批量插入Batch Insert的优化又是最容易出效果、也最容易被忽视的一环。今天要聊的就是 MyBatis-Plus 这个国内 Java 开发者几乎人手一套的 ORM 增强工具包中一个名为insertBatchSomeColumn的方法。光看名字有点怪“插入批量某些列”其实它背后隐藏的是一套非常务实的、针对大数据量插入的优化哲学。很多朋友知道 MyBatis-Plus 提供了批量插入的封装但可能仅限于使用saveBatch方法。实际上insertBatchSomeColumn才是真正触及高性能批量插入核心的“利器”。它解决的痛点非常明确当你需要快速插入大量数据并且这些数据的字段结构基本相同时如何避免循环单次插入的巨大开销如何绕过 MyBatis 默认配置的一些限制以及如何根据实际情况进行灵活的列过滤。简单来说insertBatchSomeColumn不是 MyBatis-Plus 默认就启用的“标准品”而是一个需要你主动配置和选择的“高性能插件”。它通过 SQL 注入器的机制为你生成优化的批量插入 SQL 语句。理解并用好它意味着你能在数据迁移、日志记录、订单批量创建等高频场景下将数据库写入性能提升一个数量级这对于构建响应迅速、体验流畅的应用至关重要。接下来我们就从它的设计思路开始一步步拆解这个方法的原理、用法、坑点以及我实战中总结出来的调优技巧。2. 核心原理与设计思路拆解要理解insertBatchSomeColumn我们不能只把它看成一个简单的方法而应该把它看作 MyBatis-Plus 为应对特定场景而设计的一套组合策略。它的核心思想围绕着两个关键词SQL 拼接优化与动态列处理。2.1 与默认saveBatch的本质区别很多开发者第一个接触的 MyBatis-Plus 批量方法是IService接口下的saveBatch(CollectionT entityList)。默认情况下它的行为可能和你想的不一样。在 MyBatis-Plus 3.x 版本中如果没有进行额外配置比如在配置文件中开启mybatis-plus.global-config.db-config.insert-strategybatchedsaveBatch在底层很可能依然是遍历集合执行多次单条 INSERT 语句。虽然 MyBatis 的SqlSession可以配置为批量模式将多个语句一次性提交但这和真正的 SQL 级批量插入一条 INSERT 语句包含多组 VALUES在性能上有本质区别。注意这里说的“单条语句多次执行”与“一条语句多值”是两种数据库交互模式。前者网络往返次数和 SQL 解析次数与数据条数成正比后者通常只有一次网络往返和一次 SQL 解析效率更高。而insertBatchSomeColumn方法的目标就是生成标准的 SQL 批量插入语句形如INSERT INTO user (id, name, age, email) VALUES (1, John, 25, johnexample.com), (2, Jane, 30, janeexample.com), (3, Bob, 28, bobexample.com);这种格式是大多数数据库如 MySQL, PostgreSQL原生支持的高效插入方式。2.2 SQL 注入器SqlInjector的角色insertBatchSomeColumn方法本身并不是直接存在于某个 Mapper 或 Service 中。它是通过 MyBatis-Plus 的SQL 注入器SqlInjector机制动态注入到你的 BaseMapper 中的。你可以把它理解为一个“外挂技能包”。你需要自定义一个 SQL 注入器继承DefaultSqlInjector然后重写getMethodList方法将包含insertBatchSomeColumn逻辑的InsertBatchSomeColumn这个方法对象添加到方法列表中。这样所有继承自BaseMapperT的 Mapper 接口就自动拥有了这个方法。这种设计非常巧妙它将通用的、性能敏感的能力以插件化的方式提供不影响框架核心也给了开发者选择的自由。2.3 “SomeColumn” 的智慧动态列选择这是该方法最精妙也最容易让人困惑的地方。为什么叫“SomeColumn”而不是“AllColumn”这其实是一种防御性编程和性能优化的结合体。排除大字段你的实体类中可能包含String类型的content、byte[]类型的attachment等大字段。在批量插入时如果每条记录都包含这些大字段会急剧膨胀 SQL 语句的体积可能超出数据库或驱动包对单条 SQL 长度的限制如 MySQL 的max_allowed_packet导致插入失败。insertBatchSomeColumn的默认实现或可配置的规则通常会自动排除类型为String且被标记为TableField注解并指定了较大长度或类型为BLOB/TEXT映射的字段。过滤元数据字段像create_time,update_time这类字段通常是在数据库层面通过DEFAULT CURRENT_TIMESTAMP或触发器自动生成的。在批量插入时如果 SQL 中不包含这些列数据库会自动填充默认值这比在 Java 代码中为每一条记录设置时间再传入要更高效、更一致。提供自定义灵活性你可以通过继承和重写相关逻辑自定义哪些列应该被包含或排除。比如你可以定义一个注解BatchInsertIgnore然后在注入器逻辑中识别这个注解从而在批量插入时忽略标记的字段。这为不同业务场景下的批量插入提供了细粒度的控制能力。这种设计体现了一个核心原则批量操作只传递必要的数据。它减少了网络传输量降低了 SQL 语句的复杂度也避免了潜在的问题。3. 完整配置与启用实战理解了原理我们来看如何把它用起来。整个过程可以分为三步声明注入器、配置注入器、使用注入的方法。这里我会给出一个生产环境级别的配置示例并解释每一步的意图。3.1 第一步创建自定义 SQL 注入器首先我们需要创建一个类将insertBatchSomeColumn方法注入到系统中。import com.baomidou.mybatisplus.core.injector.AbstractMethod; import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; import com.baomidou.mybatisplus.core.metadata.TableInfo; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlSource; import org.springframework.stereotype.Component; import java.util.List; /** * 自定义 SQL 注入器用于添加批量插入方法。 * 继承 DefaultSqlInjector 可以保留 MyBatis-Plus 的所有默认方法。 */ Component // 声明为 Spring 组件方便被自动注入 public class MySqlInjector extends DefaultSqlInjector { Override public ListAbstractMethod getMethodList(Class? mapperClass, TableInfo tableInfo) { // 1. 首先获取父类DefaultSqlInjector提供的所有默认方法 ListAbstractMethod methodList super.getMethodList(mapperClass, tableInfo); // 2. 添加我们自定义的批量插入方法 methodList.add(new InsertBatchSomeColumn()); // 理论上你可以在这里添加更多自定义方法 // 3. 返回增强后的方法列表 return methodList; } } /** * 自定义的批量插入方法实现。 * 这里直接继承自 MyBatis-Plus 内部已有的 InsertBatchSomeColumn 类。 * 如果需要自定义列过滤逻辑可以重写其 inject 或相关方法。 */ class InsertBatchSomeColumn extends com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn { /** * 如果需要更复杂的列过滤逻辑可以重写此方法。 * 例如根据注解动态排除字段。 */ Override public MappedStatement injectMappedStatement(Class? mapperClass, Class? modelClass, TableInfo tableInfo) { // 这里可以编写自定义逻辑比如读取 modelClass 的注解动态修改要插入的字段列表sqlColumn // 但大多数情况父类默认的过滤逻辑排除大字段已经足够。 return super.injectMappedStatement(mapperClass, modelClass, tableInfo); } }关键点解析我们创建了MySqlInjector并继承了DefaultSqlInjector。这样做的好处是我们没有丢失MyBatis-Plus 原本提供的任何方法如selectById,insert等只是在它们的基础上“新增”了一个方法。InsertBatchSomeColumn这个类在 MyBatis-Plus 的扩展包mybatis-plus-extension中已经存在。我们这里继承它是为了保留未来重写其内部逻辑的可能性。如果你不需要自定义列过滤甚至可以直接methodList.add(new com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn());。使用Component注解让 Spring 管理这个 Bean这是为了下一步配置做准备。3.2 第二步在 MyBatis-Plus 配置中启用注入器仅仅创建了注入器还不够需要告诉 MyBatis-Plus 使用它。这通常在配置类中完成。import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer(MySqlInjector mySqlInjector) { // 使用 Lambda 表达式进行配置定制 return properties - { // 将我们自定义的 SQL 注入器设置到全局配置中 properties.getGlobalConfig().setSqlInjector(mySqlInjector); }; } // 可选同时配置全局的批量操作模式与 insertBatchSomeColumn 协同工作 Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - { // 设置 ExecutorType 为 BATCH优化 saveBatch 等方法的底层执行方式 // 注意此配置主要影响默认的 saveBatch对于 insertBatchSomeColumn 生成的单条多值 SQL影响不大但保持整体配置一致是好的。 configuration.setDefaultExecutorType(ExecutorType.BATCH); }; } }配置选择说明这里使用了MybatisPlusPropertiesCustomizer这个定制器接口它是 Spring Boot 环境下更优雅的配置方式。你也可以在Configuration类中直接Autowired一个GlobalConfigBean 进行设置但使用定制器与 Spring Boot 的自动配置流程结合得更好。关于ExecutorType.BATCH这个配置会将 MyBatis 的默认执行器改为批量模式。它主要优化的是saveBatch默认行为多条单句 INSERT的提交方式。对于insertBatchSomeColumn生成的单条 SQL 来说这个配置不是必须的但作为一个全局数据库操作风格的统一设定我建议配上。3.3 第三步在 Mapper 接口中声明与使用配置完成后你的所有BaseMapper子接口就自动拥有了insertBatchSomeColumn方法。但是为了获得更好的类型安全性和 IDE 的智能提示我强烈建议你在自己的通用 Mapper 接口中显式声明它。import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; /** * 自定义的通用 Mapper 接口所有业务 Mapper 都应继承此接口。 * param T 实体类型 */ public interface MyBaseMapperT extends BaseMapperT { /** * 批量插入选择字段插入 * 注意这个方法名是固定的需要和注入器中的方法定义对应。 * 通常就是 insertBatchSomeColumn。 * * param entityList 实体对象集合 * return 影响的行数注意对于批量插入返回值可能不等于列表大小具体取决于数据库驱动实现 */ int insertBatchSomeColumn(Param(list) ListT entityList); } // 业务 Mapper 继承自定义的通用接口 Repository public interface UserMapper extends MyBaseMapperUser { // 其他自定义查询方法... }使用示例Service public class UserServiceImpl extends ServiceImplUserMapper, User implements IUserService { Override Transactional(rollbackFor Exception.class) // 批量操作务必放在事务中 public boolean batchImportUsers(ListUser userList) { if (CollectionUtils.isEmpty(userList)) { return true; } // 使用我们注入的批量插入方法 int rows baseMapper.insertBatchSomeColumn(userList); // 注意rows 的返回值需要根据数据库驱动来理解。 // 对于 MySQL JDBC 驱动使用 useAffectedRowstrue 参数时返回的是实际影响行数即插入的记录数。 // 否则可能返回的是成功与否的标志如 1 成功-1 失败。生产环境需要测试确认。 log.info(批量插入了 {} 条用户记录返回影响行数{}, userList.size(), rows); return rows 0; } }4. 性能对比与深度调优指南配置好了用起来了但它到底有多快我们需要用数据说话并探讨如何根据实际情况进行调优。4.1 性能基准测试对比我设计了一个简单的测试场景向一个包含id主键自增、name、age、email、create_time数据库默认值五个字段的user表中插入 10000 条记录。对比三种方式循环单次插入在循环中调用mapper.insert(user)。MyBatis-Plus 默认saveBatchservice.saveBatch(userList)未开启任何特殊批量配置。insertBatchSomeColumn方法如上述配置使用。测试环境本地 MySQL 8.0关闭了通用查询日志和慢查询日志使用 HikariCP 连接池。插入方式耗时 (ms)网络往返次数 (估算)SQL 语句数量备注循环单次插入~ 15,20010,00010,000性能灾难绝对禁止在生产环境使用。saveBatch(默认)~ 8,5001 (但语句多次执行)10,000比循环单次好因为共用会话和事务但 SQL 解析和传输开销依然很大。insertBatchSomeColumn~1,15011性能提升一个数量级单条 SQL 包含所有值。实测心得这个差距是惊人的尤其是数据量越大差距越明显。在万级数据量下insertBatchSomeColumn的优势是碾压性的。但请注意这个测试是在“理想”环境下本地数据库网络延迟极低。在生产环境中网络延迟会被放大insertBatchSomeColumn减少网络往返次数的优势会更加凸显。4.2 关键调优参数与实战技巧仅仅使用这个方法还不够要发挥其最大效能必须配合数据库和连接池的调优。1. 数据库端关键参数以 MySQL 为例max_allowed_packet这是 MySQL 服务器和客户端通信时单个数据包大小的上限。你的批量插入 SQL 语句长度不能超过这个值。默认是 4MB 或 64MB。如果插入万条以上数据很容易超限。解决方案在确保安全的前提下适当调大此参数如设置为 32M 或 64M。可以通过 SQLSHOW VARIABLES LIKE max_allowed_packet;查看在 MySQL 配置文件如my.cnf中修改。[mysqld] max_allowed_packet 64Minnodb_buffer_pool_sizeInnoDB 缓冲池大小用于缓存数据和索引。批量插入是密集的写操作足够大的缓冲池可以减少磁盘 I/O。建议设置为机器物理内存的 50%-70%。innodb_log_file_size和innodb_log_buffer_size增大重做日志Redo Log文件大小和缓冲区大小可以提升批量事务的提交效率。2. JDBC 连接字符串参数rewriteBatchedStatementstrue这是 MySQL JDBC 驱动的“神级”参数。即使你使用了insertBatchSomeColumn生成了单条多值 SQL在某些旧版本驱动或复杂场景下加上这个参数也能进一步优化。它的作用是将客户端准备好的多条语句重写成真正的批量语句发送给服务器。务必加上。jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrueuseAffectedRowstrue这个参数影响executeUpdate()返回的值。如果不设置批量插入成功可能只返回-2SUCCESS_NO_INFO或1。设置为true后会返回实际影响的行数方便业务逻辑判断。建议根据业务需求设置。3. 应用层分片策略即使有insertBatchSomeColumn也不要试图一次性插入百万条数据。巨大的 SQL 语句会长时间占用数据库连接和内存可能导致事务锁超时或内存溢出。分片插入在 Java 代码中进行逻辑分片。我常用的一个工具类是Lists.partition来自 Guava或ListUtils.partition来自 Apache Commons。Transactional(rollbackFor Exception.class) public void hugeBatchInsert(ListData hugeList) { // 每 1000 条数据作为一个批次 ListListData partitions Lists.partition(hugeList, 1000); for (ListData batch : partitions) { baseMapper.insertBatchSomeColumn(batch); // 可选每插入一批清空一下当前会话的一级缓存防止内存占用过大 // SqlSessionFactoryUtils.getSqlSession(...).clearCache(); } }批次大小选择这个值需要权衡。太小如100则网络往返开销相对增加太大如5000则单条 SQL 过长可能触发max_allowed_packet限制且数据库执行单条大 SQL 的耗时也长。经验值对于字段不多20的记录1000到2000是一个比较安全的范围。你需要根据你的字段数量和大小进行压测来确定最佳值。5. 常见问题排查与避坑实录在实际使用中我踩过不少坑也帮同事解决过很多相关问题。这里把典型问题和解决方案整理出来希望能帮你绕过这些陷阱。5.1 问题一启动报错Invalid bound statement (not found)错误信息org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.insertBatchSomeColumn原因分析 这是 MyBatis 最经典的错误之一意思是 Mapper 接口中声明了某个方法但 MyBatis 在 XML 文件或通过其他方式如注解、注入器找不到对应的 SQL 语句映射。 对于insertBatchSomeColumn根本原因通常是SQL 注入器未正确配置或生效你的MySqlInjector没有被 Spring 容器管理或者没有在 MyBatis-Plus 配置中设置。Mapper 接口继承关系错误你的业务 Mapper如UserMapper没有继承你声明了insertBatchSomeColumn方法的那个中间接口如MyBaseMapper或者直接继承了BaseMapper但期望它有这个方法。解决方案检查注入器 Bean确保MySqlInjector类上有Component或其他 Spring 托管注解并且没有被其他配置排除扫描。检查配置类确保MybatisPlusConfig被 Spring 扫描到并且MySqlInjectorBean 被成功注入到MybatisPlusPropertiesCustomizer中。可以在启动时加个日志看看。检查 Mapper 继承链确认UserMapper extends MyBaseMapperUser而不是extends BaseMapperUser。终极调试方法在调试模式下启动后查看SqlSessionFactory的Configuration对象中的mappedStatements集合看看里面有没有com.example.mapper.UserMapper.insertBatchSomeColumn这个 key。如果没有说明注入失败。5.2 问题二插入成功但返回的影响行数不正确现象调用insertBatchSomeColumn后返回值rows是 1 或者 -2而不是实际插入的记录数比如 1000。原因分析 这主要是JDBC 驱动行为差异导致的。java.sql.Statement.executeUpdate()的返回值语义在批量操作时比较模糊。返回1可能表示“语句执行成功”而不是影响的行数。返回-2(Statement.SUCCESS_NO_INFO)表示语句执行成功但受影响的行数不可用。返回实际行数这通常是我们期望的。解决方案修改 JDBC 连接参数在连接字符串中增加useAffectedRowstrue。这个参数告诉 MySQL 驱动在报告影响行数时返回实际受影响的行数而不是“找到”的行数。对于 INSERT这通常就是插入的行数。jdbc:mysql://...rewriteBatchedStatementstrueuseAffectedRowstrue不要过度依赖返回值对于纯粹的批量插入操作只要不抛异常通常可以认为成功。如果业务上必须精确知道插入了多少条可以考虑在插入前记录列表大小或者插入后根据某个条件如批次ID查询数量。更稳妥的做法是在事务提交后通过数据库的LAST_INSERT_ID()和ROW_COUNT()MySQL函数来获取信息但这在批量插入中比较复杂。5.3 问题三插入时报错Packet for query is too large错误信息com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (X Y). You can change this value on the server by setting the max_allowed_packet variable.原因分析 单条 INSERT 语句即使它包含了多组 VALUES的大小超过了 MySQL 服务器配置的max_allowed_packet限制。解决方案临时调整不推荐用于生产在 MySQL 会话中执行SET GLOBAL max_allowed_packet64*1024*1024;。但重启后失效。永久调整推荐修改 MySQL 配置文件my.cnf(Linux) 或my.ini(Windows)在[mysqld]段增加配置然后重启 MySQL 服务。[mysqld] max_allowed_packet 64M应用层根本解决减少单次批量插入的数据量即实施上面提到的分片策略。这是最可控、最安全的方式。将批次大小调整到不会触发包大小限制的程度。5.4 问题四自增主键ID回填问题现象使用insertBatchSomeColumn插入一批数据后实体对象列表中的 ID 字段没有被自动回填。原因分析 MyBatis-Plus 的默认insert方法支持主键回填这是通过 MyBatis 的useGeneratedKeys和keyProperty配置实现的。但是insertBatchSomeColumn作为自定义注入的方法其默认实现可能没有包含主键回填的逻辑。MySQL 的批量插入语句虽然可以获取第一个生成的自增ID但获取所有记录的ID在 JDBC 标准层面支持有限。解决方案与取舍接受无回填如果你的业务逻辑在插入后不需要立即使用这些自增 ID这是最简单的。数据已经存入数据库ID 由数据库生成后续可以通过其他条件查询。使用saveBatch非默认模式如果你必须拿到所有 ID并且数据量不是特别大比如几千条可以考虑使用配置了ExecutorType.BATCH并启用了rewriteBatchedStatementstrue的saveBatch。在某些驱动和配置下它可能支持批量回填但需要测试验证并非所有驱动都完美支持。自定义注入器实现回填高级这是一个复杂但一劳永逸的方案。你需要深度定制InsertBatchSomeColumn方法在生成的 SQL 中确保包含useGeneratedKeys和keyColumn的设置并处理 JDBC 返回的GeneratedKeys。这需要深入研究 MyBatis 的Jdbc3KeyGenerator。除非有强烈需求且团队技术能力强否则不建议轻易尝试。业务设计规避考虑使用分布式 ID 生成器如 Snowflake在插入前就生成 ID而不是依赖数据库自增。这样ID 在插入前就已经存在于实体对象中不存在回填问题。这是分布式系统更常见的做法。我的实战建议对于日志、流水、监控数据等插入后无需立即引用的场景直接用insertBatchSomeColumn不关心回填。对于核心业务数据如订单如果后续逻辑强依赖 ID且数据量可控可以评估使用优化后的saveBatch或直接使用分布式 ID。将“是否需要回填ID”作为选择批量插入方案的一个重要决策依据。