Mybatis 都有哪些 Executor 执行器?
结论先行:MyBatis 有四种执行器——SimpleExecutor(默认,每次新建 Statement)、ReuseExecutor(按 SQL 复用 Statement)、BatchExecutor(攒批执行)、以及装饰性质的 CachingExecutor(二级缓存)。它们都继承 BaseExecutor,共用一级缓存与事务逻辑,差异集中在 doQuery / doUpdate / doFlushStatements 三个模板方法。
1. 继承结构
Executor(接口)
▲
┌──────────┴───────────┐
BaseExecutor(模板,一级缓存) CachingExecutor(装饰器,二级缓存)
▲
┌──────┼──────────┬──────────────┐
SimpleExecutor ReuseExecutor BatchExecutor ClosedExecutor(内部占位)
CachingExecutor 不是 BaseExecutor 的子类,它持有另一个 Executor 并在外层做二级缓存判断,是典型的装饰器模式。
2. 四种执行器对比
| 执行器 | Statement 策略 | 适用场景 | 注意点 |
|---|---|---|---|
SimpleExecutor | 每次执行新建 PreparedStatement,用完即关 | 绝大多数场景(默认) | 无状态,最安全 |
ReuseExecutor | 以 SQL 为 key 缓存 Statement,同 SQL 复用 | 同一会话内反复执行相同 SQL | 缓存不跨会话;Statement 未关前不能改结果集 |
BatchExecutor | 缓存 Statement,addBatch 攒批,flush 时 executeBatch | 批量插入/更新 | 查询会强制 flush;自增主键要 flush 后才有值 |
CachingExecutor | 不直接执行,先查二级缓存 | 开启 <cache/> 时自动包装 | 只在 Executor 外层,不单独使用 |
3. 怎么指定
// 方式一:openSession 指定
try (SqlSession session = factory.openSession(ExecutorType.BATCH)) {
OrderMapper mapper = session.getMapper(OrderMapper.class);
for (Order o : orders) {
mapper.insert(o);
}
session.flushStatements();
session.commit();
}
// 方式二:全局默认执行器(mybatis-config.xml)
// <setting name="defaultExecutorType" value="REUSE"/>
mybatis:
executor-type: reuse # Spring Boot 配置项,等价于 defaultExecutorType
Spring 中若要某个 Mapper 走批量,通常单独定义一个 SqlSessionTemplate,而不是把全局执行器改成 BATCH——全局 BATCH 会影响所有查询,且容易忘记 flush。
4. BaseExecutor 的模板方法
// 简化后的骨架
public abstract class BaseExecutor implements Executor {
protected PerpetualCache localCache; // 一级缓存
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rb, ResultHandler rh) {
if (queryStack == 0 && ms.isUseCache()) {
list = localCache.getObject(key); // 命中直接返回
if (list != null) return list;
}
if (closed) throw new ExecutorException("Executor was closed.");
queryStack++;
try {
list = doQuery(ms, parameter, rb, rh); // 子类实现
} finally {
queryStack--;
}
if (queryStack == 0 && ms.isUseCache()) {
localCache.putObject(key, list);
}
return list;
}
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter,
RowBounds rb, ResultHandler rh);
}
doQuery 的差异就是四种执行器的全部差异:
SimpleExecutor:newStatementHandler→prepare→parameterize→query,最后closeStatement。ReuseExecutor:先从statementMap按 SQL 找 Statement,找不到才新建。BatchExecutor:把Statement放进statementList,攒够了一起executeBatch。
5. 为什么查询会打断批处理
BatchExecutor.doQuery 的第一行就是:
public <E> List<E> doQuery(...) throws SQLException {
flushStatements(); // 查询前必须先刷批,否则读不到未提交的批次
...
}
所以「边批量插入边查询」会让批次不断被切碎,性能反而下降。正确做法是把批处理与查询分成两个阶段。
6. 常见追问
追问一:CachingExecutor 和 <cache/> 是什么关系?
<cache/> 只是声明「这个 namespace 要二级缓存」,真正的缓存读写由 CachingExecutor 完成。它在 query 里查 TransactionalCacheManager,提交时才把结果刷进 Cache。
追问二:ReuseExecutor 能跨会话复用 Statement 吗?
不能。Statement 绑定在 Connection 上,SqlSession 关闭时连接归还连接池,Statement 也随之关闭。复用只在同一个会话内有效。
追问三:BatchExecutor 能拿到自增 ID 吗?
flush 之后能。executeBatch 后驱动通过 getGeneratedKeys 返回主键,MyBatis 在 Jdbc3KeyGenerator 里回填。flush 前读 id 是 null。
追问四:ExecutorType.BATCH 下 insert 返回什么?
返回 BATCH_UPDATE_RETURN_VALUE(一个负数常量)或 0,不是真实影响行数——因为语句还没执行。要看行数只能等 flushStatements() 的结果。
追问五:如何自定义执行器?
实现 Executor 接口或用 Configuration.newExecutor 的 ExecutorType 分支扩展,再通过插件包装。实际项目很少自定义,更多是用插件拦截 Executor.query/update。
小结
记法很简单:Simple 是「一次一关」,Reuse 是「同 SQL 复用」,Batch 是「攒够再发」,Caching 是「套在它们外面的二级缓存壳」。日常默认 Simple 足够,批量写入时显式开 Batch,并记住查询会强制 flush。