一、mybatis都有哪些executor执行器?
第一步:如果没有多种执行器,会面临什么问题?
假设mybatis只有一个执行器,它的职责是:执行sql、管理缓存、处理批量操作。
那么代码会变成这样:
// 伪代码:万能执行器
public class onlyexecutor {
public <e> list<e> query(mappedstatement ms, object parameter) {
// 1. 查二级缓存
// 2. 查一级缓存
// 3. 创建statement
// 4. 设置参数
// 5. 执行sql
// 6. 处理结果映射
// 7. 如果是批量模式,还要攒sql...
// 8. 如果要复用statement,还要管理statement池...
}
}弊端:
- 代码臃肿,所有逻辑耦合在一起
- 批量操作的逻辑和简单查询的逻辑混在一起,互相干扰
- 缓存逻辑和statement管理逻辑混在一起,难以维护
- 用户无法根据场景选择最合适的执行策略
所以mybatis把"执行sql"这件事拆成了多个执行器,每个执行器只负责一种策略。
第二步:executor 的继承体系
先给你一张全景图:
executor (接口)
│
├── baseexecutor (抽象类)
│ │
│ ├── simpleexecutor (简单执行器,默认)
│ ├── reuseexecutor (复用执行器)
│ └── batchexecutor (批量执行器)
│
└── cachingexecutor (缓存装饰器)关键设计:
- baseexecutor 提取了公共逻辑(一级缓存、事务管理)
- 三个具体执行器(simple、reuse、batch)继承 baseexecutor,各自实现不同的statement策略
- cachingexecutor 不继承 baseexecutor,它用装饰器模式包装其他执行器,负责二级缓存
第三步:baseexecutor —— 为什么要有抽象基类?
在讲具体执行器之前,先看 baseexecutor 解决了什么问题。
三个具体执行器(simple、reuse、batch)都需要做这些事:
- 管理一级缓存
- 管理事务(commit/rollback/close)
- 提取公共的查询/更新流程
如果没有 baseexecutor,这三个类里会写大量重复代码。
public abstract class baseexecutor implements executor {
protected transaction transaction;
protected perpetualcache localcache; // 一级缓存
// 公共逻辑:查一级缓存
public <e> list<e> query(mappedstatement ms, object parameter, ...) {
// 1. 生成缓存key
cachekey key = createcachekey(ms, parameter, ...);
// 2. 查一级缓存
list<e> list = localcache.getobject(key);
if (list != null) return list;
// 3. 缓存未命中,调用子类实现的具体查询逻辑
return queryfromdatabase(ms, parameter, key, ...);
}
// 抽象方法:子类各自实现
protected abstract int doupdate(mappedstatement ms, object parameter);
protected abstract <e> list<e> doquery(mappedstatement ms, object parameter, ...);
protected abstract list<batchresult> doflushstatements(boolean isrollback);
}baseexecutor 的职责:
- 统一实现一级缓存逻辑
- 统一事务管理
- 定义执行流程的骨架,把具体如何创建/使用statement交给子类
第四步:simpleexecutor —— 最朴素的执行方式
没有它之前的弊端
如果没有 simpleexecutor,框架不知道"默认该怎么执行sql"。
它的策略
每次执行sql,都创建一个新的statement,执行完就关闭。
public class simpleexecutor extends baseexecutor {
@override
public <e> list<e> doquery(mappedstatement ms, object parameter, ...) {
statement stmt = null;
try {
// 1. 获取连接
connection connection = transaction.getconnection();
// 2. 创建新的statement
stmt = handler.prepare(connection, ...);
// 3. 设置参数
handler.parameterize(stmt, ...);
// 4. 执行
return handler.query(stmt, ...);
} finally {
// 5. 关闭statement
closestatement(stmt);
}
}
}特点:
- 简单、安全、无状态
- 每次执行都是独立的,互不影响
- 但频繁创建/关闭statement有性能开销
适用场景: 绝大多数普通查询,是mybatis的默认执行器。
第五步:reuseexecutor —— statement复用,解决什么问题?
没有它之前的弊端
simpleexecutor 每次查询都创建新的 preparedstatement。如果你的代码在一个事务里执行多条相同sql(只是参数不同):
session.selectone("selectbyid", 1);
session.selectone("selectbyid", 2);
session.selectone("selectbyid", 3);simpleexecutor 会创建3个 preparedstatement,执行完都关闭。但它们的sql模板是一样的:select * from user where id = ?
创建 preparedstatement 是有成本的,数据库需要解析sql、生成执行计划。对于相同的sql模板,重复创建是一种浪费。
它的策略
将相同sql的statement缓存起来,下次复用。
public class reuseexecutor extends baseexecutor {
// key: sql字符串 value: statement对象
private final map<string, statement> statementmap = new hashmap<>();
@override
public <e> list<e> doquery(mappedstatement ms, object parameter, ...) {
// 1. 获取sql
boundsql boundsql = ms.getboundsql(parameter);
string sql = boundsql.getsql();
// 2. 看看缓存里有没有这个sql对应的statement
statement stmt = statementmap.get(sql);
if (stmt == null) {
// 3. 没有,创建新的
connection connection = transaction.getconnection();
stmt = handler.prepare(connection, ...);
// 4. 放入缓存
statementmap.put(sql, stmt);
}
// 5. 设置新参数(复用同一个statement,但参数不同)
handler.parameterize(stmt, ...);
// 6. 执行
return handler.query(stmt, ...);
// 注意:这里不关闭statement!
}
@override
public list<batchresult> doflushstatements(boolean isrollback) {
// 事务提交或回滚时,统一关闭所有缓存的statement
for (statement stmt : statementmap.values()) {
closestatement(stmt);
}
statementmap.clear();
return null;
}
}特点:
- 同一个事务内,相同sql模板只创建一次statement
- 通过
setxxx()更换参数,重复执行 - 事务结束时才统一关闭
适用场景: 一个事务内需要反复执行同一条sql(不同参数),比如批量查询。
第六步:batchexecutor —— 批量操作,解决什么问题?
没有它之前的弊端
假设你要插入1000条数据,用 simpleexecutor:
for (user user : userlist) {
session.insert("insertuser", user);
}simpleexecutor 每次 insert 都会:
- 创建statement
- 设置参数
- 立即执行sql(发一次网络请求到数据库)
- 关闭statement
1000次循环 = 1000次网络往返 = 性能灾难。
jdbc的批量api
jdbc其实提供了批量操作的能力:
preparedstatement ps = conn.preparestatement("insert into user values (?, ?)");
ps.setstring(1, "张三");
ps.setstring(2, "20");
ps.addbatch(); // 添加到批次,不执行!
ps.setstring(1, "李四");
ps.setstring(2, "25");
ps.addbatch();
ps.executebatch(); // 一次性发送所有sql到数据库这样可以大幅减少网络往返次数。
batchexecutor的策略
攒sql,不立即执行,等到统一提交时才批量发送。
public class batchexecutor extends baseexecutor {
// 缓存的statement列表
private final list<statement> statementlist = new arraylist<>();
// 每个statement对应的参数列表
private final list<list<object>> parameterobjectlist = new arraylist<>();
// 当前正在使用的statement
private string currentsql;
@override
public int doupdate(mappedstatement ms, object parameter) {
boundsql boundsql = ms.getboundsql(parameter);
string sql = boundsql.getsql();
statement stmt;
if (sql.equals(currentsql) && !statementlist.isempty()) {
// 和上一个sql相同,复用statement
stmt = statementlist.get(statementlist.size() - 1);
} else {
// sql变了,创建新的statement
connection connection = transaction.getconnection();
stmt = handler.prepare(connection, ...);
statementlist.add(stmt);
parameterobjectlist.add(new arraylist<>());
currentsql = sql;
}
// 添加参数到批次
handler.parameterize(stmt, ...);
// 调用jdbc的addbatch()
stmt.addbatch();
// 注意:这里不执行!返回一个无意义的值
return batch_update_return_value;
}
@override
public list<batchresult> doflushstatements(boolean isrollback) {
list<batchresult> results = new arraylist<>();
for (int i = 0; i < statementlist.size(); i++) {
statement stmt = statementlist.get(i);
// 调用jdbc的executebatch(),真正执行
int[] updatecounts = stmt.executebatch();
results.add(new batchresult(stmt, updatecounts));
}
return results;
}
}特点:
update操作不立即执行,而是addbatch()攒起来- 查询操作会先刷新批次(把之前攒的update先执行),再执行查询
- 事务提交时统一
executebatch()
适用场景: 大批量插入、更新、删除。
重要限制: batchexecutor 只支持相同sql模板的批量操作。如果sql模板不同(比如一条insert、一条update混着来),它会创建多个statement,每个statement内部批量。
第七步:cachingexecutor —— 为什么它是装饰器?
设计问题
二级缓存应该放在哪里?
- 如果放在
baseexecutor里,那么所有子类都强制有二级缓存,无法关闭。 - 如果放在某个子类里,那每个子类都要重复实现缓存逻辑。
装饰器模式
mybatis用装饰器模式解决:
public class cachingexecutor implements executor {
private final executor delegate; // 被装饰的执行器(simple/reuse/batch)
public cachingexecutor(executor delegate) {
this.delegate = delegate;
}
@override
public <e> list<e> query(mappedstatement ms, object parameter, ...) {
cache cache = ms.getcache();
if (cache == null) {
// 没开二级缓存,直接交给被装饰的执行器
return delegate.query(ms, parameter, ...);
}
// 查二级缓存
cachekey key = createcachekey(ms, parameter, ...);
list<e> list = (list<e>) tcm.getobject(cache, key);
if (list != null) return list;
// 二级缓存未命中,交给被装饰的执行器(它会查一级缓存→数据库)
list = delegate.query(ms, parameter, ...);
// 暂存到二级缓存
tcm.putobject(cache, key, list);
return list;
}
}关系图:
cachingexecutor (包装者)
└── delegate: simpleexecutor (被包装者)
└── 继承 baseexecutor (一级缓存)为什么这样设计?
- cachingexecutor 只负责二级缓存
- 真正的sql执行交给内部的 delegate
- 二级缓存和具体执行策略(simple/reuse/batch)完全解耦
- 你可以自由组合:cachingexecutor 包 simpleexecutor、cachingexecutor 包 batchexecutor...
第八步:执行器的选择与配置
方式1、全局配置(mybatis-config.xml)
<settings>
<!-- 默认执行器类型:simple / reuse / batch -->
<setting name="defaultexecutortype" value="simple"/>
</settings>方式2、代码中指定
// 打开session时指定执行器 sqlsession session = factory.opensession(executortype.batch);
【注意】:
mybatis 默认永远使用 simpleexecutor,除非你手动指定。
三种执行器对比
| 特性 | simpleexecutor | reuseexecutor | batchexecutor |
|---|---|---|---|
| statement策略 | 每次创建,用完关闭 | 缓存复用,事务结束关闭 | 攒批次,统一执行 |
| 一级缓存 | 有 | 有 | 有 |
| 二级缓存 | 有(需配合cachingexecutor) | 有 | 有 |
| 适用场景 | 普通crud(默认) | 同sql反复执行 | 大批量写入 |
| 注意事项 | 无特殊要求 | 事务内有效 | 查询会触发flush |
第九步:完整的执行流程图
用户调用 sqlsession.selectone()
│
▼
cachingexecutor.query() ← 如果有二级缓存配置
│
├── 查二级缓存 ──→ 命中?→ 直接返回
│
└── 未命中 → delegate.query()
│
▼
baseexecutor.query()
│
├── 查一级缓存 ──→ 命中?→ 直接返回
│
└── 未命中 → doquery() [子类实现]
│
┌────────┼────────┐
▼ ▼ ▼
simple reuse batch
│ │ │
▼ ▼ ▼
新建stmt 复用stmt addbatch()
│ │ │
▼ ▼ ▼
执行sql 执行sql 攒着不执行
│ │ │
▼ ▼ ▼
结果映射 结果映射 commit时executebatch()
│ │
▼ ▼
放入一级缓存
│
▼
返回结果总结:逻辑链条
- 因为 不同的业务场景对sql执行策略有不同需求(普通查询、statement复用、批量操作),如果只有一个执行器,代码会臃肿且无法按需选择;
- 所以 mybatis设计了
executor接口,并提取baseexecutor抽象基类,封装一级缓存和事务管理等公共逻辑; - 因为 大多数场景只需要简单执行,每次创建新statement最安全;
- 所以 有了
simpleexecutor,它是默认执行器,无状态、最简单; - 因为 同一个事务内反复执行同一条sql时,重复创建statement有性能开销;
- 所以 有了
reuseexecutor,它缓存statement对象,通过更换参数复用执行; - 因为 大批量写入时,逐条发送sql会造成大量网络往返,性能极差;
- 所以 有了
batchexecutor,它利用jdbc的addbatch()机制攒sql,统一批量执行; - 因为 二级缓存不应该和具体执行策略耦合,且需要可插拔;
- 所以 有了
cachingexecutor,它用装饰器模式包装其他执行器,只负责二级缓存,与底层执行策略完全解耦。
一句话概括: simpleexecutor 解决"怎么执行", reuseexecutor 解决"重复执行怎么优化", batchexecutor 解决"批量执行怎么优化", cachingexecutor 解决"跨session缓存怎么实现", baseexecutor 解决"它们共同需要什么"。
二、excutor这些特点的生命周期
mybatis 的 executor(执行器)里面保存的东西,它的有效范围只有当前 sqlsession。
1. 先搞清楚 sqlsession 和 executor 的关系
mybatis 执行 sql 的流程:
应用程序 | | sqlsession | | executor | | statement | | 数据库
也就是说:
一个 sqlsession 内部包含一个 executor。
例如:
sqlsession sqlsession = sqlsessionfactory.opensession(); usermapper mapper = sqlsession.getmapper(usermapper.class); mapper.insert(user); sqlsession.commit(); sqlsession.close();
创建 sqlsession 的时候:
mybatis 会创建 executor:
sqlsession
|
|
executor
|
|
一级缓存
statement缓存
batch任务2. 为什么说 "严格限制在 sqlsession 生命周期范围内"?
因为:
executor 属于 sqlsession。
sqlsession 创建:
10:00
opensession()
创建executor
一级缓存空
batch队列空然后:
10:01
查询用户id=1
缓存:
{
id=1 用户张三
}10:02:
再次查询:直接从executor一级缓存取
但是:
如果:
sqlsession.close();
发生:
sqlsession死亡
↓
executor释放
↓
一级缓存消失
↓
batch任务消失所以:
新的 sqlsession:
sqlsession sqlsession2 =
sqlsessionfactory.opensession();里面:
新的executor | | 一级缓存为空
再次查询:
还是访问数据库。
3. 举一个一级缓存最明显的例子
代码:
sqlsession session =
sqlsessionfactory.opensession();
usermapper mapper =
session.getmapper(usermapper.class);
// 第一次查询
user u1 = mapper.selectbyid(1);
// 第二次查询
user u2 = mapper.selectbyid(1);执行:
第一次:
executor一级缓存为空 ↓ 查询数据库 ↓ 结果放入缓存
第二次:
executor一级缓存 ↓ 找到 ↓ 不访问数据库
但是:
如果:
session.close(); sqlsession session2 = sqlsessionfactory.opensession(); usermapper mapper2 = session2.getmapper(usermapper.class); mapper2.selectbyid(1);
执行:
新的executor 一级缓存为空 ↓ 重新sql查询
4. 面试回答版本
如果面试官问:
mybatis 一级缓存为什么叫 sqlsession 级别?
可以回答:
因为一级缓存实际上存储在 executor 中,而 executor 是由 sqlsession 创建并管理的,所以一级缓存的生命周期和 sqlsession 一致。
sqlsession关闭后,executor销毁,缓存也随之失效。
sqlsession
|
|
executor
|
|
--------------------------------
| |
一级缓存 batch队列
| |
查询缓存 addbatch()这些东西不是 mybatis 全局保存的,而是每一个 sqlsession 自己的一套执行环境。这也是为什么 mybatis 默认一级缓存不能跨 sqlsession 共享。
到此这篇关于mybatis中executor执行器的使用的文章就介绍到这了,更多相关mybatis executor执行器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论