肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了!

MyBatis缓存Cache源码分析 一、缓存接口介绍 MyBatis的一级缓存和二级缓存都是使用的Cache接口实现类,它的方法定义如下: public interface Cache { String getId(); void putObject(Object key, Object value); Object getObject(Object key); Object removeObject... 阅读详情

MyBatis缓存介绍

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

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

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

重点的那句话就是: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方法!

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

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中一个关于二级缓存的类(其他相关的类和接口之前已经分析过):

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

配置

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

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

<cache/>

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

1.mybatis全局配置文件中的setting中的cacheEnabled需要为true(默认为true,不设置也行)

2.mapper配置文件中需要加入节点

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文件中加入的中开始分析源码,关于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> (!databaseIdMatchesCurrent(id, databaseId, <span class="hljs-keyword">this</span>.requiredDatabaseId)) <span class="hljs-keyword">return</span>;

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

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

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

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

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

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

builderAssistant.addMappedStatement(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方法:

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

进入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配置文件中的被设置到了XMLMapperBuilder的builderAssistant属性中,XMLMapperBuilder中使用XMLStatementBuilder遍历CRUD节点,遍历CRUD节点的时候将这个cache节点设置到这些CRUD节点中,这个cache就是所谓的二级缓存!

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

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

进入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 class="hljs-params"></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接口的实现类也使用了装饰者设计模式。

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

肝了一夜的源码,终于可以通过源码分析MyBatis的缓存了

简单说明:

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的缓存原理,希望对读者有帮助。

最后

我这边整理了一份MyBatis资料文档MyBatis、
Spring系列全家桶、Java的系统化资料:(包括Java核心知识点、面试专题和20年最新的互联网真题、电子书等)有需要的朋友可以点击这领取。

第28讲:Mybatis 源码分析总结 大家好,我是田哥 本文是MyBatis源码分析系列文章的第28篇,本文主要内容是MyBatis 源码分析总结 关于源码,我先给大家总结三个点,然后咱们在说说MyBatis中的核心代码。 三个点 一、为什么纠结? 因为面试的时候,面试官很喜欢问:你看过什么框架源码?JDK源码也行。 这时候,如果回答没有看过,虽然没让你立马回去等通知。但也许,你在面试官心中的印象就大打折扣了。 如果回答看过,并且还能较为流利地说上一番,那面试官或许会在心里赞叹:小伙子可以呀!(PS:很多面试官自己也没看过,或者看过很多也忘了) 阅读详情

相关推荐

怒肝一夜 | Mybatis源码深度解析

关注“Java后端技术全栈”回复“面试”获取全套面试资料本文:12006字,阅读时长:10分15秒前面已经发过Mybatis源码解析的文章了,本文是对前面文章进行精简以及部分调整优化,总...

田维常 1325

Mybatis源码深度解析

怒肝一夜 | Mybatis源码深度解析怒肝一夜 | Mybatis源码深度解析http://www.360doc.com/content/21/0104/15/64417993_955151966.shtml好文章!!!

L13158562070的博客 2452

【MyBatis源码分析】五、MyBatis的缓存

从MyBatis的缓存设计到源码和设计模式,再到定制化MyBatis缓存...

Javaer 1081

怒肝一夜,写了这篇 mybatis 源码阅读篇

reflection 包Java 中的反射虽然功能强大,但对大多数开发人员来说,写出高质量的反射代码还是 有一定难度的。MyBatis 中专门提供了反射模块,该模块对 Java 原生的反射进行了良好的封装,提了更加简洁易用的 API,方便上层调用,并且对反射操作进行了一系列优化,例如缓存了类的元数据,提高了反射操作的性能。Java 反射机制主要提供了以下功能。· 在运行时判断任意一个对象所属的类;· 在运行时构造任意一个类的对象;· 在运行时修改任意一个对象的成员变量;

m0_69804655的博客 178

挑战第七天:Llamaindex RAG 实践

安装python 依赖包,并安装 Llamaindex和相关包以及Sentence Transformer 模型。进入开发机后,创建新的conda环境,命名为。镜像,然后在资源配置中,使用。的选项,然后立即创建开发机器。后,点击 选择镜像 使用。然后安装相关基础依赖。

weixin_53579823的博客 728

秦时明月6.2魔改版_从零到一部署指南_含安卓客户端配置与GM后台直通

本文详细介绍了秦时明月6.2魔改版的从零到一部署指南,包括环境准备、服务端安装、安卓客户端配置以及GM后台直通等关键步骤。特别针对安卓客户端配置和GM工具使用提供了实用技巧,帮助用户快速搭建并优化游戏服务器,确保稳定运行。

weixin_42522964的博客 315

Ojbect类常用方法源码

Ojbect类详解 先看一下Object类常见的代码 public class Object { public final native Class<?> getClass(); public native int hashCode(); public boolean equals(Object obj) { return (this == obj); } public String toString()

weixin_44023129的博客 510

安装matlab_Matlab+yalmip+cplex安装教程及算例

数学建模/科研学习推荐学习软件:Matlab+yalmip+cplex安装教程及算例其实绝大多数本科生都是在用Matlab和LINGO求解优化问题,Matlab可以求解一般优化问题,但是当优化问题规模比较大或者在应对一些非线性问题时候,Matlab会出现求解时间长,求解效率低,甚至难以求解等问题。Matlab+yalmip+cplex是Matlab通过调用商业软件cplex求解,求解时间短,这里简...

weixin_28939637的博客 5005

保姆级教程:怒肝一夜,深入Mybatis源码SqlSession

保姆级教程:怒肝一夜,深入Mybatis源码SqlSession前言配置文件解析,并保存于Configuration中回到SqlSessionFactoryBuilder再回到程序结构上一层,就是我们的起始点配置文件解析全过程如何构建SqlSession创建事务Transaction把事务作为参数传递给对象configuration中方法newExecutor创建执行器对象Executor创建执行器创建DefaultSqlSession 前言 它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持

颓了呀的博客 312

Android音频学习(十四)——加载音频设备

loadHwModule 是 Android 音频系统的核心初始化函数,负责加载音频硬件抽象层(HAL)的模块实现。它建立 AudioPolicyService 与底层硬件的桥梁,将配置文件(audio_policy_config.xml)中的标签<module>定义转化为可操作的硬件对象,加载的是音频硬件模块(HAL实现),每个模块对应一种音频设备类型,以动态库的形式存在,负责与底层音频硬件交互。

miemie2014的博客 1730

AUTOSAR 3万字面试题全面解析(持续更新)

ECU抽象层(EAL)是AUTOSAR架构中的一部分,它位于微控制器抽象层(MCAL)之下,但通常与MCAL一起被提及。EAL的目标是提供一个统一的接口来访问和管理ECU(Electronic Control Unit,电子控制单元)的硬件资源,包括处理器、存储器和外围设备。通过这种抽象,EAL使得上层软件能够以一种标准化的方式与ECU硬件交互,而不需要关心具体的硬件实现细节。EAL解决了硬件异构性带来的问题,即不同ECU可能采用不同类型的处理器和硬件配置。

大模型大数据攻城狮的专栏 5602

【Mybatis】源码分析-深入源码

问题在开发过程中很多时候我们并不需要总是在加载⽤户信息时就⼀定要加载他的订单信息。此时就是我们所说的延迟加载。在⼀对多中,当我们有⼀个⽤户,它有个100个订单。在查询⽤户的时候,要不要把关联的订单查出来?在查询订单的时候,要不要把关联的⽤户查出来?在查询⽤户时,⽤户下的订单应该是,什么时候⽤,什么时候查询。在查询订单时,订单所属的⽤户信息应该是随着订单⼀起查询出来。就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。延迟加载也称懒加载。

下半夜的风 4536

IPD流程中的RR\OR\IR\PB\SR\AR\CR等名词汇总(基于新的产品包需求架构)

RR:Raw Requirement,原始需求=》IR:Initial Requirement,初始需求=》SR:System Requirement,系统需求=》AR:Allocated Requirement,分配需求=》CR:Process Change Requirement,流程变更需求。

a1809032425的博客 3186

mybatis(2)通过源码分析MyBatis的缓存

出自:http://www.cnblogs.com/fangjian0423/p/mybatis-cache.html 前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍: MyBatis支持声明式数据缓存(declarative data ca...

cowbin2012的专栏 1万+

Python爬虫实战:深度解析携程/飞猪酒店评论API与大数据采集

本文详细介绍了如何通过Python爬虫技术获取携程和飞猪平台的酒店评论数据。我们涵盖了从API分析、反爬虫对策到数据存储和处理的完整流程。API逆向分析:通过浏览器开发者工具发现和分析API接口反爬虫对策:使用请求头随机化、IP代理、浏览器自动化等技术异步处理:使用asyncio提高爬取效率数据质量:实现数据清洗和质量检查机制系统架构:构建可扩展的分布式爬虫系统未来改进方向:机器学习识别验证码分布式爬虫架构实时数据流处理更智能的反反爬虫策略。

2201_76125261的博客 599

华为OD机试双机位C卷-AI面板识别(Py/Java/C/C++/Js/Go)

华为OD机试双机位C卷 AI面板识别,提供C语言、Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解以及在线OJ模拟做题。华为OD机试 - AI面板识别。

qq_45776114的博客 1342

Mybatis二级缓存源码分析

本文中有些内容与之前关于一级缓存分析的文章中,有部分内容相似,所以有些会直接跳过,建议可以先看看关于一级缓存分析的文章一级缓存源码分析 二级缓存生命周期 mybatis一级缓存生命周期是sqlSession级别,二级缓存生命周期是sqlSessionFactory级别,也就是整个应用级别的生命周期。 如何开启二级缓存 mybatis二级缓存与一级缓存不同,需要先在全局配置文件中配置,默认开启 <setting name="cacheEnabled" value="true" /> 还需要在ma

自律使我自由 2709

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

肝了一早上,终于把mybatis的一级缓存和二级缓存原理搞懂了~

今天的沉淀,是助力明天飞向远方的基石~ 每当自己沉思下来写学习文章的时候,心情总是会得到一片宁静。 缓存的概念 我们在查询数据时,经常去查询一些条件相同、数据的正确与否对最终结果影响不大的数据,并且每次查询总是去数据库获取连接查询,这样的操作不仅浪费时间还很消耗数据库连接资源。而缓存就是将数据临时存储于内存,这样就能减少数据查询交互的通讯量,减少处理次数,提高我们的查询效率。 会话 会话就是用户与系统的一次完整的交流,在一次交流过程中会包含多次请求响应,然后发送的请求都只是同一个用户,SqlSessi.

收破烂的小熊猫 1165

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

MyBatis 源码分析 - 缓存原理

在 MyBatis 中,Cache 是缓存接口,定义了一些基本的缓存操作,所有缓存类都应该实现该接口。MyBatis 内部提供了丰富的缓存实现类,比如具有基本缓存功能的 PerpetualCache,具有 LRU 策略的缓存 LruCache,以及可保证线程安全的缓存 SynchronizedCache 和具备阻塞功能的缓存 BlockingCache 等。除此之外,还有很多缓存实现类,这里就不一一列举了。需要特别说明的是,MyBatis 在实现缓存模块的过程中,使用了装饰模式。

攻城狮不是猫 993

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

MyBatis底层源码分析

SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能。 Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护 StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等 ParameterHandler :负责对用户传递的参数转换成JDBC Statement 所对应的数据类型

dandelionl_的博客 7899

mybatis缓存源码分析

定义MyMybatisCache实现Cache接口/*** 1. 存数据 存多个数据*///定义一个map来进行缓存的存储@Override//获取类名来做为缓存标识符@Override//这里以下方法直接使用map集合的常用方法实现@Override@Override@Override@Override@Override//废弃方法一般不实现,直接返回null.这里我们为了实现接口的完整性new了一个ReadWriteLock的实现类。

helloworld工程师的博客 329

MyBatis源码分析--一级缓存、二级缓存原理

有点项目经验的朋友都知道缓存的重要性是不言而喻的,不仅仅我们在开发项目业务功能的时候使用了各种缓存,框架在设计的时候也有框架层面的缓存,尤其在查询多的场景下,缓存可以大大的减少数据库访问,提升系统效率,Mybatis 也提供了缓存,分别为一级缓存和二级缓存,默认的情况下,Mybatis 只开启一级缓存。

weixin_42118323的博客 1407

通过源码分析MyBatis的缓存

前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍: MyBatis支持声明式数据缓存(declarative data caching)。当一条SQL语句被标记为“可缓存”后,首次执行它时从数据库获取的所有数据会被存储在一段高速缓存中,今后执...

技术小学僧的博客 200

Mybatis深入源码分析之SqlSessionFactory二级缓存原理分析

上篇内容回顾可以参考;Mybatis深入源码分析之SQLSession一级缓存原理分析 这里再概括下上篇源码分析内容: 一:Mybatis一级缓存 mybatis的一级缓存是SqlSession缓存,在操作数据库的时候需要先创建SQL会话对象,在对象中有个HashMap用于存储缓存...

chuanyingcao2675的博客 523

【MyBatis源码】MyBatis缓存机制源码分析

MyBatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。MyBatis的一级缓存,用户只能控制缓存的级别,并不能关闭。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的Id相同时,即认为缓存对象相同。

发现问题,面对问题,分析问题,解决问题,总结问题 903

mybatis的缓存机制源码分析之二级缓存解析

引言 本篇源码解析基于mybatis 3.5.8版本。 MyBatis 中的缓存指的是 MyBatis 在执行一次SQL查询时,在满足一定的条件下,会把这个sql和对应的查询结果缓存起来。当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是请求到数据库。当然如果中间有更新操作,缓存会失效。 MyBatis中的缓存分为一级缓存和二级缓存,一级缓存又被称为 SqlSession 级别的缓存,二级缓存又被称为表级缓存。通俗的说,一级缓存是本次会话有效,二级缓存可以跨越多个会话共享缓存。 当开启缓存后,

lucasma的博客 273
上一篇: for、foreach、stream 哪家的效率更高,你真的用对了吗?
下一篇: 你必须懂!也可以懂的@Transactional原理!
普通网友
博客等级 码龄5年 1974粉丝 9原创
评论 16
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值