
一、Mybatis都有哪些Executor执行器第一步如果没有多种执行器会面临什么问题假设MyBatis只有一个执行器它的职责是执行SQL、管理缓存、处理批量操作。那么代码会变成这样// 伪代码万能执行器 public class OnlyExecutor { public lt;Egt; Listlt;Egt; 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 lt;Egt; Listlt;Egt; query(MappedStatement ms, Object parameter, ...) { // 1. 生成缓存Key CacheKey key createCacheKey(ms, parameter, ...); // 2. 查一级缓存 Listlt;Egt; 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 lt;Egt; Listlt;Egt; doQuery(MappedStatement ms, Object parameter, ...); protected abstract Listlt;BatchResultgt; doFlushStatements(boolean isRollback); }BaseExecutor的职责统一实现一级缓存逻辑统一事务管理定义执行流程的骨架把具体如何创建/使用Statement交给子类第四步SimpleExecutor —— 最朴素的执行方式没有它之前的弊端如果没有SimpleExecutor框架不知道默认该怎么执行SQL。它的策略每次执行SQL都创建一个新的Statement执行完就关闭。public class SimpleExecutor extends BaseExecutor { Override public lt;Egt; Listlt;Egt; 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 MapString, Statement statementMap new HashMap(); Override public lt;Egt; Listlt;Egt; 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 Listlt;BatchResultgt; doFlushStatements(boolean isRollback) { // 事务提交或回滚时统一关闭所有缓存的Statement for (Statement stmt : statementMap.values()) { closeStatement(stmt); } statementMap.clear(); return null; } }特点同一个事务内相同SQL模板只创建一次Statement通过setXxx()更换参数重复执行事务结束时才统一关闭适用场景一个事务内需要反复执行同一条SQL不同参数比如批量查询。第六步BatchExecutor —— 批量操作解决什么问题没有它之前的弊端假设你要插入1000条数据用SimpleExecutorfor (User user : userList) { session.insert(insertUser, user); }SimpleExecutor每次insert都会创建Statement设置参数立即执行SQL发一次网络请求到数据库关闭Statement1000次循环 1000次网络往返 性能灾难。JDBC的批量APIJDBC其实提供了批量操作的能力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 ListStatement statementList new ArrayList(); // 每个Statement对应的参数列表 private final ListListObject 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) amp;amp; !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 ArrayListlt;gt;()); currentSql sql; } // 添加参数到批次 handler.parameterize(stmt, ...); // 调用JDBC的addBatch() stmt.addBatch(); // 注意这里不执行返回一个无意义的值 return BATCH_UPDATE_RETURN_VALUE; } Override public Listlt;BatchResultgt; doFlushStatements(boolean isRollback) { Listlt;BatchResultgt; results new ArrayListlt;gt;(); for (int i 0; i lt; 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 lt;Egt; Listlt;Egt; query(MappedStatement ms, Object parameter, ...) { Cache cache ms.getCache(); if (cache null) { // 没开二级缓存直接交给被装饰的执行器 return delegate.query(ms, parameter, ...); } // 查二级缓存 CacheKey key createCacheKey(ms, parameter, ...); Listlt;Egt; list (Listlt;Egt;) 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.xmlsettings !-- 默认执行器类型SIMPLE / REUSE / BATCH -- setting namedefaultExecutorType valueSIMPLE/ /settings方式2、代码中指定// 打开Session时指定执行器 SqlSession session factory.openSession(ExecutorType.BATCH);【注意】MyBatis 默认永远使用SimpleExecutor除非你手动指定。三种执行器对比特性SimpleExecutorReuseExecutorBatchExecutorStatement策略每次创建用完关闭缓存复用事务结束关闭攒批次统一执行一级缓存有有有二级缓存有需配合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 会创建 ExecutorSqlSession | | Executor | | 一级缓存 Statement缓存 Batch任务2. 为什么说 严格限制在 SqlSession 生命周期范围内因为Executor 属于 SqlSession。SqlSession 创建10:00 openSession() 创建Executor 一级缓存空 Batch队列空然后10:01 查询用户id1 缓存: { id1 用户张三 }10:02再次查询直接从Executor一级缓存取但是如果sqlSession.close();发生SqlSession死亡 ↓ Executor释放 ↓ 一级缓存消失 ↓ Batch任务消失所以新的 SqlSessionSqlSession 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 共享。