MyBatis的缓存

前方高能! 本文内容有点多,通过实际测试例子 + 源码分析的方式解剖 MyBatis 缓存的概念,对这方面有兴趣的小伙伴请继续看下去 ~

MyBatis 缓存介绍

首先看一段wiki上关于 MyBatis 缓存的介绍:

MyBatis 支持声明式数据缓存(declarative data caching)。当一条 SQL 语句被标记为“可缓存”后,首次执行它时从数据库获取的所有数据会被存储在一段高速缓存中,今后执行这条语句时就会从高速缓存中读取结果,而不是再次命中数据库。MyBatis 提供了默认下基于 Java HashMap 的缓存实现,以及用于与 OSCache、Ehcache、Hazelcast 和 Memcached 连接的默认连接器。MyBatis 还提供 API 供其他缓存实现使用。

重点的那句话就是:MyBatis 执行 SQL 语句之后,这条语句就是被缓存,以后再执行这条语句的时候,会直接从缓存中拿结果,而不是再次执行 SQL

这也就是大家常说的 MyBatis 一级缓存,一级缓存的作用域 scope 是 SqlSession。

MyBatis 同时还提供了一种全局作用域 global scope 的缓存,这也叫做二级缓存,也称作全局缓存。

一级缓存

测试

同个 session 进行两次相同查询:

@Test
public void test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user);
        User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user2);
    } finally {
        sqlSession.close();
    }
}

MyBatis 只进行 1 次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

同个 session 进行两次不同的查询:

@Test
public void test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user);
        User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 2);
        log.debug(user2);
    } finally {
        sqlSession.close();
    }
}

MyBatis 进行两次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 2(Integer)
<==      Total: 1
User{id=2, name='FFF', age=50, birthday=Sat Dec 06 17:12:01 CST 2014}

不同 session,进行相同查询:

@Test
public void test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    try {
        User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user);
        User user2 = (User)sqlSession2.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user2);
    } finally {
        sqlSession.close();
        sqlSession2.close();
    }
}

MyBatis 进行了两次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

同个 session, 查询之后更新数据,再次查询相同的语句:

@Test
public void test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user);
        user.setAge(100);
        sqlSession.update("org.format.mybatis.cache.UserMapper.update", user);
        User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
        log.debug(user2);
        sqlSession.commit();
    } finally {
        sqlSession.close();
    }
}

更新操作之后缓存会被清除:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: update USERS SET NAME = ? , AGE = ? , BIRTHDAY = ? where ID = ? 
==> Parameters: format(String), 23(Integer), 2014-10-12 23:20:13.0(Timestamp), 1(Integer)
<==    Updates: 1
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

很明显,结果验证了一级缓存的概念,在同个 SqlSession 中,查询语句相同的 sql 会被缓存,但是一旦执行新增或更新或删除操作,缓存就会被清除

源码分析

在分析 MyBatis 的一级缓存之前,我们先简单看下 MyBatis 中几个重要的类和接口:

org.apache.ibatis.session.Configuration 类:MyBatis 全局配置信息类

org.apache.ibatis.session.SqlSessionFactory 接口:操作 SqlSession 的工厂接口,具体的实现类是 DefaultSqlSessionFactory

org.apache.ibatis.session.SqlSession 接口:执行 sql,管理事务的接口,具体的实现类是 DefaultSqlSession

org.apache.ibatis.executor.Executor 接口:sql 执行器,SqlSession 执行 sql 最终是通过该接口实现的,常用的实现类有 SimpleExecutor 和 CachingExecutor, 这些实现类都使用了装饰者设计模式

一级缓存的作用域是 SqlSession,那么我们就先看一下 SqlSession 的 select 过程:

这是 DefaultSqlSession(SqlSession 接口实现类,MyBatis 默认使用这个类)的 selectList 源码(我们例子上使用的是 selectOne 方法,调用 selectOne 方法最终会执行 selectList 方法):

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause:" + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

