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

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

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

@Override
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, boundSql);
        // 注意:看这里!! 从二级缓存中查询数据
        List<E> list = (List<E>) 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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容