乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

Executor执行器类型

导学

本节学习目标:

  • 理解MyBatis三种Executor的设计意图与适用场景
  • 掌握SIMPLE、REUSE、BATCH执行器的核心差异
  • 通过实测对比三种执行器的SQL输出与性能表现
  • 规避BATCH模式下返回结果特殊性与手动flush的误用

定义

Executor是MyBatis执行SQL的"发动机",负责调度Statement的创建、参数绑定、SQL执行和结果映射。MyBatis提供三种执行器类型,解决不同场景下的Statement复用与批量执行痛点:

  • SIMPLE:简单执行,每次创建新Statement
  • REUSE:复用Statement,同Session内相同SQL只编译一次
  • BATCH:批量累积,将多条修改语句合并发送,减少网络往返

选择合适的执行器,可以在读密集型、重复查询型、写密集型场景中分别获得最优性能。

核心原理

三种执行器工作流程对比

三种执行器特性对比

特性SIMPLEREUSEBATCH
Statement创建每次新建同SQL复用同SQL复用
Statement关闭执行后立即关闭Session结束统一关闭Session结束或flush时关闭
适用SQL类型所有所有仅insert/update/delete
批量能力无无有,累积后统一发送
返回结果实际影响行数实际影响行数特殊值(-2147482646)或需flush后获取
配置方式默认openSession(ExecutorType.REUSE)openSession(ExecutorType.BATCH)

BATCH模式特殊行为

BATCH执行器将SQL语句加入BatchResult队列,只有在以下时机才真正发送给数据库:

  1. 手动调用sqlSession.flushStatements()
  2. 调用sqlSession.commit()
  3. 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;

-- 初始为空表,准备批量插入

当前数据状态:

idnameagemajorscore
(空表)————

完整代码与配置

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&amp;serverTimezone=UTC&amp;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

分析

维度SIMPLEREUSEBATCH
Preparing次数5次(每条都编译)1次(首次编译,后续复用)1次(首次编译,批量执行)
网络往返次数5次5次1次(flush时统一发送)
返回结果实际影响行数1实际影响行数1特殊值-2147482646
总耗时(5条)45 ms28 ms12 ms
适用场景通用默认同Session重复执行同结构SQL大批量insert/update/delete
  1. SIMPLE:每次循环都Preparing新的Statement,编译开销大,耗时最长
  2. REUSE:首次Preparing后复用Statement,省去编译时间,但仍有5次网络往返
  3. BATCH:循环期间仅将参数加入批量队列,无SQL输出;flushStatements()时一次性发送,网络往返仅1次,耗时最短。返回的Update counts数组显示5条记录各影响1行

生产提示:批量插入1000条数据时,SIMPLE可能耗时3000ms,REUSE约2000ms,BATCH仅需200ms,性能差距可达10倍以上。

易错场景与常见误区

误区正解
BATCH模式适合所有SQLBATCH仅对insert/update/delete有效,select语句在BATCH模式下行为异常
BATCH模式下update返回的就是影响行数BATCH返回Integer.MIN_VALUE + 1002,必须通过flushStatements()后的BatchResult获取实际行数
循环调用mapper.insert()后数据已入库BATCH模式下必须调用flushStatements()或commit(),否则数据只停留在内存队列
REUSE执行器会自动批量发送SQLREUSE只复用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等主流插件的实现奥秘。

上一页
自定义缓存
下一页
分页插件