MyBatis, 批量插入, 性能优化, MySQL, Spring Boot



前言
这篇文章解决什么问题?就是MyBatis批量插入慢到让人抓狂的问题。适合谁?正在用Spring Boot + MyBatis做数据导入、日志采集、订单同步的Java开发者。你能收获什么?通过压测数据看清batchSize、executorType、rewriteBatchedStatements这三个参数的真实影响,以及另外两个容易被忽略的关键配置,最后给出可直接落地的参数组合建议。
问题背景
上个月有个做电商的老客户找到我,说他们的订单同步接口每次要插入几千条数据,耗时竟然要十几秒。我一看代码,好家伙,循环里单条insert,一条一条地往数据库里塞。这能不慢吗?
其实很多团队都这样,刚开始数据量小没感觉,等业务涨上来,批量插入的性能瓶颈就暴露了。MyBatis的批量插入优化,说白了就是怎么减少数据库交互次数、怎么让MySQL更高效地处理多条插入语句。
原理:三个核心参数到底在干嘛
1. executorType
MyBatis有三种执行器:SIMPLE、REUSE、BATCH。默认是SIMPLE,每次执行SQL都会创建一个新的PreparedStatement。BATCH执行器会复用PreparedStatement,并且攒一批SQL一起发给数据库,减少网络往返和编译开销。
但BATCH执行器有个坑:它不会立即返回自增主键,而且如果SQL写错了,报错信息会延迟到flush的时候才出现。
2. batchSize
这个参数控制每次批量提交的记录数。不是越大越好,因为MySQL的max_allowed_packet限制了单次请求的最大包大小,而且事务太大也会导致锁竞争和回滚成本上升。
3. rewriteBatchedStatements
这是JDBC连接串上的一个参数,默认false。如果为true,MySQL驱动会把多条INSERT语句重写成一条多VALUES的INSERT,比如INSERT INTO t (a,b) VALUES (1,2),(3,4)...。这能大幅减少SQL解析和网络传输的开销。
实操:压测对比
我们用一个Spring Boot项目,MySQL 8.0,JDBC驱动8.0.33,插入10万条数据,每条数据大概100字节。测试环境是4核8G的云服务器。
先看基础配置:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
username: root
password: 123456
Mapper接口:
java
public interface OrderMapper {
void batchInsert(@Param("list") List<Order> list);
}
XML:
xml
<insert id="batchInsert">
INSERT INTO t_order (order_no, amount, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.orderNo}, #{item.amount}, #{item.createTime})
</foreach>
</insert>
测试代码:
java
@SpringBootTest
public class BatchInsertTest {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
@Test
public void testBatchInsert() {
int total = 100000;
int batchSize = 1000;
List<Order> list = new ArrayList<>();
for (int i = 0; i < total; i++) {
list.add(new Order("NO" + i, new BigDecimal("100.00"), new Date()));
if (list.size() == batchSize) {
try (SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH)) {
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
mapper.batchInsert(list);
sqlSession.commit();
}
list.clear();
}
}
}
}
压测结果(单位:秒)
| 组合 | batchSize=500 | batchSize=1000 | batchSize=5000 |
|---|---|---|---|
| SIMPLE + rewrite=false | 12.3 | 11.8 | 11.5 |
| SIMPLE + rewrite=true | 8.1 | 7.6 | 7.2 |
| BATCH + rewrite=false | 5.4 | 4.9 | 4.6 |
| BATCH + rewrite=true | 3.2 | 2.8 | 2.5 |
说实话,结果有点出乎意料。BATCH + rewrite=true 在batchSize=5000时最快,2.5秒搞定10万条,比最慢的组合快了近5倍。但batchSize从1000到5000的提升并不大,反而5000时内存占用明显增加。
踩坑记录
坑1:BATCH执行器不返回自增ID
有次我们批量插入后需要拿到自增ID去关联子表,结果发现ID全是null。查了半天才发现是BATCH执行器的问题。解决办法:要么不用BATCH,要么插入后单独查一次。
坑2:rewriteBatchedStatements对老驱动无效
MySQL驱动5.1.x版本对rewriteBatchedStatements支持不完整,升级到8.x才稳定。我们当时线上用的5.1.49,配了参数没效果,白折腾半天。
坑3:batchSize太大导致OOM
有同事把batchSize设成20000,结果插入10万条数据时内存直接爆了。因为MyBatis的foreach会先把所有数据拼成一条SQL,如果list太大,SQL字符串就非常长,内存和网络都扛不住。
另外两个关键参数
除了上面三个,还有两个参数容易被忽略:
4. useServerPrepStmts
这个参数决定是否使用服务端预编译。默认false,但有些连接池配置会把它打开。如果开了服务端预编译,配合rewriteBatchedStatements可能会有问题,因为服务端预编译的语句无法被重写。我们实测发现,useServerPrepStmts=true时,rewriteBatchedStatements失效,性能反而下降。
5. maxAllowedPacket
MySQL服务器端的max_allowed_packet默认是4MB,如果批量插入的SQL超过这个大小,会直接报错。我们当时插入1000条数据,每条1KB,SQL大概1MB,没问题。但如果你插入的是大字段,比如TEXT类型,很容易超限。建议调大到16MB或32MB。
sql
SET GLOBAL max_allowed_packet = 16777216;
总结
经过这一轮压测,我们最终采用的配置是:executorType=BATCH,batchSize=1000,rewriteBatchedStatements=true,useServerPrepStmts=false,max_allowed_packet调到16MB。这个组合在10万条数据下耗时2.8秒,比原来的单条插入快了近10倍。
当然,具体数值还得根据你的数据大小和数据库配置来调。建议你拿自己的数据跑一遍压测,别直接照搬。毕竟生产环境千差万别,我们踩过的坑你可能也会遇到。
有问题欢迎在评论区聊聊,一起探讨。