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

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

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

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第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 等主流插件的实现奥秘。

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