我们看到 SqlSession 最终会调用 Executor 接口的方法。

接下来我们看下 DefaultSqlSession 中的 executor 接口属性具体是哪个实现类。

DefaultSqlSession 的构造过程(DefaultSqlSessionFactory 内部):

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
      return new DefaultSqlSession(configuration, executor);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause:" + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

我们看到 DefaultSqlSessionFactory 构造 DefaultSqlSession 的时候,Executor 接口的实现类是由 Configuration 构造的:

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

Executor 根据 ExecutorType 的不同而创建,最常用的是 SimpleExecutor,本文的例子也是创建这个实现类。 最后我们发现如果 cacheEnabled 这个属性为 true 的话,那么 executor 会被包一层装饰器,这个装饰器是 CachingExecutor。其中 cacheEnabled 这个属性是 mybatis 总配置文件中 settings 节点中 cacheEnabled 子节点的值,默认就是 true,也就是说我们在 mybatis 总配置文件中不配 cacheEnabled 的话,它也是默认为打开的。

现在,问题就剩下一个了,CachingExecutor 执行 sql 的时候到底做了什么?

带着这个问题,我们继续走下去(CachingExecutor 的 query 方法):

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) { 
        ensureNoOutParams(ms, parameterObject, boundSql);
        if (!dirty) {
          cache.getReadWriteLock().readLock().lock();
          try {
            @SuppressWarnings("unchecked")
            List<E> cachedList = (List<E>) cache.getObject(key);
            if (cachedList != null) return cachedList;
          } finally {
            cache.getReadWriteLock().readLock().unlock();
          }
        }
        List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
        return list;
      }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
    

其中 Cache cache = ms.getCache(); 这句代码中,这个 cache 实际上就是个二级缓存,由于我们没有开启二级缓存 ( 二级缓存的内容下面会分析),因此这里执行了最后一句话。这里的 delegate 也就是 SimpleExecutor,SimpleExecutor 没有 Override 父类的 query 方法,因此最终执行了 SimpleExecutor 的父类 BaseExecutor 的 query 方法。

所以一级缓存最重要的代码就是 BaseExecutor 的 query 方法!

BaseExecutor 的属性 localCache 是个 PerpetualCache 类型的实例,PerpetualCache 类是实现了 MyBatis 的 Cache 缓存接口的实现类之一,内部有个 Map<object, object=""> 类型的属性用来存储缓存数据。 这个 localCache 的类型在 BaseExecutor 内部是写死的。 这个 localCache 就是一级缓存!

接下来我们看下为何执行新增或更新或删除操作,一级缓存就会被清除这个问题。

首先 MyBatis 处理新增或删除的时候,最终都是调用 update 方法,也就是说新增或者删除操作在 MyBatis 眼里都是一个更新操作。

我们看下 DefaultSqlSession 的 update 方法:

public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause:" + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

很明显,这里调用了 CachingExecutor 的 update 方法:

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
}

这里的 flushCacheIfRequired 方法清除的是二级缓存,我们之后会分析。 CachingExecutor 委托给了 (之前已经分析过)SimpleExecutor 的 update 方法,SimpleExecutor 没有 Override 父类 BaseExecutor 的 update 方法,因此我们看 BaseExecutor 的 update 方法:

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);
}

我们看到了关键的一句代码: clearLocalCache(); 进去看看:

public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
}

没错,就是这条,sqlsession 没有关闭的话,进行新增、删除、修改操作的话就是清除一级缓存,也就是 SqlSession 的缓存。

二级缓存

二级缓存的作用域是全局,换句话说,二级缓存已经脱离 SqlSession 的控制了。

在测试二级缓存之前,我先把结论说一下:

二级缓存的作用域是全局的,二级缓存在 SqlSession 关闭或提交之后才会生效。

