SqlSession 与缓存

SqlSession 与执行链:Executor、一级缓存、二级缓存

上一篇 01-Mapper 动态代理 把接口调用转成了 sqlSession.selectOne(...),这一篇看 SqlSession 之后发生什么。
SqlSession 只是门面,真正执行 SQL 的是 Executor 链:CachingExecutor(二级缓存,装饰器)包着 BaseExecutor(一级缓存,模板方法),最后由 SimpleExecutor.doQuery 经 StatementHandler 落到 JDBC。
两级缓存都挂在这条链上,共用同一把 CacheKey

先看门面。DefaultSqlSession 里没有任何执行逻辑,它做三件事:按语句 id 捞出 MappedStatement、包装集合参数、维护 dirty 脏标记。

代码块JAVA · 57 行收起展开
// 基于本地 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,所以它站在链路最外层,查询先从它进:

代码块JAVA · 63 行收起展开
// 基于本地 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:它挡在共享缓存前面,把本事务的写操作全部暂存在自己身上。

代码块JAVA · 49 行收起展开
// 基于本地 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,一级缓存和真正的查询骨架都在这里:

代码块JAVA · 108 行收起展开
// 基于本地 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.selectOneselectList,从 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.commitdelegate.commit(清一级缓存、提数据库事务),再 tcm.commit 把暂存区 flushPendingEntries 刷进共享缓存——数据库先落定、缓存后可见,顺序不能反。回滚则更简单:暂存区直接扔掉,什么都没发生过。

同一个 session 内第二次执行相同查询,在 BaseExecutor.querylocalCache.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 而不是悄悄取第一条:宁可炸也不掩盖”你以为唯一其实不唯一”的数据问题。

延伸阅读