在《Mybatis入门到精通 一》中,我们了解了Mybatis启动过程:配置解析、创建SqlSession。本文介绍了SQL执行引擎Executor,以及和Executor密切相关的一二级缓存实现。
注:本文中源码来自mybatis 3.4.x版本,地址github.com/mybatis/myb...
一 Executor接口
1.1 作用
Executor接口中,是MyBatis的SQL执行引擎,通过精巧的设计模式组合,实现了SQL执行的高效协调和灵活扩展。该接口有以下关键职责:
- SQL执行协调
java
// 查询操作
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);
// 更新操作
int update(MappedStatement ms, Object parameter);
- 缓存管理
java
// 一级缓存管理
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
- 事务控制
java
void commit(boolean required);
void rollback(boolean required);
Transaction getTransaction();
- 延迟加载
java
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
1.2 接口体系

BaseExecutor是顶级父抽象类,用模版方法实现公共逻辑,如维护一级缓存。
java
public abstract class BaseExecutor implements Executor {
// 模板方法:定义执行流程
public <E> List<E> query(...) {
// 1. 检查一级缓存
list = localCache.getObject(key);
if (list != null) {
return list;
}
// 2. 从数据库查询
return queryFromDatabase(...);
}
// 抽象方法:子类实现具体策略
protected abstract <E> List<E> doQuery(...);
}
使用策略模式,BaseExecutor有以下实现类
- SimpleExecutor,每次执行完都关闭Statement
java
public int doUpdate(MappedStatement ms, Object parameter) {
Statement stmt = null;
try {
StatementHandler handler = configuration.newStatementHandler(...);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
// 每次都关闭Statement
closeStatement(stmt);
}
}
- ReuseExecutor ,缓存Statement对象,实现重用
- BatchExecutor,批量执行,提升性能
CachingExecutor采用了装饰器模式,在BaseExecutor基础上,提供了二级缓存能力。
java
public class CachingExecutor implements Executor {
// 被装饰的BaseExecutor子类对象
private final Executor delegate;
public <E> List<E> query(...) {
Cache cache = ms.getCache();
if (cache != null) {
// 二级缓存逻辑
List<E> list = tcm.getObject(cache, key);
// 未命中缓存,查询数据库并放入缓存池
if (list == null) {
list = delegate.query(...); // 委托给原执行器
tcm.putObject(cache, key, list);
}
return list;
}
//未启用二级缓存,直接查询数据库
return delegate.query(...);
}
}
1.3 创建Executor
Configuration#newExecutor方法会根据executorType来创建相应的实现类对象,并使用插件增强。
默认得executorType = ExecutorType.SIMPLE,即使用SimpleExecutor。
java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 二级缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 代理增强
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
二 缓存体系


2.1 一级缓存
2.1.1 特点和启用
一级缓存有如下特点:
- 作用域:SqlSession级别,不同Session不共享
- 生命周期:SqlSession创建到关闭
- 存储结构:HashMap(PerpetualCache)
- 默认状态:始终开启,无法关闭
- 清理时机:commit、rollback、update操作
一级缓存默认开启,默认作用域是SESSION。 启用方式如下:
xml
<!-- mybatis-config.xml -->
<settings>
<!-- SESSION: 会话级别(默认) -->
<!-- STATEMENT: 语句级别(执行完立即清空) -->
<setting name="localCacheScope" value="SESSION"/>
</settings>
来看一个命中一级缓存的示例
java
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,走数据库
User user1 = mapper.selectById(1);
// 第二次查询,命中一级缓存,不走数据库
User user2 = mapper.selectById(1);
System.out.println(user1 == user2); // true,同一对象
} finally {
session.close();
}
2.1.2 作用域
localCacheScope决定了一级缓存的有效范围。取值SESSION和STATEMENT有如下区别:
SESSION:
- 一级缓存在整个 SqlSession 会话期间都有效。
- 只有当
SqlSession执行insert/update/delete操作(会自动清空当前 SqlSession 的一级缓存)、调用sqlSession.clearCache()手动清空缓存,或SqlSession关闭(sqlSession.close())时,该会话的一级缓存才会失效或被销毁。
STATEMENT:
- 一级缓存仅对当前查询语句有效,查询结束后缓存立即失效 ,相当于 "禁用了一级缓存";
- 查询执行完成后,缓存会被立即清空 / 失效,第二次查询不会复用第一次的缓存结果,仍然会去数据库重新查询;
- 用于解决嵌套查询的重复数据加载问题,如关联查询中的相同数据避免重复查询。
2.1.3 非线程安全
一级缓存实现是PerpetualCache,不是线程安全的,因为底层使用了HashMap,而且putObject方法也不加锁。