在分析 MyBatis 的二级缓存之前,我们先简单看下 MyBatis 中一个关于二级缓存的类 (其他相关的类和接口之前已经分析过):

org.apache.ibatis.mapping.MappedStatement:

MappedStatement 类在 Mybatis 框架中用于表示 XML 文件中一个 sql 语句节点,即一个 <select />、<update /> 或者 <insert /> 标签。Mybatis 框架在初始化阶段会对 XML 配置文件进行读取,将其中的 sql 语句节点对象化为一个个 MappedStatement 对象。

配置

二级缓存跟一级缓存不同,一级缓存不需要配置任何东西,且默认打开。 二级缓存就需要配置一些东西。

本文就说下最简单的配置,在 mapper 文件上加上这句配置即可:

<cache/>

其实二级缓存跟 3 个配置有关:

  1. mybatis 全局配置文件中的 setting 中的 cacheEnabled 需要为 true(默认为 true,不设置也行)
  2. mapper 配置文件中需要加入 <cache> 节点
  3. mapper 配置文件中的 select 节点需要加上属性 useCache 需要为 true(默认为 true,不设置也行)

测试

不同 SqlSession,查询相同语句,第一次查询之后 commit SqlSession:

@Test
public void testCache2() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    try {
        String sql = "org.format.mybatis.cache.UserMapper.getById";
        User user = (User)sqlSession.selectOne(sql, 1);
        log.debug(user);
        // 注意,这里一定要提交。 不提交还是会查询两次数据库
        sqlSession.commit();
        User user2 = (User)sqlSession2.selectOne(sql, 1);
        log.debug(user2);
    } finally {
        sqlSession.close();
        sqlSession2.close();
    }
}

MyBatis 仅进行了一次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

不同 SqlSession,查询相同语句,第一次查询之后 close SqlSession:

@Test
public void testCache2() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    try {
        String sql = "org.format.mybatis.cache.UserMapper.getById";
        User user = (User)sqlSession.selectOne(sql, 1);
        log.debug(user);
        sqlSession.close();
        User user2 = (User)sqlSession2.selectOne(sql, 1);
        log.debug(user2);
    } finally {
        sqlSession2.close();
    }
}

MyBatis 仅进行了一次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

不同 SqlSesson,查询相同语句。 第一次查询之后 SqlSession 不提交:

@Test
public void testCache2() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    try {
        String sql = "org.format.mybatis.cache.UserMapper.getById";
        User user = (User)sqlSession.selectOne(sql, 1);
        log.debug(user);
        User user2 = (User)sqlSession2.selectOne(sql, 1);
        log.debug(user2);
    } finally {
        sqlSession.close();
        sqlSession2.close();
    }
}

MyBatis 执行了两次数据库查询:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

源码分析

我们从在 mapper 文件中加入的 <cache/> 中开始分析源码,关于 MyBatis 的 SQL 解析请参考另外一篇博客Mybatis 解析动态 sql 原理分析。接下来我们看下这个 cache 的解析:

XMLMappedBuilder(解析每个 mapper 配置文件的解析类,每一个 mapper 配置都会实例化一个 XMLMapperBuilder 类)的解析方法:

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace.equals("")) {
          throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause:" + e, e);
    }
}

我们看到了解析 cache 的那段代码:

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
    }
}

解析完 cache 标签之后会使用 builderAssistant 的 userNewCache 方法,这里的 builderAssistant 是一个 MapperBuilderAssistant 类型的帮助类,每个 XMLMappedBuilder 构造的时候都会实例化这个属性,MapperBuilderAssistant 类内部有个 Cache 类型的 currentCache 属性,这个属性也就是 mapper 配置文件中 cache 节点所代表的值:

