SqlSession 与缓存
SqlSession 与执行链:Executor、一级缓存、二级缓存
上一篇 01-Mapper 动态代理 把接口调用转成了 sqlSession.selectOne(...),这一篇看 SqlSession 之后发生什么。
SqlSession 只是门面,真正执行 SQL 的是 Executor 链:CachingExecutor(二级缓存,装饰器)包着 BaseExecutor(一级缓存,模板方法),最后由 SimpleExecutor.doQuery 经 StatementHandler 落到 JDBC。
两级缓存都挂在这条链上,共用同一把 CacheKey。
先看门面。DefaultSqlSession 里没有任何执行逻辑,它做三件事:按语句 id 捞出 MappedStatement、包装集合参数、维护 dirty 脏标记。
代码块收起展开
// 基于本地 MyBatis 仓 (mybatis-3.5.19), org.apache.ibatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor; // 所有增删改查最终都甩给它,SqlSession 自己不碰 JDBC
private final boolean autoCommit;
private boolean dirty; // 脏标记:commit/rollback/close 时据此决定要不要产生真实的事务操作
// ...
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
}
if (list.size() > 1) {
throw new TooManyResultsException(
"Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
// ...
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement); // 用 "namespace.方法名" 捞出这条 SQL 的全部元信息
dirty |= ms.isDirtySelect(); // select 也可能标脏:affectData=true 的"写型查询"(如 select ... for update 场景)
return executor.query(ms, wrapCollection(parameter), rowBounds, handler); // wrapCollection:裸 List/数组包成 map,键名 list/array
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public int update(String statement, Object parameter) {
try {
dirty = true; // 先标脏再执行:即使 SQL 抛异常,close 时也会按脏 session 走回滚
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter)); // insert/delete 也都转发到这里,对 Executor 而言写操作只有一种
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// ...
private boolean isCommitOrRollbackRequired(boolean force) {
return !autoCommit && dirty || force; // 只读 session 的 commit 是空操作:没写过就不浪费一次数据库往返
}
// ...
}开启二级缓存(cacheEnabled=true,默认就是 true)时,Configuration.newExecutor 会把真正的 Executor 包进 CachingExecutor,所以它站在链路最外层,查询先从它进:
代码块收起展开
// 基于本地 MyBatis 仓 (mybatis-3.5.19), org.apache.ibatis.executor.CachingExecutor
public class CachingExecutor implements Executor {
private final Executor delegate; // 被装饰的真实 Executor(BaseExecutor 子类)
private final TransactionalCacheManager tcm = new TransactionalCacheManager(); // 二级缓存的"事务暂存层",见下一段
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this); // 把外层引用塞给里层:延迟加载发起的二次查询也要从最外层进,不能绕过二级缓存
}
// ...
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms); // 增删改默认 flushCache=true:整个 namespace 的二级缓存一起作废
return delegate.update(ms, parameterObject);
}
// ...
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // key 委托 delegate 生成:两级缓存共用同一把 key
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache(); // namespace 里配了 <cache/> 才非空,否则这层直接穿透
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) { // 自定义 resultHandler 时没法缓存:结果集被用户逐行消费掉了
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116 // 只写进暂存区,事务提交才落真缓存,防跨 session 脏读
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// ...
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required); // 先提数据库事务
tcm.commit(); // 再刷缓存;顺序反了会出现"缓存已可见、库里还没提交"的脏窗口
}
// ...
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache); // 同样只记一个 clear 标记,真正清空也延迟到 commit
}
}
// ...
}“事务提交才生效”的机关全在 TransactionalCache:它挡在共享缓存前面,把本事务的写操作全部暂存在自己身上。
代码块收起展开
// 基于本地 MyBatis 仓 (mybatis-3.5.19), org.apache.ibatis.cache.decorators.TransactionalCache
public class TransactionalCache implements Cache {
// ...
private final Cache delegate; // 真正的共享二级缓存(本体 PerpetualCache,外面可再套 Lru/Serialized 等装饰器)
private boolean clearOnCommit; // 本事务执行过 clear:提交前先对自己屏蔽缓存,提交时才真清
private final Map<Object, Object> entriesToAddOnCommit; // 写暂存区,commit 前对其他 SqlSession 完全不可见
private final Set<Object> entriesMissedInCache; // 记录未命中的 key:给 BlockingCache 回滚时解锁用
// ...
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key); // 读直接穿到共享缓存:里面只有已提交数据,读不脏
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null; // 自己刚宣布作废的缓存,自己也不能再读到
}
return object;
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object); // 写只进暂存区——这一行就是"二级缓存提交才生效"的实现位置
}
// ...
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries(); // 暂存区整体刷进共享缓存,其他 SqlSession 从这一刻起可见
reset();
}
public void rollback() {
unlockMissedEntries(); // 回滚不用撤销任何东西(暂存区直接丢弃),只需通知 BlockingCache 释放 miss 时上的锁
reset();
}
// ...
}二级缓存未命中(或根本没开)就落到 BaseExecutor,一级缓存和真正的查询骨架都在这里:
代码块收起展开
// 基于本地 MyBatis 仓 (mybatis-3.5.19), org.apache.ibatis.executor.BaseExecutor
public abstract class BaseExecutor implements Executor {
// ...
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; // 嵌套查询里暂时装配不了的属性,攒到最外层查询结束统一处理
protected PerpetualCache localCache; // 一级缓存本体:PerpetualCache 就是个裸 HashMap,无淘汰、无上限、不线程安全
protected PerpetualCache localOutputParameterCache; // 存储过程 OUT 参数专用,平时不用管
// ...
protected int queryStack; // 嵌套查询深度:回到 0 才做延迟加载、清 STATEMENT 级缓存这些收尾动作
// ...
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache(); // 关键:任何增删改先清空整个一级缓存,宁可全部重查也不留可能的脏数据
return doUpdate(ms, parameter);
}
// ...
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache(); // flushCache=true 只在最外层查询清:嵌套中途清会把上层正在用的条目抽走
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 命中后只有存储过程要回填 OUT 参数,普通查询直接返回
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load(); // 此时被依赖的结果都已进 localCache,循环引用也能装配上
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache(); // STATEMENT 作用域:每条语句结束就清,一级缓存等于只在嵌套查询内部生效
}
}
return list;
}
// ...
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
// ...
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // 五要素进 key:语句 id、分页 offset/limit、SQL 文本、每个实参值、环境 id
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// ... 逐个实参 cacheKey.update(value),最后 update(environment.getId())
return cacheKey;
}
// ...
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER); // 先放占位符:嵌套查询循环引用时,DeferredLoad 靠它识别"正在查,先挂起"
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); // 模板方法:Simple/Reuse/Batch 三个子类只实现 doXxx
} finally {
localCache.removeObject(key); // 放 finally:查询失败必须撤掉占位符,否则这个 key 永远卡死
}
localCache.putObject(key, list);
// ...
return list;
}
// ...
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache(); // commit/rollback 都清一级缓存:事务边界一过,旧快照不再可信
flushStatements();
if (required) {
transaction.commit();
}
}
// ...
}原理串讲
以开着二级缓存的 session.selectOne("com.x.UserMapper.getById", 1) 走一遍。
DefaultSqlSession.selectOne 调 selectList,从 Configuration 里按 id 捞出 MappedStatement,然后 executor.query(ms, ...)。
因为 cacheEnabled=true,Configuration.newExecutor 在创建 session 时已经把 SimpleExecutor 包进了 CachingExecutor,所以先进 CachingExecutor.query:第一步 createCacheKey——注意它委托给 delegate,也就是 BaseExecutor.createCacheKey,把语句 id、分页、SQL 文本、实参、环境 id 五样东西哈希成一把 CacheKey。
为什么参数和 SQL 文本都要进 key?因为动态 SQL 下同一个语句 id 可能生成完全不同的 SQL,同一 SQL 不同参数结果也不同,少放任何一样都会拿错缓存。两级缓存共用这一把 key,避免算两次。
然后 tcm.getObject(cache, key)。TransactionalCacheManager 给每个 namespace 的 Cache 懒建一个 TransactionalCache,getObject 读操作直接穿到共享缓存——读不用暂存,因为共享缓存里只有别人已提交的数据。
未命中,落到 delegate.query,进 BaseExecutor.query:查 localCache(一级缓存),还是未命中,走 queryFromDatabase。
这里先放 EXECUTION_PLACEHOLDER 占位再 doQuery,由 SimpleExecutor 创建 StatementHandler、拿连接、prepare、设参数、执行并把 ResultSet 映射成对象;查完撤占位符、把真结果放进 localCache。
为什么要占位符这个弯?为了嵌套查询的循环引用:A 的结果要装配 B,B 又引用 A,B 装配时发现 A 的 key 上是占位符(还在查),就把自己挂进 deferredLoads,等最外层查询结束(queryStack == 0)、A 的真结果已入缓存后再统一 load()。没有占位符,循环引用要么死循环要么重复查库。
结果一路返回到 CachingExecutor,tcm.putObject(cache, key, list)——注意这行只把结果写进 entriesToAddOnCommit 暂存区,共享缓存此刻纹丝不动。
为什么写入要延迟到提交?因为本事务还没 commit,如果立刻写共享缓存,其他 SqlSession 就能读到未提交的数据;一旦本事务回滚,那就是实打实的脏读。
所以写暂存、读直穿,session.commit() 时 CachingExecutor.commit 先 delegate.commit(清一级缓存、提数据库事务),再 tcm.commit 把暂存区 flushPendingEntries 刷进共享缓存——数据库先落定、缓存后可见,顺序不能反。回滚则更简单:暂存区直接扔掉,什么都没发生过。
同一个 session 内第二次执行相同查询,在 BaseExecutor.query 的 localCache.getObject(key) 直接命中返回,不碰数据库;中间任何一次 update(insert/delete 也走它)都会 clearLocalCache 全量清空。
为什么清全部而不是精确失效对应条目?因为 CacheKey 是”语句+参数”维度的,MyBatis 不做 SQL 语义分析,无法从一条 update 反推它影响了哪些查询的结果集,与其漏清出脏数据,不如全清换正确性——一级缓存本来就只是省同一事务内的重复查询,清了大不了重查。
上游 MapperProxy 怎么把接口调用送到 selectOne,见 01-Mapper 动态代理。
设计取舍
- 一级缓存默认 SESSION 作用域,但多个 session 并发读写同一行时,命中旧缓存等于读到过期快照;且与 Spring 集成后每次 mapper 调用通常是新 SqlSession,基本命中不了。
介意就配localCacheScope=STATEMENT,源码里它只是”每条语句结束多调一次 clearLocalCache”。 - 二级缓存按 namespace 隔离是最大的坑:UserMapper 里的多表关联查询缓存了 order 数据,OrderMapper 的 update 不会失效它。跨 namespace 写多的系统开二级缓存必脏,生产上通常关掉,用 Redis 在服务层自己做。
- PerpetualCache 就是无界 HashMap,一级缓存在长事务里大批量查询会一直涨;二级缓存靠外面再包 LruCache 等装饰器做淘汰——缓存功能全靠装饰器组合而不是继承,一层一个职责。
- CachingExecutor 用装饰器而不是塞进 BaseExecutor:
cacheEnabled=false时干脆不包这层,查询路径上一行缓存判断代码都不用走。 selectOne查出多条抛 TooManyResultsException 而不是悄悄取第一条:宁可炸也不掩盖”你以为唯一其实不唯一”的数据问题。