
干后端的人迟早都会碰上这么一档子事一张几十万行的表要批量写入你按老规矩在for循环里一条一条insert跑了半小时还没完DBA已经在群里喊话了。换成Mybatis做批量插入网上方案五花八门最常听到的就是foreach插入、SqlSession批量插入、原生sql拼接这三种。说实话我第一次对比完这三者的时候被结果吓到了——在特定连接参数下SqlSession批量插入居然比foreach还慢而手拼SQL却能快上一倍。这个“Mybatis插入大量数据效率对比”说白了不是比谁的代码写得炫而是比谁真正懂得JDBC批处理、SQL文本长度、事务刷盘这些底层机制。这篇文章把三种方案全部拆开讲附上实测数据和踩坑记录适合正在优化批量写入速度、准备mybatis面试题、或者做电商后台这类高写入场景的人参考。1. 三种批量插入方案的代码形态与直观差异1.1 foreach插入最直观但SQL文本会无限膨胀foreach插入应该是我见过最普遍的写法。mapper.xml里写一个insert标签里面用foreach把list拼成多条valuesinsert idbatchInsertForeach parameterTypelist INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES foreach collectionlist itemitem separator, (#{item.orderNo}, #{item.userId}, #{item.amount}, #{item.createTime}) /foreach /insert从码农视角看这段代码清晰好懂不需要额外的执行器设置直接用mapper接口调一次就完成全部插入。它的工作方式是一次请求发一条巨大的INSERT语句给MySQL把所有记录织进一条SQL的多个VALUE元组里。这种做法的好处是网络往返只有一次对比一条一条insert动不动几百次请求已经算得上是质的飞跃。但代价同样明显SQL文本长度和条目数成正比。list里100条的时候SQL文本几KB问题不大到了几千条SQL文本十几MBMySQL服务端的max_allowed_packet直接给你脸色看。而且MySQL解析一条超长SQL还需要消耗更多的CPU做词法分析、语法树构建和执行计划生成条目越多单条SQL的平均解析成本越高性能边际递减非常明显。所以foreach不是不能用而是必须限制批量大小。我自己用foreach的底线是单批不超过500条超过就拆分多次调用。如果你单批2000条还能跑得动那多半是表结构简单、字段少、平均每条values只有几十字节千万别觉得全世界都这样。1.2 SqlSession批量插入依赖ExecutorType.BATCH第二种方案是直接用SqlSession打开批量模式。做法是先拿到一个指定了ExecutorType.BATCH的SqlSession然后反复调用mapper的insert方法最后由自己控制flushStatements和commitSqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH, false); try { OrderMapper mapper session.getMapper(OrderMapper.class); for (int i 0; i orders.size(); i) { mapper.insert(orders.get(i)); if (i % 1000 0) { session.flushStatements(); } } session.commit(); } finally { session.close(); }Mybatis在BATCH模式下会使用BatchExecutor。它的设计思路是同一条SQL语句如果反复执行就不再重复创建PreparedStatement而是把每次传入的参数攒下来通过JDBC的addBatch丢进同一个批量集合里。只有当你调用flushStatements或者触发commit的时候这些参数才会真正作为一个batch发送出去。这个方案从表面看没有改变SQL文本仍然是单条insert语句循环执行但它把“每条SQL一次网络往返”变成了“一批参数一次网络往返”网络消耗大幅下降。很多人的误解是只要打开BATCH就自动快。实际上不是。JDBC的addBatch只是把参数放进了客户端内存真正发送到服务端是在executeBatch阶段而且发送方式还取决于MySQL驱动的rewriteBatchedStatements参数。这个细节我放在第二章详细说因为它是整个性能测试里最大的变量。另外BATCH模式对事务的要求更高。它基本必须配合手动commit而且flushStatements本身不会提交事务只是把本地缓存的批量语句推给数据库执行事务边界仍然由commit和rollback控制。如果忘记commit数据不会落库如果flushStatements执行中一半失败你要么自己处理部分提交的问题要么把整个事务回滚。注意Spring环境下如果用SqlSessionTemplate它对ExecutorType.BATCH的支持是有特殊处理的。很多时候你在业务代码里手动openSession绕过Spring事务管理容易造成连接泄漏或事务不生效建议直接用SqlSessionTemplate配置batch模式或者单独在特定方法里切换别图省事。1.3 原生SQL拼接绕过Mybatis自己拼values第三种方案更暴力彻底绕过Mybatis动态SQL直接在service层用StringBuilder把SQL拼出来再用JdbcTemplate或原生JDBC执行。典型写法StringBuilder sql new StringBuilder(INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES ); for (int i 0; i orders.size(); i) { if (i 0) { sql.append(,); } sql.append(().append(orders.get(i).getOrderNo()).append(,) .append(orders.get(i).getUserId()).append(,) .append(orders.get(i).getAmount()).append(,) .append(NOW()).append()); } jdbcTemplate.execute(sql.toString());这段代码我会先放出来然后必须强调它只适合内部数据导入工具不适合任何暴露给外部输入的接口。因为字符串拼接天然会让SQL注入有可乘之机——如果orderNo是用户传入的单引号转义处理不好一个精心构造的payload就能把你的SQL改得面目全非。另外Date类型、BigDecimal的格式都要你自己处理序列化Mybatis的类型转换、null处理、特殊字符转义全部失效开发量并不小。但它确实能带来最极端的插入速度。因为MySQL收到一条巨长的多values INSERT时从解析到执行只需要一次SQL状态机处理比重复多轮的batch更快。如果控制好长度不触发max_allowed_packet对5k条记录而言实测往往比加了rewriteBatchedStatements的BATCH还要快一点。这种差距主要来自于SQL文本本身比batch执行中反复的消息编解码更“直接”。然而为了那一点速度牺牲安全性和可维护性在业务代码里很不划算。我见过有人在订单表的主流程里手拼SQL后来一条脏数据让整张表出现重复订单排查了一下午。批量导入工具里用一用可以业务代码里我还是强烈建议用前两种方案。2. 底层原理深挖为什么同一份代码性能差好几倍2.1 JDBC的batch机制和rewriteBatchedStatements参数先说JDBC原生batch。我们执行“一条insert语句循环绑定参数再executeBatch”的时候JDBC驱动做的事情并不是真的把一批数据打包成一个网络包发出去。在MySQL Connector/J里默认情况下executeBatch的实现是在客户端循环调用executeStatement也就是网络层面还是一条一条发只是应用层看起来像批量。真正能提速的开关是连接串里的rewriteBatchedStatementstrue。当这个参数打开后Connector/J在executeBatch收到多条相同结构的INSERT语句时会先把它们按VALUES结构合并成一条多values的INSERT再发送给服务端。这一步等于在客户端自动地帮你做了“foreach拼接”而且由于参数仍然是以PreparedStatement方式传入的类型转换、转义都由驱动处理安全性和可靠性比手拼高得多。所以说SqlSession批量插入的性能好本质是“JDBC batch rewriteBatchedStatements”这两个条件同时满足的结果。如果连接串没加这个参数BATCH模式的收益主要只有PreparedStatement复用网络往返并没有实质减少性能自然上不去。这就是为什么很多人在网上发帖说“批量插入10w条用了20秒”另一个人同配置却只要3秒这一行连接参数就是分水岭。提示MySQL Connector/J从5.1.x到8.x都支持rewriteBatchedStatements参数但要注意这个参数对INSERT ... ON DUPLICATE KEY UPDATE这类语句的支持并不完整某些版本会退化成逐条执行。使用前最好在驱动源码或文档里确认识别条件。2.2 Mybatis三种Executor的差异与BatchExecutor源码关键点回到Mybatis框架本身它有三个执行器SimpleExecutor每执行一次SQL都创建新的StatementHandler用完即关闭ReuseExecutor把StatementHandler缓存起来碰到相同SQL的重复执行就复用PreparedStatementBatchExecutor在Reuse的基础上把同一条SQL的多次执行打包成addBatch延迟到flushStatements才真正提交。很多人容易混淆Reuse和Batch。Reuse只复用PreparedStatement不批量submitBatch才真正调用addBatch。而BatchExecutor的判断逻辑藏在一段关键的if里大致是if (ms ! currentMs || sql.equals(currentSql)) { // 不是同一条SQL关闭原来的Statement新建一个 }也就是说BATCH模式下如果两次插入的SQL文本不同批量会立刻失效退化成逐条执行。最常见的触发原因就是在mapper.xml里用了动态SQLforeach里如果拼了if判断list中某些对象缺失字段生成的SQL文本就会不一样例如有的条目包含字段remark有的不包含等于一会儿是INSERT(...) VALUES(...)一会儿是INSERT(...,remark) VALUES(...,?)结构不一致batch彻底失效。另外BATCH模式下Mybatis对parameterObject的处理是延迟到flush时才真正绑定参数所以自定义TypeHandler可能出现行为异常。常规SIMPLE模式下setParameter是立即执行的BATCH模式下参数是在addBatch时记录到集合里最后由JDBC驱动统一绑定。如果你写了一个自定义TypeHandler做字段加密批量插入时可能发现加密逻辑没生效最后库里存的是原文。排查时要先想到这个坑不要一上来就怀疑TypeHandler本身写错了。2.3 InnoDB提交刷盘与事务取舍抛开Mybatis批量插入的最终写入方是InnoDB。InnoDB在提交事务时默认innodb_flush_log_at_trx_commit1意思是每次事务提交都要把redo log刷到磁盘保证持久性。如果业务代码里每插入一条就commit一次那就是1万次fsync而把1万条放进一个事务提交一次fsync次数直接降到1次。这块性能差距比Mybatis内部任何优化都大。很多批量插入慢根因根本不在SQL层而是事务粒度太细。把多条插入包进一个事务后磁盘IO从“每秒几百次同步刷盘”变成“每秒几万条数据的顺序写”差距非常直观。如果把innodb_flush_log_at_trx_commit从1调成2性能还能再上一个台阶代价是操作系统宕机时可能丢失最近一秒的事务日志是否接受取决于业务属性。对订单这种核心数据我一般维持1对日志表、流水表这类允许轻微丢失的数据我见过不少团队调到0或2来换吞吐。此外批量插入还要注意记录锁和间隙锁。如果表上有二级索引InnoDB的插入会维护索引结构二级索引越多插入越慢。批量数据用事务打包后锁的范围可能变大如果其他事务还要插入相近主键的数据可能出现锁等待。INSERT和INSERT之间本身不会互相阻塞只要没有唯一键冲突和间隙锁干扰大批量插入在同一个事务里反而是最优的。3. 实测10万条数据三种方案跑分对比3.1 测试环境、表结构与数据准备先交代测试环境方便你对比自己的机器。JDK8Spring Boot2.7.xmybatis-spring-boot-starter2.3.1MySQL8.0.33连接方式JDBC mysql-connector-java 8.0.33操作系统macOS 笔记本SSD数据量10万条订单记录表结构CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint NOT NULL, amount decimal(10,2) NOT NULL DEFAULT 0.00, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;准备数据时我用一个循环生成10万个OrderBean其中order_no用UUID前8位加自增序号拼接user_id随机amount在1到1000之间。生成完成后分别跑三种方案每组重复三次取中间值避免JIT和缓存抖动带来的误判。3.2 三种方案的完整代码foreach方案insert idbatchInsertForeach parameterTypelist INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES foreach collectionlist itemitem separator, (#{item.orderNo}, #{item.userId}, #{item.amount}, #{item.createTime}) /foreach /insertservice里按500条一批切分后调用public int batchInsertForeach(ListOrderBean orders) { int batchSize 500; int total 0; for (int i 0; i orders.size(); i batchSize) { ListOrderBean subList orders.subList(i, Math.min(i batchSize, orders.size())); total orderMapper.batchInsertForeach(subList); } return total; }SqlSession批量方案public void batchInsertBySqlSession(ListOrderBean orders) { SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH, false); try { OrderMapper mapper session.getMapper(OrderMapper.class); for (int i 0; i orders.size(); i) { mapper.insertBySingle(orders.get(i)); if (i % 1000 0) { session.flushStatements(); } } session.commit(); } finally { session.close(); } }对应的mapper.xml只需要一个普通单条insertinsert idinsertBySingle parameterTypecom.demo.OrderBean INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES (#{orderNo}, #{userId}, #{amount}, #{createTime}) /insert原生SQL方案public void batchInsertByRawSql(ListOrderBean orders, int batchSize) { JdbcTemplate jdbcTemplate ...; for (int i 0; i orders.size(); i batchSize) { ListOrderBean subList orders.subList(i, Math.min(i batchSize, orders.size())); StringBuilder sb new StringBuilder(INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES ); for (int j 0; j subList.size(); j) { if (j 0) sb.append(,); OrderBean ob subList.get(j); sb.append(().append(ob.getOrderNo()).append(,) .append(ob.getUserId()).append(,) .append(ob.getAmount()).append(,) .append(NOW()).append()); } jdbcTemplate.execute(sb.toString()); } }这段代码我特意没有在字符串里对单引号做过滤就是为了提醒你真实工程里千万不要这么裸接外部数据。3.3 跑分结果与逐项分析我跑完了完整的10万条数据下表是纯插入耗时数据生成和事务提交之外的执行段。方案关键配置单批条数总耗时秒备注foreach默认连接串5002.3SQL约几十KB正常foreach默认连接串20008.7SQL明显变长解析成本上升foreach默认连接串5000报错Packet too largeSqlSession BATCH默认连接串100018.6没有rewrite时很慢SqlSession BATCHrewriteBatchedStatementstrue10003.9有明显提升SqlSession BATCHrewriteBatchedStatementstrue50002.8本次最快原生SQL默认连接串50001.9最快但有隐患原生SQL默认连接串20000报错超过packet限制有几个结果值得细说。第一foreach 500条一批只要2.3秒因为它的网络往返只有200次10w/500SQL长度可控MySQL解析起来压力不大。但是到了2000条一批总往返虽然只有50次SQL单条文本却长了四倍解析开销开始盖过网络收益耗时飙到8.7秒。这就是“解析成本随SQL长度非线性增长”的直观体现。第二SqlSession BATCH在默认连接串下慢得离谱18.6秒。原因就是我前面说的JDBC batch没有rewriteBatchedStatements时只是在客户端逐个execute网络往返并没有减少反而因为多了一层Executor包装多一些开销。一旦把rewriteBatchedStatementstrue加进连接串同样代码直接掉到3.9秒配置5000条一批时甚至到2.8秒已经接近手拼SQL的水平。第三手拼SQL 5000条一批只要1.9秒是全场最快但你要记得这里面是没有任何PreparedStatement处理的安全性完全裸奔。我用它做参照更多是为了展示“上界在哪里”如果有一天MySQL本身成了瓶颈手拼SQL也不是银弹因为它的提升主要来自于绕过驱动层消息编解码也就是优化网络和解析并没有改变InnoDB的写入本质。3.4 决定性能的几个关键参数计算很多人问我批量大小到底怎么定我会先估算SQL长度再反向推算批大小。一条values元组文本长度可以通过字段加权估算。以上面的t_order为例一条元组大约65字节含括号和逗号。如果单批N条整体SQL长度大约是SQL总长度 ≈ INSERT固定部分约60字节 N × 65字节max_allowed_packet在MySQL 8.0默认是64MB线上很多环境调到128MB。如果单批5000条SQL大约325KB远低于64MB为什么我们5000条还报Packet too large原因是用foreach时Mybatis实际上是先把所有值拼成一个字符串再传给JDBC同时PreparedStatement的参数在客户端要缓冲有些版本的驱动会把参数展开一定倍数导致内存中实际占用的判定空间比文本长度大很多。为了安全我的经验值是单批条数×单条values字节数不要超过max_allowed_packet的十分之一。比如64MB的packet单批SQL文本控制在6MB以内换算到上面的表就是大约9万条但实际工程里单批9万条显然太大了真正受限制的是事务日志、连接读写超时、应用内存所以我会把单批控制在1000到5000条之间。连接串参数影响也不小建议参考jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8rewriteBatchedStatementstrueuseServerPrepStmtsfalse其中useServerPrepStmtsfalse会关闭服务端预编译对批量写场景通常能减少一次额外的预处理往返同时避免一些版本下的缓存问题。不过这个参数在MySQL 8.0 Connector/J里的默认行为有变化具体表现因版本而异跑分时我建议分两组对照。4. 批量插入避坑指南生产环境问题复盘4.1 Packet for query is too large为什么这么常见异常信息长这样java.sql.SQLException: Packet for query is too large (4,358,708 4,194,304). You can change this value on the server by setting the max_allowed_packet variable.大部分新人看到这个第一反应是调大max_allowed_packet但我要说明两点第一这是服务端参数同时客户端也有一个maxAllowedPacket参数两端只要有一边小就会报错第二调大之后只是让“更大的SQL”能进来不代表性能好MySQL要为一个几十MB的SQL预分配内存连接一多就可能拖垮实例。注意连接串里用maxAllowedPacket单独设置时单位是字节而且只对当前连接生效。生产环境如果用的是连接池要确认连接池的参数配置方式能够正确传递这个值否则你改了JDBC URL也没用。针对这个问题治本的办法永远是控制单批条数其次才是调参数。我见过的严重事故是业务侧把1万条订单一次性foreach出去packet超限后重试又把内存打满直接把MySQL实例搞到OOM边缘。先拆批再谈优化。4.2 事务回滚失效、自动提交陷阱还是那句话批量插入必须开事务。Mybatis的SqlSession默认不自动提交但不少人用Spring却忘了加Transactional于是底层连接每次execute之后都会自动commit数据写一条提交一条。一旦中途报错前面成功的那几千条就留在库里了你以为是“全部回滚”其实是“只滚了最后一条”。正确做法是在批量插入入口方法上标记Transactional并保证事务传播行为符合预期。如果手动openSession记得显式session.commit()和session.rollback()。另外flushStatements并不代表事务提交它只是把批量语句发到数据库如果flush之后、commit之前抛异常事务还是可以整体回滚的。我遇到过最隐蔽的问题在Spring事务方法里手动openSession拿到的是同一个底层连接但事务状态由Spring管理你手动调session.commit()后事务直接提前提交后面的代码再执行时发现连接已经脱离了事务上下文。所以能用Spring事务管住的场景就别自己手动openSession。4.3 动态SQL导致批量失效、TypeHandler失效这个坑是我这半年才真正意识到的。BATCH模式下如果mapper.xml里的INSERT用到了 动态标签批量是否生效完全取决于每次拼接出的SQL文本是否一致。比如insert iddynamicInsert INSERT INTO t_order (order_no, user_id, amount, create_time if testremark ! null,remark/if ) VALUES (#{orderNo}, #{userId}, #{amount}, #{createTime} if testremark ! null,#{remark}/if ) /insertlist里如果有几条remark是null、几条不是null那么“前面一条SQL的列集合”和“后面一条的列集合”不同BatchExecutor就会发现currentSql变化立刻换一批批量退化成逐条执行。你以为是批量实际是一条条发的。这类问题最大的麻烦是线上数据一旦混合性能波动很难解释。解决办法很简单批量插入使用的insert语句不要用动态标签要么所有字段都传要么把常见字段统一处理。如果你确实要支持部分字段可以按字段组合先分桶保证同一个桶里的insert结构完全一致再分别批量执行。TypeHandler失效的问题我刚才提过BATCH模式下自定义TypeHandler的setParameter不一定按SIMPLE模式那样立即执行。如果你要做字段加密、枚举转换先确保在批量模式下也拿去验证一遍不要等上了线才发现库里全是明文。我当时给订单号加解密逻辑单条插入一切正常改成批量后加密字段全变成了空排查了半天最后定位到BatchExecutor参数绑定时机上。4.4 慢SQL与IO瓶颈的排查顺序上线后发现批量插入慢不要一头扎进Mybatis配置。我的排查顺序是看慢查询日志确认慢的是哪条SQL、执行时间和锁等待时间用SHOW ENGINE INNODB STATUS看是否有锁等待、undo log膨胀看磁盘IO的await和util确认是不是刷盘导致瓶颈看binlog同步、主从复制是否拖后腿最后再回头检查应用侧连接串、批大小、事务提交频率。常见的原因是主库IO占用高因为批量写入把binlog和redo log的量放大如果每个batch都commitgroup commit的作用会被削弱。把多个batch合进一个事务提交是立竿见影的优化。还有一个容易被忽略的点批量插入时如果表上有频繁的二级索引变更InnoDB的change buffer压力会变大可以在一次性导入场景里暂时删除部分非必要索引导完再重建。业务的实时写入别乱用这招。5. 选型建议与实践经验5.1 按数据量分级100条、1000条、1万条、10万条的推荐根据我自己的实际使用经验批量插入方案的选型不能脱离数据量。我建议按这个表来数据量推荐方案必要条件备注≤100foreach无代码简洁性能足够100~1000foreach或BATCHrewriteBatchedStatements两者差距不大1000~10000SqlSession BATCH控制单批1000~5000性能最稳10000以上SqlSession BATCH 多线程连接池足够注意事务和幂等百万级导入LOAD DATA INFILE文件可访问、权限允许和业务代码解耦单批条数不是越大越好。BATCH模式下单批太大意味着JDBC驱动要预先缓冲大量参数内存占用高flush时也可能触发更长的单次网络传输一旦失败重试代价很大。我更看重“可控的批量”加上“合理的重试机制”。5.2 常见mybatis面试题批量插入高频考点最近面试题里经常出现Mybatis批量高频问题大概是这几个Mybatis能不能用一条SQL完成批量插入能多values的INSERTforeach拼出一串values。为什么用了foreach批量插入反而性能不好SQL文本过长、解析成本提高、有max_allowed_packet上限还有MySQL对超长SQL的内存占用问题。ExecutorType.BATCH的原理是什么BatchExecutor缓存PreparedStatement参数通过addBatch累积flushStatements统一提交网络往返减少。rewriteBatchedStatementstrue的作用是什么驱动层把一批相同结构的INSERT合并成多values SQL减少SQL文本解析基数和服务端处理次数。批量插入时一级缓存为什么直接失效因为insert/update/delete操作会触发SqlSession一级缓存的清空。BATCH模式下TypeHandler为什么会失效因为参数延迟到flush时才绑定某些自定义处理逻辑在批量模式下的执行时机不同。能把这几个问题讲清楚基本可以把“熟悉Mybatis”写得名正言顺。如果还能画出BatchExecutor的执行流程印象分会高很多。5.3 从批量插入到SQL优化的延伸去重、忽略插入、唯一索引既然聊到了SQL优化这里也分享一个批量插入里的去重场景。业务上经常要往一张表里批量写入订单或日志重复数据如果不处理后面统计全脏。比起“先查一遍再插”我建议建好唯一索引后用INSERT IGNORE或ON DUPLICATE KEY UPDATEINSERT IGNORE INTO t_order (order_no, user_id, amount, create_time) VALUES (...), (...); INSERT INTO t_order (order_no, user_id, amount, create_time) VALUES (...), (...) ON DUPLICATE KEY UPDATE amount VALUES(amount);这样MySQL会在索引冲突时自动跳过或更新应用侧不需要额外查询性能比“先查再插”高出一大截而且唯一索引冲突本身就是幂等保护。不过要注意ON DUPLICATE KEY UPDATE在多行VALUES批量下会让每个冲突行的更新触发额外索引写入量大的时候也要评估。如果写入频率不高、但单次量很大我觉得INSERT IGNORE更稳。“sql语句去重”这个需求其实还有另一层含义批量插入时先做内存去重。JVM里用HashSet对id或唯一键过滤一遍再走批量插入。这个方案的优点是数据库压力小缺点是对超大列表内存有要求。我通常是双层应用层先用HashSet粗过滤库里再用唯一索引兜底双保险。6. 写在最后的个人体会文章写到这儿其实没必要强行总结了。就说一点我踩透了的感悟批量插入性能这件事永远不要只盯着Mybatis层。SQL文本长度、JDBC驱动连接参数、事务提交频率、InnoDB刷盘策略这四个变量没理清你在Mybatis里怎么折腾都是隔靴搔痒。我自己现在写批量插入前会习惯性问三个问题数据量级是多少允许丢数据吗连接串参数确认过没有如果都答得上来再选方案。如果你正打算把项目里的批量插入改掉哪怕只做一件事也建议先给JDBC连接串加上rewriteBatchedStatementstrue然后跑一次压测你会看到惊喜。最后再分享一个小技巧真遇到百万级以上的数据导入别死磕Mybatis直接用LOAD DATA INFILE配合临时表和业务代码解耦性能差不多是线性级别的提升。工具用对了比堆优化参数有效得多。