public Cache useNewCache(Class<? extends Cache> typeClass,
  Class<? extends Cache> evictionClass,
  Long flushInterval,
  Integer size,
  boolean readWrite,
  Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

ok,现在 mapper 配置文件中的 cache 节点被解析到了 XMLMapperBuilder 实例中的 builderAssistant 属性中的 currentCache 值里。

接下来 XMLMapperBuilder 会解析 select 节点,解析 select 节点的时候使用 XMLStatementBuilder 进行解析 (也包括其他 insert,update,delete 节点):

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
<span class="hljs-keyword">if</span> (!<span class="hljs-title function_">databaseIdMatchesCurrent</span>(id, databaseId, <span class="hljs-variable language_">this</span>.<span class="hljs-property">requiredDatabaseId</span>)) <span class="hljs-keyword">return</span>;

<span class="hljs-title class_">Integer</span> fetchSize = context.<span class="hljs-title function_">getIntAttribute</span>(<span class="hljs-string">"fetchSize"</span>);
<span class="hljs-title class_">Integer</span> timeout = context.<span class="hljs-title function_">getIntAttribute</span>(<span class="hljs-string">"timeout"</span>);
<span class="hljs-title class_">String</span> parameterMap = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"parameterMap"</span>);
<span class="hljs-title class_">String</span> parameterType = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"parameterType"</span>);
<span class="hljs-title class_">Class</span>&lt;?&gt; parameterTypeClass = <span class="hljs-title function_">resolveClass</span>(parameterType);
<span class="hljs-title class_">String</span> resultMap = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"resultMap"</span>);
<span class="hljs-title class_">String</span> resultType = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"resultType"</span>);
<span class="hljs-title class_">String</span> lang = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"lang"</span>);
<span class="hljs-title class_">LanguageDriver</span> langDriver = <span class="hljs-title function_">getLanguageDriver</span>(lang);

<span class="hljs-title class_">Class</span>&lt;?&gt; resultTypeClass = <span class="hljs-title function_">resolveClass</span>(resultType);
<span class="hljs-title class_">String</span> resultSetType = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"resultSetType"</span>);
<span class="hljs-title class_">StatementType</span> statementType = <span class="hljs-title class_">StatementType</span>.<span class="hljs-title function_">valueOf</span>(context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"statementType"</span>, <span class="hljs-title class_">StatementType</span>.<span class="hljs-property">PREPARED</span>.<span class="hljs-title function_">toString</span>()));
<span class="hljs-title class_">ResultSetType</span> resultSetTypeEnum = <span class="hljs-title function_">resolveResultSetType</span>(resultSetType);

<span class="hljs-title class_">String</span> nodeName = context.<span class="hljs-title function_">getNode</span>().<span class="hljs-title function_">getNodeName</span>();
<span class="hljs-title class_">SqlCommandType</span> sqlCommandType = <span class="hljs-title class_">SqlCommandType</span>.<span class="hljs-title function_">valueOf</span>(nodeName.<span class="hljs-title function_">toUpperCase</span>(<span class="hljs-title class_">Locale</span>.<span class="hljs-property">ENGLISH</span>));
<span class="hljs-built_in">boolean</span> isSelect = sqlCommandType == <span class="hljs-title class_">SqlCommandType</span>.<span class="hljs-property">SELECT</span>;
<span class="hljs-built_in">boolean</span> flushCache = context.<span class="hljs-title function_">getBooleanAttribute</span>(<span class="hljs-string">"flushCache"</span>, !isSelect);
<span class="hljs-built_in">boolean</span> useCache = context.<span class="hljs-title function_">getBooleanAttribute</span>(<span class="hljs-string">"useCache"</span>, isSelect);
<span class="hljs-built_in">boolean</span> resultOrdered = context.<span class="hljs-title function_">getBooleanAttribute</span>(<span class="hljs-string">"resultOrdered"</span>, <span class="hljs-literal">false</span>);

<span class="hljs-comment">// Include Fragments before parsing</span>
<span class="hljs-title class_">XMLIncludeTransformer</span> includeParser = <span class="hljs-keyword">new</span> <span class="hljs-title class_">XMLIncludeTransformer</span>(configuration, builderAssistant);
includeParser.<span class="hljs-title function_">applyIncludes</span>(context.<span class="hljs-title function_">getNode</span>());

