mybatis源码分析——缓存的原理

 

  mybatis缓存有一级缓存和二级缓存,一级缓存的作用域是sqlSession,在一次会话内,默认是开启的,如果在一次会话内,查询的sql、参数相同,则

会从缓存中取数据,执行dml操作会清楚缓存,二级缓存的作用域是sqlSessionFactory,默认是关闭的,需要在mybatis-config.xml指定开启,在一个会话完成后,

会将所有的select的查询数据缓存,其他的会话如果以相同的sql和参数查询,有能够从缓存中拿到结果。

 

一:一级缓存的使用

测试用例如下:在同一个sqlSession会话内,执行两次相同的查询操作

    public static void main(String[] args) throws IOException {
        // 将mybatis-config的配置文件读入内存,生成字符流对象
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");

        // 解析全局配置文件mybatis-config.xml
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(reader);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //       PageHelper.startPage(1,2);

        // 测试一级缓存:
        List list = userMapper.selectUser("hello105");
        System.out.println("第一次查询结果:" + list.size());

        List list2 = userMapper.selectUser("hello105");
        System.out.println("第二次查询结果:" + list2.size());
    }

  

 

 

因为一级缓存默认开启,且缓存key值相同,从结果可以看到,第二次没有执行数据库select的操作,直接从缓存拿的数据。

二:从源码层面分析一下一级缓存

  1:在看查询缓存之前,我们先来看一下Executor的创建,这个是SqlSessionFactory中的方法,看一下configuration.newExecutor

  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);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } 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();
    }
  }

 从代码可以看出,创建完成一个executor对象后,会把它包装成一个cacheExecutor,因为cacheEnabled默认是开启的 

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

  

  protected boolean cacheEnabled = true;

  

来看一下构造函数里面的逻辑:

executor = new CachingExecutor(executor);

  

会把真正的executor维护到cacheExecutor的属性delegate上。

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  

2:executor的创建完成后,我们来看一下怎么缓存的,来到DefaultSqlSession类,因为要执行查询操作

  public  List 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();
    }
  }

  

这里会根据sql语句、参数、命名空间生成一个缓存key值

  public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  

调用query方法,首先会判断mapper.xml或者mapper类上是否开启了二级缓存(sqlSessionFactory全局),这里我们没有开启,会跳过if语句,直接执行查询,delegate.query

就是创建execcutor的时候封装进去的SimpleExecutor对象

  public  List 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);
        @SuppressWarnings("unchecked")
        List list = (List) 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
        }
        return list;
      }
    }
    return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  

这里的查询,首先会到一级缓存localCache,第一次查询localCache中没有数据,返回null,然后会调用queryFromDatabase接口,去真正的执行数据库的查询操作

  public  List 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();
    }
    List list;
    try {
      queryStack++;
      list = resultHandler == null ? (List) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

  

这个方法,首先设置了一个默认值放入localCache,查询完成后删除key值,然后将查询结果list放入缓存localCache

  private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

  

所以当第二次执行查询操作时,发现key值相同,就会到一级缓存中去查询,这样就会出现日志中查询两次,但是只会执行一次数据库操作的现象了

mybatis源码分析——缓存的原理_第1张图片

 

 

 一级缓存的源码就是这样,下面来看一下二级缓存

 

三:二级缓存的使用

mybatis-config.xml可以配置也可以不配置

    
        
        
        
    

  

在mapper接口类配置注解或者在mapper.xml文件中配置cache标签

 

 

 

测试用例:创建2个不同的sqlSession,但是查询sql和参数相同

public static void main(String[] args) throws IOException {
        // 将mybatis-config的配置文件读入内存,生成字符流对象
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");

        // 解析全局配置文件mybatis-config.xml
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(reader);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //       PageHelper.startPage(1,2);

        // 测试一级缓存:
        List list = userMapper.selectUser("hello105");
        System.out.println("第一次查询结果:" + list.size());

//        List list2 = userMapper.selectUser("hello105");
//        System.out.println("第二次查询结果:" + list2.size());

        // 只有sqlSession关闭时,数据才会缓存到二级缓存
        sqlSession.close();

        // 测试二级缓存:
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("另外一个会话查询结果:"+userMapper2.selectUser("hello105").size());

    }

 

执行结果,只查询了1次,第二次查询的缓存:

mybatis源码分析——缓存的原理_第2张图片

 

 

 

看一下注册,mybatis-config.xml中开启时,会在XMLConfigBuilder中解析设置:

mybatis源码分析——缓存的原理_第3张图片

 

 

 不配默认是true:

 

 

 

Mapper类上加注解@CacheNamespace 或者在Mapper.xml中配置Cache标签

mybatis源码分析——缓存的原理_第4张图片

 

 

 

看一下解析:XMLMapperBuilder类中解析命名空间:

mybatis源码分析——缓存的原理_第5张图片

 

 

 mybatis源码分析——缓存的原理_第6张图片

 

 

 mybatis源码分析——缓存的原理_第7张图片

 

 

 解析CacheNamespace注解:

 

 

 然后将解析后的元数据封装到Cache对象中,缓存到configuration中

mybatis源码分析——缓存的原理_第8张图片

 

 

 把cache对象 赋值给了currentCache,而在创建MapperStatement的时候会把currentCache赋值给cache属性

 

 

 

 

  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class parameterType,
      String resultMap,
      Class resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

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

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

 

 

 

下面看看具体的查询,其实在分析一级缓存的时候有涉及到,这里我们详细看一下:

mybatis源码分析——缓存的原理_第9张图片

 

 

 

MappedStatement的getCache方法,如果配置Cache标签或者注解就不为null

 

 

 

 

 

 

默认用到缓存类型是LruCache:

存取数据

mybatis源码分析——缓存的原理_第10张图片

 

 

 

mybatis源码分析——缓存的原理_第11张图片

 

 

 

向二级缓存中放数据是在会话关闭这个动作放的

  

总结:mybatis缓存

  在创建executor的时候,会对executor进行包装,包装成cacheExecutor,在DefaultSqlSession中调用query时,

会路由到CacheExecutor的query方法,首先判断二级缓存是否开启,如果开启,则到二级缓存中查询,看是否命中,

命中cacheKey则返回结果,如果没有命中则委托给simpleExecutor处理,simpleExecutor首先会到一级缓存中查询,如果

命中则返回,没有命中则执行数据库查询操作,从数据库查询到结果后把数据放到一级缓存中,当这个sqlSession关闭的

时候会向二级缓存中缓存数据,大致的查询流程就是这样

 

如果一级和二级缓存都开启,那么在同一个sqlSession内会取一级缓存,其他的sqlSession会到二级缓存中获取

 

  一级缓存 二级缓存
作用域 sqlSession会话内 sqlSessionFactory全局
开启情况 默认开启 需要在mapper接口或者mapper.xml配置
     
     




 

你可能感兴趣的:(mybatis源码分析——缓存的原理)