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等主流插件的实现奥秘。