<span class="hljs-comment">// Parse selectKey after includes and remove them.</span>
<span class="hljs-title function_">processSelectKeyNodes</span>(id, parameterTypeClass, langDriver);

<span class="hljs-comment">// Parse the SQL (pre: &lt;selectKey&gt; and &lt;include&gt; were parsed and removed)</span>
<span class="hljs-title class_">SqlSource</span> sqlSource = langDriver.<span class="hljs-title function_">createSqlSource</span>(configuration, context, parameterTypeClass);
<span class="hljs-title class_">String</span> resultSets = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"resultSets"</span>);
<span class="hljs-title class_">String</span> keyProperty = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"keyProperty"</span>);
<span class="hljs-title class_">String</span> keyColumn = context.<span class="hljs-title function_">getStringAttribute</span>(<span class="hljs-string">"keyColumn"</span>);
<span class="hljs-title class_">KeyGenerator</span> keyGenerator;
<span class="hljs-title class_">String</span> keyStatementId = id + <span class="hljs-title class_">SelectKeyGenerator</span>.<span class="hljs-property">SELECT_KEY_SUFFIX</span>;
keyStatementId = builderAssistant.<span class="hljs-title function_">applyCurrentNamespace</span>(keyStatementId, <span class="hljs-literal">true</span>);
<span class="hljs-keyword">if</span> (configuration.<span class="hljs-title function_">hasKeyGenerator</span>(keyStatementId)) {
  keyGenerator = configuration.<span class="hljs-title function_">getKeyGenerator</span>(keyStatementId);
} <span class="hljs-keyword">else</span> {
  keyGenerator = context.<span class="hljs-title function_">getBooleanAttribute</span>(<span class="hljs-string">"useGeneratedKeys"</span>,
      configuration.<span class="hljs-title function_">isUseGeneratedKeys</span>() &amp;&amp; <span class="hljs-title class_">SqlCommandType</span>.<span class="hljs-property">INSERT</span>.<span class="hljs-title function_">equals</span>(sqlCommandType))
      ? <span class="hljs-keyword">new</span> <span class="hljs-title class_">Jdbc3KeyGenerator</span>() : <span class="hljs-keyword">new</span> <span class="hljs-title class_">NoKeyGenerator</span>();
}

builderAssistant.<span class="hljs-title function_">addMappedStatement</span>(id, sqlSource, statementType, sqlCommandType,
    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    resultSetTypeEnum, flushCache, useCache, resultOrdered, 
    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

}

这段代码前面都是解析一些标签的属性,我们看到了最后一行使用 builderAssistant 添加 MappedStatement,其中 builderAssistant 属性是构造 XMLStatementBuilder 的时候通过 XMLMappedBuilder 传入的,我们继续看 builderAssistant 的 addMappedStatement 方法:

进入 setStatementCache:

private void setStatementCache(
  boolean isSelect,
  boolean flushCache,
  boolean useCache,
  Cache cache,
  MappedStatement.Builder statementBuilder) {
    flushCache = valueOrDefault(flushCache, !isSelect);
    useCache = valueOrDefault(useCache, isSelect);
    statementBuilder.flushCacheRequired(flushCache);
    statementBuilder.useCache(useCache);
    statementBuilder.cache(cache);
}

最终 mapper 配置文件中的 <cache/> 被设置到了 XMLMapperBuilder 的 builderAssistant 属性中,XMLMapperBuilder 中使用 XMLStatementBuilder 遍历 CRUD 节点,遍历 CRUD 节点的时候将这个 cache 节点设置到这些 CRUD 节点中,这个 cache 就是所谓的二级缓存!

接下来我们回过头来看查询的源码,CachingExecutor 的 query 方法:

进入 TransactionalCacheManager 的 putObject 方法:

public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}

TransactionalCache 的 putObject 方法:

public void putObject(Object key, Object object) {
    entriesToRemoveOnCommit.remove(key);
    entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
}

我们看到,数据被加入到了 entriesToAddOnCommit 中,这个 entriesToAddOnCommit 是什么东西呢,它是 TransactionalCache 的一个 Map 属性:

private Map<Object, AddEntry> entriesToAddOnCommit;

AddEntry 是 TransactionalCache 内部的一个类:

private static class AddEntry {
    private Cache cache;
    private Object key;
    private Object value;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">AddEntry</span>(<span class="hljs-params">Cache cache, Object key, Object <span class="hljs-keyword">value</span></span>)</span> {
  <span class="hljs-keyword">this</span>.cache = cache;
  <span class="hljs-keyword">this</span>.key = key;
  <span class="hljs-keyword">this</span>.<span class="hljs-keyword">value</span> = <span class="hljs-keyword">value</span>;
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">commit</span>()</span> {
  cache.putObject(key, <span class="hljs-keyword">value</span>);
}

}

好了,现在我们发现使用二级缓存之后:查询数据的话,先从二级缓存中拿数据,如果没有的话,去一级缓存中拿,一级缓存也没有的话再查询数据库。有了数据之后在丢到 TransactionalCache 这个对象的 entriesToAddOnCommit 属性中。

接下来我们来验证为什么 SqlSession commit 或 close 之后,二级缓存才会生效这个问题。

DefaultSqlSession 的 commit 方法:

public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause:" + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

CachingExecutor 的 commit 方法:

public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
    dirty = false;
}

tcm.commit 即 TransactionalCacheManager 的 commit 方法:

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
}

TransactionalCache 的 commit 方法:

public void commit() {
    delegate.getReadWriteLock().writeLock().lock();
    try {
      if (clearOnCommit) {
        delegate.clear();
      } else {
        for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
          entry.commit();
        }
      }
      for (AddEntry entry : entriesToAddOnCommit.values()) {
        entry.commit();
      }
      reset();
    } finally {
      delegate.getReadWriteLock().writeLock().unlock();
    }
}

发现调用了 AddEntry 的 commit 方法:

public void commit() {
  cache.putObject(key, value);
}

发现了! AddEntry 的 commit 方法会把数据丢到 cache 中,也就是丢到二级缓存中!

关于为何调用 close 方法后,二级缓存才会生效,因为 close 方法内部会调用 commit 方法。本文就不具体说了。 读者有兴趣的话看一看源码就知道为什么了。

其他

Cache 接口简介

org.apache.ibatis.cache.Cache 是 MyBatis 的缓存接口,想要实现自定义的缓存需要实现这个接口。

MyBatis 中关于 Cache 接口的实现类也使用了装饰者设计模式。

我们看下它的一些实现类:

简单说明:

LRU – 最近最少使用的: 移除最长时间不被使用的对象。

FIFO – 先进先出: 按对象进入缓存的顺序来移除它们。

SOFT – 软引用: 移除基于垃圾回收器状态和软引用规则的对象。

WEAK – 弱引用: 更积极地移除基于垃圾收集器状态和弱引用规则的对象。

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

可以通过 cache 节点的 eviction 属性设置,也可以设置其他的属性。

cache-ref 节点

mapper 配置文件中还可以加入 cache-ref 节点,它有个属性 namespace。

如果每个 mapper 文件都是用 cache-ref,且 namespace 都一样,那么就代表着真正意义上的全局缓存。

如果只用了 cache 节点,那仅代表这个这个 mapper 内部的查询被缓存了,其他 mapper 文件的不起作用,这并不是所谓的全局缓存。

总结

总体来说,MyBatis 的源码看起来还是比较轻松的,本文从实践和源码方面深入分析了 MyBatis 的缓存原理,希望对读者有帮助。