那么为什么不使用线程安全的Map呢?基于以下几点原因:
- MyBatis的设计原则:SqlSession不应该被多线程共享
- 使用ConcurrentHashMap会带来性能开销
因此,每个线程应当使用独立的SqlSession(如使用SqlSessionManager ),或使用Spring等框架提供的线程安全封装(如SqlSessionTemplate )。
2.2 二级缓存
2.2.1 特点和启用
二级缓存有以下特点:
- 作用域:Mapper(Namespace)级别,跨Session共享
- 生命周期:应用启动到关闭
- 存储结构:可配置(LRU、FIFO等)
- 默认状态:关闭,需手动开启
- 事务性:commit后才生效,rollback会清空
启用二级缓存,需要满足三个条件:
- 全局开启(默认启用)
xml
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
只有当Configuration中cacheEnabled=true时,才会创建CachingExecutor。

- Mapper局部配置(必须手动配置)
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基本配置,激活二级缓存 -->
<cache/>
<!-- 完整配置 -->
<cache
eviction="LRU" <!-- 淘汰策略:LRU/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 刷新间隔:60秒 -->
size="512" <!-- 缓存对象数量 -->
readOnly="false"/> <!-- 是否只读 -->
</mapper>
- 提供实体类序列化(推荐)
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// ...
}
Mapper 中的 select查询语句useCache默认为true。
当执行 insert 、 update 、 delete操作时( flushCache默认为true),MyBatis 会自动清空当前 Mapper 命名空间下的二级缓存,确保缓存数据与数据库数据一致性。
来看跨Session共享二级缓存的示例
java
// Session A
SqlSession session1 = sqlSessionFactory.openSession();
try {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1); // 查询数据库
session1.commit(); // 提交后才放入二级缓存
} finally {
session1.close();
}
// Session B
SqlSession session2 = sqlSessionFactory.openSession();
try {
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 命中二级缓存
} finally {
session2.close();
}
2.2.2 其他特性
此外,二级缓存支持语句级控制、自定义缓存实现、启用缓存日志
xml
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<!-- select默认使用缓存,可不配置useCache -->
<select id="selectById" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 不使用缓存 -->
<select id="selectRealtime" useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 更新操作清空缓存(默认) -->
<update id="updateUser" flushCache="true">
UPDATE user SET name = #{name} WHERE id = #{id}
</update>
</mapper>
xml
<!-- 使用Redis作为二级缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
</cache>
xml
<!-- 开启缓存日志 -->
<cache type="org.apache.ibatis.cache.decorators.LoggingCache">
<property name="logImpl" value="STDOUT_LOGGING"/>
</cache>
2.3 总结
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper(即Namespace) |
| 默认状态 | 开启 | 关闭 |
| 线程安全 | 否 | 是 |
| 序列化 | 不需要 | 需要 |
| 适用场景 | 单次会话优化 | 跨会话共享 |
- 一级缓存是SqlSession级别,无法关闭。但是当设置LocalCacheScope为STATEMENT时,查询语句执行结束时都会清空一级缓存,相当于 "禁用了一级缓存的跨查询复用能力";
- 二级缓存,适合读多写少场景,需谨慎使用;
- 在分布式系统下,需要禁用Mybatis的一二级缓存,因为它们都是本地缓存,会引起数据不一致问题。应当使用Redis等分布式缓存。