通过源码分析MyBatis的缓存

Posted 懂一点架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过源码分析MyBatis的缓存相关的知识,希望对你有一定的参考价值。

按一般资料上的说法,Mybatis有两种缓存:一级缓存和二级缓存。一级缓存的作用域是SqlSession,而二级缓存是全局缓存,跨SqlSession。


一,一级缓存

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

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次数据库查询;


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

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进行两次数据库查询;


testCase3:不同session,进行相同查询:

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进行了两次数据库查询;


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

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

    }

}

结果:更新操作之后缓存会被清除,MyBatis进行了两次数据库查询;


小结:在同个SqlSession中,查询语句相同的sql会被缓存,但是一旦执行新增或更新或删除操作,缓存就会被清除。


源码分析

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

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {

  try {

    MappedStatement ms = configuration.getMappedStatement(statement);

    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

  } catch (Exception e) {

    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);

  } finally {

    ErrorContext.instance().reset();

  }

}

Executor根据ExecutorType的不同而创建,最常用的是SimpleExecutor,本文的例子也是创建这个实现类。 一级缓存最重要的代码就是BaseExecutor的query方法:


说明:

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

2,cacheKey是MappedStatement + parameterObject + rowBounds一起构造而成。


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

public int update(MappedStatement ms, Object parameter) throws SQLException {

  clearLocalCache();

  return doUpdate(ms, parameter);

}

public void clearLocalCache() {

  if (!closed) {

    localCache.clear();

    localOutputParameterCache.clear();

  }

}


二,二级缓存

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

二级缓存跟一级缓存不同,一级缓存不需要配置任何东西,且默认打开。 二级缓存就需要配置一些东西。最简单的配置,在mapper文件上加上这句配置即可:<cache/>

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

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

2,mapper配置文件中需要加入<cache>节点;

3,mapper配置文件中的select节点需要加上属性useCache需要为true(默认为true,不设置也行);


testCase1:不同SqlSession,查询相同语句,第一次查询之后commit SqlSession(或者close SqlSession):

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仅进行了一次数据库查询;


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

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执行了两次数据库查询;


源码解读:

我们从mapper文件中加入的<cache/>中开始分析源码,接下来我们看下这个cache的解析,XMLMappedBuilder的解析方法如下:

private void configurationElement(XNode context) {

  try {

    String namespace = context.getStringAttribute("namespace");

    if (namespace == null || 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);

  }

}

其中cacheElement(context.evalNode("cache"));就是解析cache节点的代码,其中最重要的功能就是构造一个Cache实例并加入到Configuration。

public Cache useNewCache(Class<? extends Cache> typeClass,

    Class<? extends Cache> evictionClass,

    Long flushInterval,

    Integer size,

    boolean readWrite,

    boolean blocking,

    Properties props) {

  Cache cache = new CacheBuilder(currentNamespace)

      .implementation(valueOrDefault(typeClass, PerpetualCache.class))

      .addDecorator(valueOrDefault(evictionClass, LruCache.class))

      .clearInterval(flushInterval)

      .size(size)

      .readWrite(readWrite)

      .blocking(blocking)

      .properties(props)

      .build();

  configuration.addCache(cache);

  currentCache = cache;

  return cache;

}

接下来再构造MapperStatement对象时,就会把currentCache参数传给它。

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)

    .resource(resource)

    .fetchSize(fetchSize)

    .timeout(timeout)

    .statementType(statementType)

    .keyGenerator(keyGenerator)

    .keyProperty(keyProperty)

    .keyColumn(keyColumn)

    .databaseId(databaseId)

    .lang(lang)

    .resultOrdered(resultOrdered)

    .resulSets(resultSets)

    .resultMaps(getStatementResultMaps(resultMap, resultType, id))

    .resultSetType(resultSetType)

    .flushCacheRequired(valueOrDefault(flushCache, !isSelect))

    .useCache(valueOrDefault(useCache, isSelect))

    .cache(currentCache);


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



小结:使用二级缓存之后:查询数据的话,先从二级缓存中拿数据,如果没有的话,去一级缓存中拿,一级缓存也没有的话再查询数据库。


为什么SqlSession commit或close之后,二级缓存才会生效呢?

public Object getObject(Object key) {

  Object object = delegate.getObject(key);

  if (object == null) {

    entriesMissedInCache.add(key);

  }

  if (clearOnCommit) {

    return null;

  } else {

    return object;

  }

}


public void putObject(Object key, Object object) {

  entriesToAddOnCommit.put(key, object);

}


public void commit() {

  if (clearOnCommit) {

    delegate.clear();

  }

  flushPendingEntries();

  reset();

}


private void flushPendingEntries() {

  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {

    delegate.putObject(entry.getKey(), entry.getValue());

  }

  for (Object entry : entriesMissedInCache) {

    if (!entriesToAddOnCommit.containsKey(entry)) {

      delegate.putObject(entry, null);

    }

  }

}


参考文献

通过源码分析MyBatis的缓存

http://www.cnblogs.com/fangjian0423/p/mybatis-cache.html


以上是关于通过源码分析MyBatis的缓存的主要内容,如果未能解决你的问题,请参考以下文章

通过源码分析MyBatis的缓存

mybatis源码分析之05一级缓存

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

mybatis源码分析之06二级缓存

MaBatis学习---源码分析MyBatis缓存原理

MyBatis源码分析五MyBatis的缓存