2.[源码]mybatis二级缓存源码分析(一)----一级缓存与二级缓存的结构关系

上一篇我们介绍了mybatis的一级缓存, 讲解了一级缓存与会话的关系, 一级缓存的生命周期, 一级缓存查询执行的过程等, 其中也有提到二级缓存的地方, 但是都暂且略过了, 而今天这次我们就要来嗑一嗑mybatis二级缓存与一级缓存的关系 ~ 友情提示: 搭配 [https://www.jianshu.com/p/837e6d92e747)食用更香。

NO.1 |思维发散

二级缓存是用来解决一级缓存不能跨会话共享的问题,范围是namespace级别,可以被多个sqlSession(会话)共享, 生命周期和应用同步。默认关闭。

通过对一级缓存的学习我们知道, 一级缓存是默认开启的, 如果我们同时开启了二级缓存, 那就势必存在一级缓存和二级缓存都要使用的情况。这样一来我们要思考的第一个问题产生了, 一级缓存和二级缓存的执行顺序是怎样的呢?

还是先推断一下, 二级缓存作为一个作用范围更广的缓存(可以跨会话), 从节省资源的角度来设计, 二级缓存肯定是要工作在一级缓存(不能跨会话)之前的。也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。如果你的MyBatis使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取数据,取不到才会去走一级缓存,一级缓存中也取不到, 就会与数据库进行交互了。即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
按照这种执行顺序设计来思考, 一级缓存已经利用BaseExecutor完成了自身功能的实现, 那么二级缓存要加在哪里进行维护,才合适呢? 实际上MyBatis这里用了一个设计模式——装饰器模式来维护二级缓存,实现这个功能的类就是CachingExecutor(缓存执行器)。
tips:装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装

具体是怎么做的呢?MyBatis让CachingExecutor对BaseExecutor进行了包装。CachingExecutor中不仅要实现了二级缓存的功能, 同时也要持有一个基础执行器(BaseExecutor)。当查询请求来临的时候,CachingExecutor会先判断二级缓存是否有缓存结果,如果有就直接返回,如果没有则委派交给自己持有的BaseExecutor实现类,比如SimpleExecutor(简单执行器)来执行查询, 这样就顺理成章的从二级缓存过渡到了一级缓存的执行流程了。最后会把得到结果缓存起来,并且返回给用户。

实际上是否是如此呢, 又到了喜闻乐见的放源码时间。

NO.2 |源码论证

(1)还是要从创建会话讲起。

创建会话的过程中, 会先创建执行器(具体创建执行器过程参见(2)), 这个执行器便是CachingExecutor了。然后把得到的执行器——CachingExecutor交给DefaultSqlSession(会话)来持有。


DefaultSqlSessionFactory:
@Overridepublic SqlSession openSession(ExecutorType execType) {  
  return openSessionFromDataSource(execType, null, false);
}
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);      
    // 注意:看这里!! 创建Executor执行器, 这里创建的是CachingExecutor      
    final Executor executor = configuration.newExecutor(tx, execType);      
    // 创建DefaultSqlSession      
    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();
    }  
}

(2)创建执行器部分源码。

在newExecutor()方法中,首先还是要创建基础执行器(BaseExecutor)的子类, 毕竟一级缓存的逻辑还要依靠它去完成。如果开启了缓存, 最后要创建一个缓存执行器(CachingExecutor), 并把之前创建好的基础执行器(BaseExecutor)的子类作为CachingExecutor的有参构造的参数传入, 让CachingExecutor持有BaseExecutor。并返回CachingExecutor让DefaultSqlSession(会话)来持有。

Configuration:
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;
  }

(3)执行查询部分源码

Mybatis是以sqlSession.selectList()方法, 作为查询的入口。可以看见,在selectList() 方法中,调用了executor.query()方法来获取数据, 而sqlSession持有的Executor正是缓存执行器(CachingExecutor)。也就是说这里调用的是CachingExecutor的query()方法。


DefaultSqlSession: 
private final Configuration configuration;
private final Executor executor;

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

在CachingExecutor的query()方法中, 如果使用了二级缓存并且二级缓存存在, 则先去二级缓存中查找数据, 如果数据存在则返回数据。如果数据不存在,会直接委派给一级缓存进行查询。(二级缓存本身的组件结构和实现逻辑没有写出来, 不要急, 下次会写哒!)

CachingExecutor:
private final Executor delegate;//注意: 看这里!! 这里持有的便是基础执行器的子类
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

@Override
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, boundSql);
        // 注意:看这里!! 从二级缓存中查询数据
        List list = (List) tcm.getObject(cache, key);
        // 注意:看这里!! 二级缓存中没有数据, 委托给BaseExecutor执行
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 委托给BaseExecutor执行
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

到这里就完成了我们整个一级缓存与二级缓存结构关系的介绍。

NO.3 |结构与总结

总结:sqlSession 持有 CachingExecutor, CachingExecutor来完成二级缓存的功能实现,并且持有BaseExecutor , 在二级缓存开启并且查不到数据时(或者二级缓存本身没有开启), 都会委派给BaseExecutor来执行查询。

整体结构图如下:


image.png

怎么样, 现在对mybatis二级缓存与一级缓存的关系是不是有了大概的了解~


image.png

这里除了编程知识分享,同时也是我成长脚步的印记, 期待与您一起学习和进步.

你可能感兴趣的:(2.[源码]mybatis二级缓存源码分析(一)----一级缓存与二级缓存的结构关系)