Executor 执行器类型
导学
本节学习目标:
- 理解MyBatis 三种Executor 的设计意图与适用场景
- 掌握SIMPLE、REUSE、BATCH 执行器的核心差异
- 通过实测对比三种执行器的SQL 输出与性能表现
- 规避BATCH 模式下返回结果特殊性与手动flush 的误用
定义
Executor 是MyBatis 执行SQL 的"发动机",负责调度Statement 的创建、参数绑定、SQL 执行和结果映射。MyBatis 提供三种执行器类型,解决不同场景下的Statement 复用与批量执行痛点:
- SIMPLE:简单执行,每次创建新Statement
- REUSE:复用Statement,同Session 内相同SQL 只编译一次
- BATCH:批量累积,将多条修改语句合并发送,减少网络往返
选择合适的执行器,可以在读密集型、重复查询型、写密集型场景中分别获得最优性能。
核心原理
三种执行器工作流程对比
三种执行器特性对比
| 特性 | SIMPLE | REUSE | BATCH |
|---|---|---|---|
| Statement 创建 | 每次新建 | 同SQL 复用 | 同SQL 复用 |
| Statement 关闭 | 执行后立即关闭 | Session 结束统一关闭 | Session 结束或flush 时关闭 |
| 适用SQL 类型 | 所有 | 所有 | 仅insert/update/delete |
| 批量能力 | 无 | 无 | 有,累积后统一发送 |
| 返回结果 | 实际影响行数 | 实际影响行数 | 特殊值(-2147482646)或需flush 后获取 |
| 配置方式 | 默认 | openSession(ExecutorType.REUSE) | openSession(ExecutorType.BATCH) |
BATCH 模式特殊行为
BATCH 执行器将SQL 语句加入BatchResult队列,只有在以下时机才真正发送给数据库:
- 手动调用
sqlSession.flushStatements() - 调用
sqlSession.commit() - SqlSession 关闭时
返回结果特殊性:BATCH 模式下,update()返回的不是实际修改行数,而是Integer.MIN_VALUE + 1002(即-2147482646),代表"待批量执行"。只有调用flushStatements()后,才能通过返回的BatchResult获取实际影响行数。
完整示例
场景说明
乐途公司人事系统需要批量导入新员工数据。本节模拟插入5 条记录(大翔、白歌、小崔、黄俪、李眉),分别用三种执行器执行,对比SQL 输出差异与执行效率。
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始为空表,准备批量插入
当前数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| (空表) | — | — | — | — |
完整代码与配置
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="SLF4J"/>
<!-- 默认执行器类型,可被openSession 覆盖 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/fly_db?useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/fly/mapper/StudentMapper.xml"/>
</mappers>
</configuration>
注意:MySQL 连接串添加
rewriteBatchedStatements=true,让MySQL 驱动真正将批量SQL 合并为一条multi-value 语句,否则BATCH 模式的优势无法完全发挥。
StudentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fly.mapper.StudentMapper">
<insert id="insertStudent" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student (name, age, major, score)
VALUES (#{name}, #{age}, #{major}, #{score})
</insert>
</mapper>
Student.java
package com.fly.entity;
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
public Student() {}
public Student(String name, Integer age, String major, Double score) {
this.name = name;
this.age = age;
this.major = major;
this.score = score;
}
// Getter 与Setter 省略
}
ExecutorCompareDemo.java
package com.fly.demo;
import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
public class ExecutorCompareDemo {
private static SqlSessionFactory sqlSessionFactory;
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
List<Student> students = Arrays.asList(
new Student("大翔", 22, "计算机科学", 95.5),
new Student("白歌", 21, "软件工程", 88.0),
new Student("小崔", 20, "计算机科学", 92.0),
new Student("黄俪", 21, "信息安全", 90.5),
new Student("李眉", 22, "软件工程", 87.0)
);
testSimple(students);
testReuse(students);
testBatch(students);
}
// ========== SIMPLE 执行器 ==========
static void testSimple(List<Student> students) {
System.out.println("\n========== SIMPLE 执行器 ==========");
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
long start = System.currentTimeMillis();
for (Student s : students) {
int result = mapper.insertStudent(s);
System.out.println("插入 " + s.getName() + ",返回:" + result);
}
session.commit();
long cost = System.currentTimeMillis() - start;
System.out.println("SIMPLE 总耗时:" + cost + " ms");
}
}
// ========== REUSE 执行器 ==========
static void testReuse(List<Student> students) {
System.out.println("\n========== REUSE 执行器 ==========");
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
long start = System.currentTimeMillis();
for (Student s : students) {
int result = mapper.insertStudent(s);
System.out.println("插入 " + s.getName() + ",返回:" + result);
}
session.commit();
long cost = System.currentTimeMillis() - start;
System.out.println("REUSE 总耗时:" + cost + " ms");
}
}
// ========== BATCH 执行器 ==========
static void testBatch(List<Student> students) {
System.out.println("\n========== BATCH 执行器 ==========");
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
StudentMapper mapper = session.getMapper(StudentMapper.class);
long start = System.currentTimeMillis();
for (Student s : students) {
int result = mapper.insertStudent(s);
System.out.println("插入 " + s.getName() + ",返回:" + result + "(注意:BATCH 模式返回特殊值)");
}
System.out.println("--- 调用flushStatements 前,SQL 尚未发送到数据库 ---");
// 手动flush,获取实际执行结果
List<org.apache.ibatis.executor.BatchResult> batchResults = session.flushStatements();
System.out.println("--- flushStatements 后 ---");
for (org.apache.ibatis.executor.BatchResult br : batchResults) {
System.out.println("Statement: " + br.getMappedStatement().getId());
System.out.println("Update counts: " + java.util.Arrays.toString(br.getUpdateCounts()));
}
session.commit();
long cost = System.currentTimeMillis() - start;
System.out.println("BATCH 总耗时:" + cost + " ms");
} finally {
session.close();
}
}
}
实际执行结果
控制台SQL 日志输出:
========== SIMPLE 执行器 ==========
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 大翔(String), 22(Integer), 计算机科学(String), 95.5(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 大翔,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 白歌(String), 21(Integer), 软件工程(String), 88.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 白歌,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 小崔(String), 20(Integer), 计算机科学(String), 92.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 小崔,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 黄俪(String), 21(Integer), 信息安全(String), 90.5(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 黄俪,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 李眉(String), 22(Integer), 软件工程(String), 87.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 李眉,返回:1
SIMPLE 总耗时:45 ms
========== REUSE 执行器 ==========
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 大翔(String), 22(Integer), 计算机科学(String), 95.5(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 大翔,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 白歌(String), 21(Integer), 软件工程(String), 88.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 白歌,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 小崔(String), 20(Integer), 计算机科学(String), 92.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 小崔,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 黄俪(String), 21(Integer), 信息安全(String), 90.5(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates:1
插入 黄俪,返回:1
[com.fly.mapper.StudentMapper.insertStudent] - ==> Parameters: 李眉(String), 22(Integer), 软件工程(String), 87.0(Double)
[com.fly.mapper.StudentMapper.insertStudent] - <== Updates: 1
插入 李眉,返回:1
REUSE 总耗时:28 ms
========== BATCH 执行器 ==========
插入 大翔,返回:-2147482646(注意:BATCH 模式返回特殊值)
插入 白歌,返回:-2147482646(注意:BATCH 模式返回特殊值)
插入 小崔,返回:-2147482646(注意:BATCH 模式返回特殊值)
插入 黄俪,返回:-2147482646(注意:BATCH 模式返回特殊值)
插入 李眉,返回:-2147482646(注意:BATCH 模式返回特殊值)
--- 调用flushStatements 前,SQL 尚未发送到数据库 ---
[com.fly.mapper.StudentMapper.insertStudent] - ==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
--- flushStatements 后 ---
Statement: com.fly.mapper.StudentMapper.insertStudent
Update counts: [1, 1, 1, 1, 1]
BATCH 总耗时:12 ms
分析
| 维度 | SIMPLE | REUSE | BATCH |
|---|---|---|---|
| Preparing 次数 | 5 次(每条都编译) | 1 次(首次编译,后续复用) | 1 次(首次编译,批量执行) |
| 网络往返次数 | 5 次 | 5 次 | 1 次(flush 时统一发送) |
| 返回结果 | 实际影响行数1 | 实际影响行数1 | 特殊值-2147482646 |
| 总耗时(5 条) | 45 ms | 28 ms | 12 ms |
| 适用场景 | 通用默认 | 同Session 重复执行同结构SQL | 大批量insert/update/delete |
- SIMPLE:每次循环都
Preparing新的Statement,编译开销大,耗时最长 - REUSE:首次
Preparing后复用Statement,省去编译时间,但仍有5 次网络往返 - BATCH:循环期间仅将参数加入批量队列,无SQL 输出;
flushStatements()时一次性发送,网络往返仅1 次,耗时最短。返回的Update counts数组显示5 条记录各影响1 行
生产提示:批量插入1000 条数据时,SIMPLE 可能耗时3000ms,REUSE 约2000ms,BATCH 仅需200ms,性能差距可达10 倍以上。
易错场景与常见误区
| 误区 | 正解 |
|---|---|
| BATCH 模式适合所有SQL | BATCH 仅对insert/update/delete 有效,select 语句在BATCH 模式下行为异常 |
| BATCH 模式下update 返回的就是影响行数 | BATCH 返回Integer.MIN_VALUE + 1002,必须通过flushStatements()后的BatchResult获取实际行数 |
| 循环调用mapper.insert()后数据已入库 | BATCH 模式下必须调用flushStatements()或commit(),否则数据只停留在内存队列 |
| REUSE 执行器会自动批量发送SQL | REUSE 只复用Statement,不合并网络请求;BATCH 才合并发送 |
defaultExecutorType设为BATCH 后所有操作都更快 | BATCH 下select 行为不可预期,且未flush 前无法获取自增主键 |
反例:BATCH 模式下依赖自增主键
// 反例:BATCH 模式下立即获取自增ID
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student s = new Student("大翔", 22, "计算机科学", 95.5);
mapper.insertStudent(s);
// 错误!此时s.getId()为null,因为SQL 还没发送到数据库
System.out.println(s.getId()); // 输出 null
session.flushStatements(); // 必须flush 后才能获取ID
System.out.println(s.getId()); // 此时才有值
session.commit();
session.close();
正解:BATCH 模式下,若需使用自增主键,必须在
flushStatements()之后读取。
面试考点
Q1:MyBatis 的三种Executor 有什么区别?
SIMPLE 每次创建新Statement,执行完即关闭,是默认模式;REUSE 在同Session 内缓存相同SQL 的Statement,避免重复编译,适合同结构SQL 多次执行;BATCH 将修改语句累积到队列,统一flush 时批量发送,大幅减少网络往返,适合大批量写操作。
Q2:BATCH 模式下如何获取实际的影响行数?
BATCH 模式下
update()返回特殊值Integer.MIN_VALUE + 1002。需调用sqlSession.flushStatements(),返回List<BatchResult>,通过batchResult.getUpdateCounts()获取每条SQL 的实际影响行数数组。
Q3:REUSE 和BATCH 都复用Statement,它们的核心区别是什么?
REUSE 复用Statement 但逐条执行、逐条网络往返,优势是省去SQL 编译时间;BATCH 不仅复用Statement,还将多条SQL合并为一次网络请求发送,优势是减少网络IO。REUSE 适合读或零散写,BATCH 适合大批量写。
Q4:生产环境使用BATCH 模式需要注意什么?
四点注意:① 仅用于insert/update/delete;② 需配合MySQL 的
rewriteBatchedStatements=true才能发挥最大效果;③ 批量大小需控制,一次flush 过多可能导致内存溢出或数据库拒绝;④ 获取自增主键必须在flush 之后。
小结
Executor 是MyBatis 性能调优的关键旋钮。SIMPLE 是通用默认选项;REUSE 通过Statement 复用减少编译开销,适合同结构SQL 的重复执行;BATCH 通过累积+批量发送,将网络往返从N 次降到1 次,是写密集型场景的利器。理解三种执行器的工作流程与边界条件,才能在实际业务中做出正确选择。
下一章引子
执行器优化了SQL 的发送方式,但当查询结果集庞大时,如何高效地分页获取数据成为新的挑战。MyBatis 提供了RowBounds逻辑分页和物理分页插件两种方案,前者简单但内存开销大,后者高效但需插件支持。下一节将对比两种分页方式的原理、SQL 输出差异与适用场景,并揭示PageHelper 等主流插件的实现奥秘。