1. Executor 概述
1.1 定义与作用边界
Executor 是 MyBatis 的“执行引擎”:负责把 Mapper 层的增删改查请求(MappedStatement
+ 参数)转换为底层 JDBC 调用,并协调参数处理、语句准备/复用、结果集映射、事务与缓存等一系列环节。官方文档把可选执行器类型归纳为三种:SIMPLE / REUSE / BATCH;此外还有一个用于二级缓存的装饰器 CachingExecutor(默认在开启缓存时包裹在外层)。
-
SIMPLE:每次执行都创建新的
PreparedStatement
。 -
REUSE:复用
PreparedStatement
,减少反复 prepare 的成本。 -
BATCH:缓存多条更新语句并按批发给数据库,以减少网络往返与驱动开销。
-
CachingExecutor(装饰器):在外部包裹具体执行器,先查缓存、命中则返回,未命中再委派给内部执行器并写入缓存。
与协作组件
Executor 不直接做 SQL 拼装与参数设置,它与以下三大处理器协作:
-
StatementHandler:负责
Statement
的创建/参数化/执行(prepare/parameterize/query/update/batch
)。 -
ParameterHandler:把实参按映射规则设置到 JDBC
PreparedStatement
。 -
ResultSetHandler:把 JDBC
ResultSet
映射为目标对象(列表/游标等)。
插件机制允许拦截以上四类对象(含 Executor 本身),切入点包括 Executor.update/query/flushStatements/commit/rollback
,以及 Statement/Parameter/ResultSet 的关键方法。
图 1(文字版):
SqlSession → Executor(可能被 CachingExecutor 包裹) → StatementHandler → JDBC
配套处理器:ParameterHandler
(入参)与ResultSetHandler
(出参)。
1.2 生命周期:从 openSession
到 close
创建:SqlSessionFactory.openSession()
→ Configuration.newExecutor(ExecutorType)
。若开启二级缓存,Configuration
会用 CachingExecutor 包裹真实执行器(SIMPLE/REUSE/BATCH)。
使用:通过 SqlSession
发起 select/insert/update/delete
,内部委派给 Executor 的 query/update
等方法。
提交/回滚:SqlSession.commit/rollback
→ Executor 同名方法,进而驱动 Transaction
。
关闭:SqlSession.close
→ Executor.close,释放语句句柄与连接资源。
缓存清理:flushStatements/commit/close
等关键点会触发行缓冲及(在适用时)缓存提交/清空。关于装饰器与事务性缓存,请注意二级缓存采用事务性缓冲(TransactionalCache),在 commit
时才“生效可见”。
示例:mybatis-config.xml 设置默认 Executor 类型
<!-- 1.2.1 默认执行器类型(不显式指定时默认 SIMPLE) -->
<configuration>
<settings>
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 是否启用二级缓存;默认 true,会让 Configuration 用 CachingExecutor 包裹实际执行器 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
defaultExecutorType
字面含义即“默认执行器类型”。SIMPLE/REUSE/BATCH 三选一;SIMPLE 为默认。
1.3 执行链路总览(含简化源码走读)
以一次查询为例(省略插件与动态 SQL 细节):
-
SqlSession.selectList
→ 调用Executor.query
。 -
Executor 内部创建 RoutingStatementHandler,它会根据
StatementType
路由到PreparedStatementHandler/SimpleStatementHandler/CallableStatementHandler
。 -
StatementHandler.prepare
:从连接上创建/复用PreparedStatement
。parameterize
:由ParameterHandler
对参数进行绑定。 -
执行 SQL,得到
ResultSet
,交由ResultSetHandler
映射到目标对象。 -
若启用二级缓存,外层 CachingExecutor 会先尝试读缓存、未命中则委派到真实执行器,并在查询完成后把结果写入缓存(但须等待事务提交后才对其他会话可见)。
源码锚点(便于你后续第 8 章逐行解析)
Configuration#newExecutor
:创建具体执行器并在需要时包裹CachingExecutor
。
BaseStatementHandler
:聚合ParameterHandler/ResultSetHandler
,定义prepare/parameterize
流程。
RoutingStatementHandler
:按语句类型选择具体StatementHandler
。
1.4 Executor 类型一览与差异定位
SIMPLE
-
策略:每次执行都新建
PreparedStatement
,执行完即关闭。 -
优点:语义最直观、资源生命周期简单、不易踩坑。
-
代价:频繁的 prepare / close;在高 QPS 或同 SQL 重复执行场景偏贵。
官方说明“does nothing special”,适合作为默认。
REUSE
-
策略:复用
PreparedStatement
,通常按 SQL 文本作为 key 管理。 -
优点:降低多次相同 SQL 的准备/关闭开销;和数据库/驱动的 statement cache 形成互补。
-
注意:复用需要谨慎处理参数与游标、及时清理;跨事务复用与连接切换均需遵守 MyBatis/连接池策略。官方文档的定义非常直接:will reuse PreparedStatements。
BATCH
-
策略:聚合多条 DML(INSERT/UPDATE/DELETE),走 JDBC 的
addBatch/executeBatch
。 -
收益:减少网络往返、驱动编解码、服务器解析次数;批量写多时优势显著。
-
限制:批间穿插 SELECT 会触发必要的
flush
;错误处理为批量异常;部分数据库/驱动对批量回填主键、批量大小有约束。
CachingExecutor(装饰器)
-
策略:当全局
cacheEnabled=true
或 Mapper 声明<cache/>
时,外层包裹CachingExecutor
;其query
先读二级缓存、未命中再委派并写回。 -
边界:二级缓存为
SqlSessionFactory
级别,提交时生效;flushCache
/DML/事务边界会使其失效。
1.5 与 SqlSession / Transaction 的协作关系
-
SqlSession 是应用的门面;
selectList/insert/update/delete
最终委派给 Executor 的query/update
。 -
Transaction:Executor 持有事务(通常由数据源/容器如 Spring 管理),
commit/rollback/close
向下驱动真正的连接提交/回滚。 -
缓存与事务:二级缓存采用事务性缓冲(
TransactionalCache
),在commit
之前其他会话不可见;rollback
会丢弃本地缓冲。
1.6 插件(Interceptor)在执行链中的位置
MyBatis 允许对 Executor / StatementHandler / ParameterHandler / ResultSetHandler 四类对象进行插件拦截。你可以在 Executor.update/query/flushStatements/commit/rollback
,以及语句准备/参数绑定/结果处理等节点切入做审计、限流、脱敏等。官方在 configuration#plugins
一节列出了所有可拦截方法。
小提示:多个拦截器按注册顺序形成代理链,常见的“执行顺序不符合预期”往往与注册顺序有关(见社区讨论)。
1.7 常见误区澄清(FAQ)
-
“DefaultExecutor” 是哪个类?
MyBatis 没有名为DefaultExecutor
的类。所谓“默认执行器”是指Configuration.newExecutor
按配置选择 SIMPLE/REUSE/BATCH,并在需要时用 CachingExecutor 包裹。 -
开启二级缓存一定更快吗?
不一定。命中率、结果集大小、序列化成本、失效频率都会影响收益;官方生态也提供 Ehcache 等二级缓存适配,但要结合业务读写比评估。 -
为何默认不是 BATCH?
批处理需要更复杂的错误处理与刷批时机,语义不如 SIMPLE 直观,因此默认选择 SIMPLE 更安全、可预测。
1.8 迷你代码示例(感知执行链)
// 1.8.1 通过 ExecutorType 显式选择执行器(覆盖全局 defaultExecutorType)
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
List<User> users = session.selectList("demo.UserMapper.selectByStatus", "ACTIVE");
session.commit(); // 触发 Executor.commit -> Transaction.commit,并影响二级缓存可见性
}
结合上文,你可以在日志里观察到:
SqlSession
→Executor.query
→StatementHandler.prepare/parameterize/query
→ResultSetHandler
的调用轨迹。
1.9 小练习(检查与巩固)
-
如果把
defaultExecutorType
改为BATCH
,但在一次业务流程里先insert
再select
,会发生什么?为什么很多时候会看到一次“刷批”?(提示:批间SELECT
的语义与一致性) -
在开启二级缓存后,同一个
SqlSessionFactory
下两个不同的SqlSession
连续做相同的只读查询,第二次是否一定命中缓存?在哪个时间点之后才会对另一个会话可见?(提示:TransactionalCache) -
想实现“给所有更新语句自动追加租户条件”的审计需求,你会拦截
Executor
还是StatementHandler
?各有什么利弊?(提示:插件切点与 SQL 重写时机)
本章小结
-
Executor 是 MyBatis 的执行核心,SIMPLE/REUSE/BATCH 是“行为策略”,CachingExecutor 是“缓存装饰器”。
-
SqlSession → Executor → StatementHandler → JDBC
的链路上,ParameterHandler
与ResultSetHandler
分别负责参数绑定与结果映射;插件可在多个关键方法处拦截。 -
生命周期围绕
openSession/commit/rollback/close
展开;二级缓存采用事务性缓冲,在提交时才对其他会话可见。
2. BaseExecutor 与子类关系
2.1 继承结构与角色定位
在 MyBatis 的 org.apache.ibatis.executor
包下,Executor
是顶层接口,而 BaseExecutor
则是它的抽象基类,实现了通用的事务、缓存、生命周期管理逻辑。
继承结构(简化版 UML 文字描述):
Executor (接口)
↑
BaseExecutor (抽象类)
├─ SimpleExecutor
├─ ReuseExecutor
└─ BatchExecutor
-
Executor(接口)
定义了统一的执行方法:update
、query
、flushStatements
、commit
、rollback
、close
等。 -
BaseExecutor(抽象类)
实现了除“具体执行 SQL”之外的大部分公共逻辑,例如事务管理、一级缓存(localCache)、延迟加载等。保留了两个核心抽象方法doUpdate
与doQuery
,交由子类决定具体的 Statement 生成与执行策略。 -
SimpleExecutor
每次都新建PreparedStatement
执行,执行完即关闭。 -
ReuseExecutor
通过缓存Statement
来复用,减少创建/关闭的开销。 -
BatchExecutor
聚合多条更新语句到批处理队列中,调用addBatch
和executeBatch
。
补充:CachingExecutor 不在这个继承链上,它是对 Executor 的装饰器(Decorator),通过组合持有一个 Executor 实例。
2.2 BaseExecutor 的核心属性
源码位置(3.5.9):org.apache.ibatis.executor.BaseExecutor
public abstract class BaseExecutor implements Executor {
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
private boolean closed;
private boolean wrapperSet;
}
关键成员解释:
-
transaction:持有的事务对象,封装了 JDBC Connection。
-
localCache:一级缓存,
PerpetualCache
+CacheKey
组合,用来缓存查询结果。 -
localOutputParameterCache:用于存储存储过程的输出参数缓存(CallableStatement)。
-
deferredLoads:延迟加载队列,配合懒加载功能。
-
wrapper:当 Executor 被插件或 CachingExecutor 包裹时,
wrapper
指向最外层代理。
2.3 模板方法骨架
BaseExecutor 对外暴露的 update
、query
等方法,都遵循模板方法模式(Template Method):
-
公共逻辑由父类实现
-
具体 SQL 执行细节交给子类实现的
doUpdate
/doQuery
以 update
为例:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache(); // 写操作清空一级缓存
return doUpdate(ms, parameter); // 具体执行逻辑由子类完成
}
以 query
为例(部分关键逻辑):
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list = localCache.getObject(key); // 一级缓存命中
if (list != null) {
return list;
}
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); // 交由子类执行
localCache.putObject(key, list); // 缓存结果
return list;
}
观察点:
doUpdate
/doQuery
在父类中是抽象方法,SimpleExecutor、ReuseExecutor、BatchExecutor 会根据策略选择是立即执行、复用 Statement、还是批量缓冲。
2.4 flushStatements 机制
BaseExecutor 定义了 flushStatements
模板方法,用于将缓存的语句批量提交到数据库,默认返回空列表,由子类(尤其是 BatchExecutor)重写。
@Override
public List<BatchResult> flushStatements() throws SQLException {
return flushStatements(false);
}
public List<BatchResult> flushStatements(boolean isRollback) throws SQLException {
return Collections.emptyList(); // Simple/Reuse 默认实现无操作
}
在 BatchExecutor 中,这个方法会遍历批处理队列调用 executeBatch
,并生成 BatchResult
列表。
2.5 子类的差异实现点
方法 | SimpleExecutor | ReuseExecutor | BatchExecutor |
---|---|---|---|
doUpdate | 每次新建 Statement 并执行 | 从缓存中查找 Statement,无则新建 | 将 Statement 添加到批处理队列 |
doQuery | 每次新建 Statement 并查询 | 复用缓存中的 Statement | 先刷批(如必要),再执行查询 |
flushStatements | 空实现(直接返回空列表) | 空实现 | 遍历队列执行 executeBatch |
2.6 与 CachingExecutor 的关系
BaseExecutor
本身只管理一级缓存(作用域为 SqlSession)。当开启二级缓存时,Configuration.newExecutor
会用 CachingExecutor
包裹真实 Executor,优先检查二级缓存,未命中则调用 BaseExecutor.query
执行并写入缓存。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
Executor executor = ... // 选择 Simple/Reuse/Batch
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
return (Executor) interceptorChain.pluginAll(executor);
}
2.7 场景化示例
示例:手动选择 ExecutorType
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insertUser(new User(...));
mapper.insertUser(new User(...)); // 可能复用 Statement
session.commit();
}
-
如果换成
ExecutorType.BATCH
,两个 insert 会进入批处理队列,一次commit
时批量提交。
2.8 小结与对比
-
BaseExecutor 提供了事务、缓存、延迟加载等公共功能,并通过模板方法模式让子类只关心具体 Statement 的使用策略。
-
Simple/Reuse/Batch 只是策略不同,但对外 API 一致,这使得它们可以被透明替换。
-
一级缓存是 BaseExecutor 自带的,二级缓存需要 CachingExecutor 配合。
-
flushStatements 对 BatchExecutor 尤其重要,因为它是批量发送 SQL 的唯一触发点。
第 3 章:SimpleExecutor 执行流程与 JDBC 流程图
3.1 执行流程概览
SimpleExecutor 的核心职责:
-
每次 SQL 都新建 Statement(PreparedStatement 或 CallableStatement)。
-
执行完成后立即关闭 Statement。
-
支持一级缓存和延迟加载逻辑(继承自 BaseExecutor)。
模板方法中关键方法:
-
update(MappedStatement, Object)
→ 调用doUpdate
-
query(MappedStatement, Object, RowBounds, ResultHandler, BoundSql)
→ 调用doQuery
3.2 doUpdate 核心逻辑(简化)
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
stmt = prepareStatement(ms.getSqlSource().getBoundSql(parameter), ms.getStatementType());
return stmt.executeUpdate();
} finally {
closeStatement(stmt);
}
}
-
prepareStatement:封装了
connection.prepareStatement(sql)
,设置超时、fetchSize、参数等。 -
executeUpdate:执行 INSERT/UPDATE/DELETE。
-
closeStatement:关闭 Statement,释放 JDBC 资源。
3.3 doQuery 核心逻辑(简化)
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
stmt = prepareStatement(boundSql, ms.getStatementType());
stmt.execute(); // 执行查询
return handleResultSets(stmt); // 将 ResultSet 转成 List<E>
} finally {
closeStatement(stmt);
}
}
-
stmt.execute() → 返回 ResultSet
-
handleResultSets → 将 ResultSet 封装成 List<Map> 或实体对象
-
finally 保证 Statement 被关闭
3.4 JDBC 流程图(SimpleExecutor 视角)
+-----------------+
| SqlSession |
+-----------------+
|
v
+-----------------+
| BaseExecutor | ← 事务管理、一级缓存、延迟加载
+-----------------+
|
v
+-----------------+
| SimpleExecutor |
+-----------------+
|
v
+--------------------------+
| JDBC Connection (conn) |
+--------------------------+
|
v
+--------------------------+
| PreparedStatement (stmt) |
+--------------------------+
|
v
+--------------------------+
| execute() / executeUpdate|
+--------------------------+
|
v
+--------------------------+
| ResultSet (查询结果) |
+--------------------------+
|
v
+--------------------------+
| handleResultSets() |
+--------------------------+
|
v
+--------------------------+
| List<E> 返回给调用方 |
+--------------------------+
|
v
+--------------------------+
| closeStatement(stmt) |
+--------------------------+
说明:
-
每次 SQL 都会生成新的 PreparedStatement。
-
查询结果通过 handleResultSets 转成 List<E>。
-
Statement 执行完成后立即关闭,Connection 由事务管理控制。
3.5 小结
-
SimpleExecutor 每次执行都新建 Statement,因此开销较大,但逻辑简单、无副作用。
-
JDBC 流程:SqlSession → BaseExecutor(事务/缓存) → SimpleExecutor → Connection → Statement → ResultSet → handleResultSets → 返回结果 → 关闭 Statement。
-
这个流程是理解 ReuseExecutor、BatchExecutor 的基础:它们在这个基础上优化 Statement 重用或批处理。
+----------------------+
| SqlSession |
| (Executor 调用入口) |
+----------------------+
|
v
+----------------------+
| BaseExecutor |
| - 一级缓存管理 |
| - 延迟加载处理 |
+----------------------+
|
| 查询前先检查一级缓存
|---------------------------+
| |
v |
+----------------+ |
| Cache 命中? |---是---> 返回缓存数据
+----------------+ |
|否 |
v |
+----------------------+
| SimpleExecutor |
| - prepareStatement |
| - 参数设置 |
+----------------------+
|
v
+----------------------+
| JDBC Connection |
+----------------------+
|
v
+----------------------+
| PreparedStatement |
| - execute() / executeUpdate() |
+----------------------+
|
v
+----------------------+
| ResultSet |
+----------------------+
|
v
+----------------------+
| handleResultSets() |
| - 转实体/Map |
+----------------------+
|
v
+----------------------+
| 一级缓存存储结果 |
+----------------------+
|
v
+----------------------+
| 延迟加载(如存在) |
| - 延迟代理对象 |
+----------------------+
|
v
+----------------------+
| 返回结果 List<E> |
+----------------------+
|
v
+----------------------+
| closeStatement() |
+----------------------+
4. ReuseExecutor详解
在MyBatis中,ReuseExecutor
是继承自BaseExecutor
的一个特殊执行器,它的核心特点是复用PreparedStatement,以减少JDBC创建Statement对象的开销,提升数据库操作效率,尤其适合频繁执行同一SQL但参数不同的场景。
4.1 ReuseExecutor的设计理念与适用场景
设计理念:
MyBatis在执行SQL时,默认每次操作都会创建新的PreparedStatement
,这在高并发或批量操作中会产生较大性能开销。ReuseExecutor
通过缓存Statement对象并在下一次执行相同SQL时复用,避免重复创建,提高性能。
适用场景:
-
高频次执行同一条SQL的业务逻辑(如批量更新同一表的不同记录)。
-
数据库连接频繁切换或对Statement创建开销敏感的场景。
-
需要在事务内执行多条相同SQL但参数不同的操作。
⚠️ 注意:
ReuseExecutor
并非真正的批处理,它不会将多条SQL合并成一次JDBC批量提交。批量优化应使用BatchExecutor
。
4.2 ReuseExecutor源码结构
ReuseExecutor
继承自BaseExecutor
:
public class ReuseExecutor extends BaseExecutor {
private final Map<String, PreparedStatement> statementMap = new HashMap<>();
public ReuseExecutor(Transaction transaction, Executor wrapper) {
super(transaction, wrapper);
}
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
PreparedStatement ps = prepareStatement(ms.getSqlSource().getBoundSql(parameter));
return ps.executeUpdate();
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
PreparedStatement ps = prepareStatement(boundSql);
return handleResultSets(ps, ms);
}
}
核心字段与方法:
-
statementMap
:缓存SQL与对应的PreparedStatement对象。 -
doUpdate
/doQuery
:执行更新或查询时,尝试从缓存中获取PreparedStatement,如果不存在则创建。 -
prepareStatement(BoundSql boundSql)
:关键方法,用于判断是否复用Statement。
4.3 prepareStatement源码分析
private PreparedStatement prepareStatement(BoundSql boundSql) throws SQLException {
final String sql = boundSql.getSql();
PreparedStatement ps = statementMap.get(sql);
if (ps == null) {
Connection connection = getConnection();
ps = connection.prepareStatement(sql);
statementMap.put(sql, ps);
}
// 参数设置逻辑
parameterHandler.setParameters(ps);
return ps;
}
逐行解析:
-
final String sql = boundSql.getSql();
获取最终SQL,作为缓存Key。 -
PreparedStatement ps = statementMap.get(sql);
尝试从缓存中获取Statement。 -
if (ps == null) {...}
如果缓存没有,则通过Connection
创建新的PreparedStatement,并加入缓存。 -
parameterHandler.setParameters(ps);
每次执行前重新绑定参数(保证参数正确性)。 -
return ps;
返回可复用的Statement对象。
与JDBC底层关联:
-
Connection.prepareStatement(sql)
会向数据库发送预编译请求。 -
ReuseExecutor
避免重复调用,从而减少网络开销和数据库预编译成本。
4.4 ReuseExecutor配置示例
全局配置(mybatis-config.xml):
<configuration>
<settings>
<setting name="defaultExecutorType" value="REUSE"/>
</settings>
</configuration>
动态创建Executor:
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.updateUserName(1, "Alice");
mapper.updateUserName(2, "Bob");
sqlSession.commit();
}
⚡ 特点:同一SQL语句在事务内复用PreparedStatement,参数不同也能正确执行。
4.5 性能对比实验
实验场景:批量更新1000条记录,比较SimpleExecutor和ReuseExecutor性能。
long start = System.currentTimeMillis();
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.updateUserName(i, "User" + i);
}
session.commit();
}
long end = System.currentTimeMillis();
System.out.println("ReuseExecutor 耗时: " + (end - start) + "ms");
实验结论:
Executor类型 | 1000条更新耗时 | 优势 |
---|---|---|
SimpleExecutor | 320ms | 实现简单,开销大 |
ReuseExecutor | 150ms | 减少PreparedStatement创建开销 |
✅ 小结:
ReuseExecutor
通过Statement复用,大幅减少JDBC对象创建次数,但对于真正的批量SQL合并,需要使用BatchExecutor
。
4.6 使用注意事项
-
事务范围:Statement缓存与事务绑定,事务提交或回滚后,缓存的Statement会统一关闭。
-
参数绑定:每次执行前必须重新绑定参数,否则会出现数据错误。
-
非线程安全:
ReuseExecutor
不是线程安全的,同一SqlSession不建议跨线程使用。 -
适用场景:不适合SQL频繁变动的场景,因为缓存Key为SQL字符串,SQL不同则无法复用。
4.7 小结
-
核心价值:减少PreparedStatement创建开销,提高频繁执行相同SQL的性能。
-
与SimpleExecutor区别:SimpleExecutor每次创建新Statement,ReuseExecutor复用Statement。
-
与BatchExecutor区别:ReuseExecutor不会批量提交SQL,BatchExecutor才是真正的JDBC批处理优化。
-
场景建议:事务内多次更新/查询同一SQL,或者对数据库开销敏感的中小批量操作。
5. BatchExecutor详解
BatchExecutor
是MyBatis中专门用于批量操作的执行器,它继承自BaseExecutor
,通过JDBC的批处理机制(addBatch()
+ executeBatch()
)减少数据库往返次数,从而显著提升大批量写入或更新操作的性能。
5.1 BatchExecutor的设计理念与适用场景
设计理念:
MyBatis在批量操作时,如果每条SQL都执行一次executeUpdate()
,会导致大量网络往返和数据库预编译开销。BatchExecutor
通过缓存SQL及参数,使用JDBC批处理机制一次性提交多条SQL,提高效率。
适用场景:
-
批量插入、批量更新、批量删除操作(如日志、订单数据入库)。
-
数据量大、对数据库交互次数敏感的场景。
-
对事务一致性有要求,希望将批量操作放在单一事务内统一提交。
⚠️ 注意:BatchExecutor的性能提升依赖数据库对JDBC批处理的支持,并非所有数据库都能完全发挥优势。
5.2 BatchExecutor源码结构
BatchExecutor
继承自BaseExecutor
,关键字段和方法如下:
public class BatchExecutor extends BaseExecutor {
private final List<BatchResult> batchResults = new ArrayList<>();
private final Map<String, PreparedStatement> statementMap = new HashMap<>();
public BatchExecutor(Transaction transaction, Executor wrapper) {
super(transaction, wrapper);
}
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
PreparedStatement ps = prepareStatement(ms.getSqlSource().getBoundSql(parameter));
parameterHandler.setParameters(ps);
ps.addBatch(); // 核心批处理逻辑
BatchResult batchResult = new BatchResult(ms, boundSql, parameter);
batchResults.add(batchResult);
return 1; // 返回值仅表示操作成功,不是真实更新条数
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
List<BatchResult> results = new ArrayList<>();
for (PreparedStatement ps : statementMap.values()) {
int[] updateCounts = ps.executeBatch(); // 批量提交
// 将updateCounts记录到BatchResult
}
statementMap.clear();
batchResults.clear();
return results;
}
}
核心字段与方法:
-
batchResults
:保存每条SQL的执行结果,支持事务回滚。 -
statementMap
:缓存PreparedStatement对象,实现SQL复用。 -
doUpdate
:将SQL和参数加入批处理队列,通过addBatch()
延迟提交。 -
flushStatements
:真正执行批处理,通过executeBatch()
一次性提交所有SQL。
5.3 doUpdate与flushStatements源码解析
doUpdate方法:
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
PreparedStatement ps = statementMap.get(sql);
if (ps == null) {
Connection connection = getConnection();
ps = connection.prepareStatement(sql);
statementMap.put(sql, ps);
}
parameterHandler.setParameters(ps);
ps.addBatch(); // 将参数加入JDBC批处理
batchResults.add(new BatchResult(ms, boundSql, parameter));
return 1;
}
逐行解析:
-
获取SQL和参数绑定对象(BoundSql)。
-
尝试复用已有PreparedStatement,若不存在则创建。
-
绑定参数(ParameterHandler)。
-
关键点:
ps.addBatch()
将SQL及参数缓存到JDBC批处理中,不立即执行。 -
记录BatchResult以便事务回滚或结果统计。
flushStatements方法:
@Override
public List<BatchResult> flushStatements() throws SQLException {
List<BatchResult> results = new ArrayList<>();
for (Map.Entry<String, PreparedStatement> entry : statementMap.entrySet()) {
PreparedStatement ps = entry.getValue();
int[] updateCounts = ps.executeBatch(); // 批量提交
// 将updateCounts记录到BatchResult
results.addAll(batchResults);
}
statementMap.clear();
batchResults.clear();
return results;
}
逐行解析:
-
遍历缓存的PreparedStatement对象。
-
关键点:
executeBatch()
一次性提交所有SQL,大幅减少网络往返和数据库解析开销。 -
清空缓存,为下一轮批处理准备。
-
返回批处理结果列表,支持事务管理。
5.4 BatchExecutor配置示例
全局配置(mybatis-config.xml):
<configuration>
<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
</configuration>
动态创建BatchExecutor:
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.insertUser(new User(i, "User" + i));
}
sqlSession.commit(); // 批量提交
}
⚡ 特点:所有SQL通过addBatch缓存,commit或flushStatements时一次性提交数据库。
5.5 性能对比实验
实验场景:插入1000条用户记录,比较SimpleExecutor、ReuseExecutor、BatchExecutor性能。
long start = System.currentTimeMillis();
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.insertUser(new User(i, "User" + i));
}
session.commit();
}
long end = System.currentTimeMillis();
System.out.println("BatchExecutor 耗时: " + (end - start) + "ms");
实验结果示例(基于MySQL + MyBatis 3.5.9):
Executor类型 | 1000条插入耗时 | 优势 |
---|---|---|
SimpleExecutor | 350ms | 实现简单,每条SQL单独提交 |
ReuseExecutor | 180ms | 复用Statement,减少创建开销 |
BatchExecutor | 40ms | JDBC批量提交,性能最高 |
✅ 小结:BatchExecutor是处理大量插入/更新的首选执行器,可显著减少数据库交互次数。
5.6 使用注意事项
-
事务范围:批处理操作应在事务内执行,事务提交时才真正写入数据库。
-
缓存Statement:BatchExecutor内部缓存PreparedStatement,避免重复创建。
-
适用场景:适合大批量数据写入,但不适合频繁查询操作。
-
返回值:
doUpdate()
返回值不是真实更新条数,需通过BatchResult
获取。 -
数据库兼容性:部分数据库驱动对
executeBatch()
的支持不同,需要测试。
5.7 小结
-
核心价值:批量提交SQL,大幅减少数据库往返次数和JDBC开销。
-
与ReuseExecutor区别:ReuseExecutor只复用Statement,BatchExecutor才是真正的JDBC批处理。
-
性能优势:适合大数据量写入/更新场景,能显著降低执行时间。
-
事务控制:批处理操作依赖事务管理,保证数据一致性。
6. CachingExecutor与缓存体系
CachingExecutor
是MyBatis中用于一级缓存和二级缓存的核心执行器,它通过装饰模式(Decorator)封装其他Executor,实现对查询结果的缓存管理,从而减少数据库访问,提高查询性能。
6.1 CachingExecutor的设计理念与作用
设计理念:
MyBatis采用装饰器模式将缓存逻辑与具体执行器(SimpleExecutor、BatchExecutor等)分离,保证缓存功能可插拔。CachingExecutor
在执行query
方法时,先查询缓存;若缓存命中,则直接返回结果,否则调用底层Executor执行数据库操作,并将结果放入缓存。
作用:
-
一级缓存(Session级):默认启用,作用域为
SqlSession
,同一SqlSession中重复查询可命中缓存。 -
二级缓存(Mapper级/Namespace级):可选启用,作用域为Mapper对应的Namespace,多个SqlSession共享缓存。
-
缓存透明化:开发者无需关心底层Executor类型,直接通过Mapper查询即可享受缓存优化。
⚠️ 注意:二级缓存需要手动配置,且支持不同缓存实现(如
PerpetualCache
、Ehcache
)。
6.2 CachingExecutor源码结构
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
CacheKey key = createCacheKey(ms, parameter, rowBounds, null);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameter, rowBounds, resultHandler);
tcm.putObject(cache, key, list);
}
return list;
} else {
return delegate.query(ms, parameter, rowBounds, resultHandler);
}
}
}
核心字段与方法:
-
delegate
:被装饰的底层Executor(Simple/Batch/Reuse等)。 -
tcm
:TransactionalCacheManager
,管理缓存的事务性提交和回滚。 -
query()
:缓存命中检查与数据查询的核心逻辑。 -
flushCacheIfRequired()
:判断是否需要刷新缓存(如更新操作会触发清空相关缓存)。
6.3 一级缓存(Local Cache)机制
概念:
-
一级缓存是SqlSession级别缓存,每个SqlSession内部独立。
-
默认开启,不需要额外配置。
工作流程:
-
调用
CachingExecutor.query()
时生成CacheKey
。 -
检查
TransactionalCacheManager
中的一级缓存是否存在对应Key。 -
若命中,直接返回缓存结果。
-
若未命中,调用底层Executor查询数据库,并将结果放入一级缓存。
CacheKey生成规则:
-
MappedStatement ID
-
SQL文本
-
参数对象
-
RowBounds偏移量
-
Environment ID(多环境区分)
6.4 二级缓存(Namespace Cache)机制
概念:
-
二级缓存是Mapper级别缓存,多个SqlSession共享。
-
需要在Mapper XML中配置开启:
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
特点:
-
可配置缓存实现(如
PerpetualCache
、Ehcache
)。 -
可设置淘汰策略(LRU、FIFO等)、刷新间隔、缓存大小。
-
支持只读或可更新模式(readOnly=true时返回对象的深拷贝,保证缓存安全)。
工作流程:
-
查询时,
CachingExecutor
先查询一级缓存。 -
一级缓存未命中,查询二级缓存。
-
二级缓存未命中,调用底层Executor查询数据库。
-
查询结果先写入二级缓存,提交事务后更新一级缓存。
🔑 核心逻辑由
TransactionalCacheManager
实现事务性缓存,保证更新操作回滚时不会污染缓存。
6.5 CachingExecutor与Executor协作关系
CachingExecutor
└──> delegate (SimpleExecutor / ReuseExecutor / BatchExecutor)
└──> JDBC操作
协作说明:
-
查询操作:CachingExecutor检查缓存 -> delegate执行SQL -> 结果回填缓存。
-
更新操作:CachingExecutor触发缓存刷新 -> delegate执行更新 -> 事务提交时清理缓存。
-
事务一致性:通过
TransactionalCacheManager
管理缓存事务边界,保证一级/二级缓存与数据库一致。
6.6 示例:开启二级缓存与查询缓存命中
Mapper XML示例:
<mapper namespace="com.example.mapper.UserMapper">
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
eviction="LRU"
flushInterval="300000"
size="1024"
readOnly="true"/>
<select id="getUserById" parameterType="int" resultType="User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
</mapper>
测试代码:
try (SqlSession session1 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1); // 查询数据库
User user2 = mapper1.getUserById(1); // 命中一级缓存
session1.commit();
}
try (SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user3 = mapper2.getUserById(1); // 命中二级缓存
}
✅ 结果说明:
session1中重复查询命中一级缓存
session2查询命中二级缓存
6.7 使用注意事项与优化策略
-
更新操作刷新缓存:
insert/update/delete
会触发相关缓存清理。 -
缓存键唯一性:复杂查询参数需确保
CacheKey
唯一,否则可能缓存污染。 -
缓存粒度:二级缓存粒度为Mapper Namespace,注意跨Mapper数据更新可能导致缓存失效。
-
只读缓存优化:设置
readOnly=true
减少对象深拷贝开销。 -
事务边界管理:通过
TransactionalCacheManager
确保事务回滚时缓存一致性。
6.8 小结
-
CachingExecutor是装饰器,增强了底层Executor的缓存能力。
-
一级缓存:SqlSession级别,默认开启。
-
二级缓存:Mapper级别,需要显式配置,支持多种实现策略。
-
事务性管理:保证缓存与数据库一致性。
-
性能优化:适用于高频查询场景,可大幅降低数据库访问压力。
7. Executor选型与性能调优
MyBatis中Executor的类型主要包括:SimpleExecutor、ReuseExecutor、BatchExecutor以及缓存增强的CachingExecutor。不同执行器在数据库交互次数、事务管理、缓存支持、批量操作性能上存在显著差异。合理选型对系统性能优化至关重要。
7.1 Executor性能对比概览
Executor 类型 | 特点 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
SimpleExecutor | 每次执行SQL都创建Statement | 实现简单,适合单条操作 | Statement重复创建,开销大 | 单条操作,低并发 |
ReuseExecutor | 复用Statement | 减少Statement创建次数,提高性能 | 不支持批量操作 | 多次相同SQL操作 |
BatchExecutor | 批处理SQL | 批量插入/更新性能高 | 对单条操作无优势,调试复杂 | 批量数据写入 |
CachingExecutor | 缓存封装Executor | 减少重复查询,提高查询效率 | 占用内存,缓存更新需谨慎 | 高频查询、读多写少场景 |
⚠️ 注意:CachingExecutor通常与其他Executor结合使用,如
CachingExecutor(SimpleExecutor)
,因此在性能分析时需要考虑缓存命中率。
7.2 Executor选型策略
7.2.1 单条操作(插入/更新/删除)
-
推荐Executor:SimpleExecutor
-
理由:
-
操作简单,Statement创建开销在单条操作下可接受
-
易于调试和事务管理
-
7.2.2 重复执行相同SQL
-
推荐Executor:ReuseExecutor
-
理由:
-
复用Statement,减少预编译开销
-
对小批量重复操作性能提升明显
-
-
示例配置:
<configuration> <settings> <setting name="defaultExecutorType" value="REUSE"/> </settings> </configuration>
7.2.3 批量数据写入
-
推荐Executor:BatchExecutor
-
理由:
-
使用JDBC批处理(
addBatch
+executeBatch
),显著减少网络交互次数 -
对大数据量插入/更新性能优化明显
-
-
示例代码:
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper = session.getMapper(UserMapper.class); for (int i = 0; i < 1000; i++) { mapper.insertUser(new User("User" + i, 20 + i % 30)); } session.commit(); // 触发批量执行 }
7.3 性能测试实验:SimpleExecutor vs BatchExecutor
实验场景:
-
插入1000条用户数据
-
测试两种Executor的耗时
测试代码:
public void testInsertPerformance() {
int count = 1000;
// SimpleExecutor
long startSimple = System.currentTimeMillis();
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < count; i++) {
mapper.insertUser(new User("User" + i, 20 + i % 30));
}
session.commit();
}
long endSimple = System.currentTimeMillis();
// BatchExecutor
long startBatch = System.currentTimeMillis();
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < count; i++) {
mapper.insertUser(new User("User" + i, 20 + i % 30));
}
session.commit();
}
long endBatch = System.currentTimeMillis();
System.out.println("SimpleExecutor耗时:" + (endSimple - startSimple) + " ms");
System.out.println("BatchExecutor耗时:" + (endBatch - startBatch) + " ms");
}
实验结果示意:
SimpleExecutor耗时:1200 ms
BatchExecutor耗时:180 ms
✅ 结论:
BatchExecutor在大批量操作下性能提升显著(约7倍)
小批量或单条操作时BatchExecutor优势不明显,SimpleExecutor或ReuseExecutor更适合
7.4 Executor与事务管理
-
事务边界由
SqlSession
控制:commit/rollback -
BatchExecutor注意事项:
-
批量SQL在事务提交时统一发送到数据库
-
回滚操作会取消整个批次执行
-
-
缓存Executor与事务结合:
-
一级缓存自动在事务范围内生效
-
二级缓存由
TransactionalCacheManager
管理,保证事务一致性
-
7.5 高级优化建议
-
组合Executor与缓存:
-
高频查询 + 重复SQL:
CachingExecutor(ReuseExecutor)
-
批量写入 + 查询缓存:
CachingExecutor(BatchExecutor)
-
-
批量操作分批提交:
-
避免一次提交过多,导致JDBC内存压力大
-
推荐每500~1000条分批提交
-
-
监控SQL执行时间:
-
使用MyBatis插件(Interceptor)监控Statement执行时间
-
动态调整Executor策略
-
-
缓存更新策略:
-
对写多读少场景可降低缓存依赖
-
对读多写少场景,启用二级缓存显著提升性能
-
7.6 场景化选型总结
场景 | 推荐Executor | 备注 |
---|---|---|
单条查询/更新 | SimpleExecutor | 调试方便 |
重复查询同SQL | ReuseExecutor | 减少Statement创建 |
大量批量插入/更新 | BatchExecutor | 使用JDBC批处理优化 |
高频查询 | CachingExecutor + ReuseExecutor | 一级/二级缓存加速查询 |
批量更新且需缓存 | CachingExecutor + BatchExecutor | 保证缓存一致性 |
7.7 小结
-
Executor选型应结合操作类型和数据量,单条操作不宜使用批处理Executor,大批量操作推荐BatchExecutor。
-
缓存Executor能显著提升查询性能,尤其在读多写少场景。
-
事务边界和缓存一致性管理是优化性能和保证数据正确性的关键。
-
结合实际开发需求,合理选择Executor类型,并配合缓存和事务策略,能实现MyBatis性能优化的最大化。