一、MyBatis里的Executor是什么?
平时做后端开发,用MyBatis写批量插入的时候,你有没有遇到过:插1000条用户数据,普通方式等了10秒还没完成,换个写法居然1秒就搞定?这背后的核心原因,就是MyBatis的Executor执行器选得不对。Executor就像是MyBatis和数据库之间的“运输队”,不同的运输队干活的方式不一样,直接决定了批量写的速度。
1.1 Executor的基本作用
简单说,Executor就是MyBatis用来和数据库打交道的“工具人”,不管是查数据、插数据还是改数据,都要经过Executor。它的核心职责是把你写的SQL,转换成数据库能听懂的指令,再和数据库建立连接、发送请求、接收结果,相当于在业务代码和数据库之间搭了一座“沟通的桥”。不同的Executor实现,这座桥的宽度和通行效率完全不一样。
二、批量写入选什么Executor?
做批量写入的时候,最常用的是BatchExecutor,而MyBatis默认的是SimpleExecutor,这两个是性能差异最大的Executor类型,也是我们今天要重点说的。
2.1 BatchExecutor的特殊逻辑
如果把SimpleExecutor比作“送快递的小哥,每次只送一个包裹”,那BatchExecutor就是“快递车队,攒够一整车包裹再一起配送”。具体来说,BatchExecutor不会每写一条数据就立刻发一次SQL请求,而是会把所有要执行的插入操作,先“攒”到一个内部的批处理任务列表里,直到你主动让它提交任务(比如调用commit())的时候,才把一整批的SQL一次性发给数据库,这样就大大减少了和数据库的交互次数——而数据库连接和请求的建立,正是批量写入时最耗时的部分。
三、写个Demo对比性能
我们用实际的代码,对比两种Executor在批量插入时的性能差异,这里用统一的技术栈:Java 8 + MyBatis 3.5.9 + MySQL 8.0,代码里都加了注释,方便看懂每一步的作用。
3.1 测试代码(完整示例)
// 技术栈:Java 8 + MyBatis 3.5.9 + MySQL 8.0
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import java.io.InputStream;
// 实体类,对应数据库user表
class User {
private Long id;
private String name;
private Integer age;
// 省略getter/setter,不影响核心逻辑
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
// Mapper接口,定义插入方法
interface UserMapper {
int insertUser(User user);
}
// 主测试类,对比两种Executor的性能
public class BatchPerformanceTest {
public static void main(String[] args) throws Exception {
// 1. 初始化MyBatis的SqlSessionFactory(简化版,实际项目中从配置文件读)
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2. 测试默认的SimpleExecutor(普通执行器)
long start = System.currentTimeMillis();
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 插入1000条测试数据
for (int i = 0; i < 1000; i++) {
mapper.insertUser(new User("普通Executor用户_" + i, 20 + i%10));
}
session.commit(); // 手动提交事务
}
System.out.println("普通Executor耗时:" + (System.currentTimeMillis() - start) + "ms");
// 3. 测试BatchExecutor(批量执行器)
start = System.currentTimeMillis();
// 关键:打开SqlSession时指定Executor类型为BATCH!
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 插入1000条测试数据
for (int i = 0; i < 1000; i++) {
mapper.insertUser(new User("BatchExecutor用户_" + i, 20 + i%10));
// 每插入100条就手动刷一次任务,避免内存溢出
if (i % 100 == 99) {
session.flushStatements();
}
}
session.flushStatements(); // 最后刷一次,把剩余数据提交
session.commit();
}
System.out.println("BatchExecutor耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
3.2 示例结果说明
实际运行这个测试,普通Executor插入1000条数据的耗时大概在5000-8000ms,而BatchExecutor的耗时大概在500-1000ms,性能差了5-10倍——这就是Executor选择对批量写入的直接影响。
四、适用场景
不是所有批量写入都适合用BatchExecutor,得看具体场景:
4.1 适合用BatchExecutor的场景
当你要写入的数据量比较大,比如一次性导入1万条以上的用户数据、同步订单数据、生成测试数据的时候,BatchExecutor的优势会非常明显。因为这些场景的核心是“减少数据库交互次数”,而BatchExecutor正好解决了这个问题。
4.2 不适合用BatchExecutor的场景
如果只是写入少量数据(比如几十条以内),或者是单条数据的常规插入(比如用户注册),用BatchExecutor反而会因为攒任务的额外开销,比普通Executor慢;另外,如果你的批量操作需要中途查询其他数据,或者需要更灵活的事务控制,也不适合用BatchExecutor。
五、技术优缺点
两种Executor各有优劣,我们分点说清楚:
5.1 BatchExecutor的优缺点
优点:性能提升明显,减少数据库连接和请求次数,大数据量下的优势巨大;缺点:需要手动控制flush,不然数据会卡在内存里提交不进去;如果批量操作中途出错,回滚的复杂度比普通Executor高;小数据量下额外开销反而更大,还可能占内存。
5.2 普通Executor的优缺点
优点:灵活,适合单条或小批量操作,事务控制更简单,不会因为攒任务占内存;缺点:大数据量写入时性能太差,每次请求数据库的开销会累积,导致耗时过长。
六、使用注意事项
用BatchExecutor的时候,有几个关键点一定要注意,不然会踩坑:
6.1 必须手动flush数据
BatchExecutor不会自动把攒的任务提交给数据库,所以一定要在循环里适时调用session.flushStatements(),或者最后提交事务前调用,不然数据会留在内存里,根本不会插入到数据库里。
6.2 事务的正确处理
批量操作一定要放在一个事务里,用session.commit()提交,不要自动提交(自动提交模式下BatchExecutor可能失效),如果中途出错,要记得回滚,不然会出现部分插入的脏数据。
6.3 避免内存溢出
不要一次性攒太多任务,比如一次插入10万条就不flush,这样会把JVM的内存占满,导致OOM。一般每插入100-1000条就flush一次,根据数据大小调整,比如每条数据很大的话,flush的数量可以少一点。
6.4 不要混操作
同一个BatchExecutor里不要同时做插入、查询、更新等多种操作,不然会把其他类型的SQL也攒到任务里,执行的时候可能出错,最好每个操作单独开一个SqlSession。
七、总结
批量写入的性能瓶颈,很多时候不是SQL写得不好,而是选的Executor不对。普通Executor适合小数据量或单条操作,BatchExecutor适合1000条以上的大数据量写入,用的时候要记得手动flush、控制批量大小、处理好事务。学会选对Executor,就能轻松把批量插入的性能提升好几倍,不用再为等数据插入而着急。
Comments