一、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,就能轻松把批量插入的性能提升好几倍,不用再为等数据插入而着急。