Mybatis源码学习(十一):二级缓存流程分析

一、前文回顾

在上一篇文章中我们学习了Mybatis的二级缓存,并对其做了相对详细的分析。今天我们接着上文继续从头到尾走一遍二级缓存的流程。

二、前置代码

public static void main(String[] args) throws Exception {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //创建SqlSession工厂
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    //通过SqlSessionFactory创建SqlSession (try-with-resource,会自动关闭连接)
    try (SqlSession session = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession()) {
        //通过Sqlsession1 获取mapper
        CompanyMapper companyMapper = session.getMapper(CompanyMapper.class);
        //通过Sqlsession2 获取mapper
        CompanyMapper companyMapper2 = session2.getMapper(CompanyMapper.class);

        Company condition = new Company();
        condition.setCompanyName("Test Company Name");
        condition.setCompanyNo("123");
        companyMapper.list(condition);

        //提交事务!!!一定要提交,不然二级缓存不生效
        session.commit();

        companyMapper2.list(condition);

    }
}

image.png

三、带着问题Debug

我们知道Mybatis中执行相关操作的是Executor类,本次采用查询作为Debug的例子,所以我们把断点打在Executor的Query方法上。

问题1:Mybatis是如何选择Executor的

我们知道在Mybatis中有许多类型的Executor,那么Mybatis是如何选择的呢。就以二级缓存为例,为什么会使用CachingExecutor呢。要回答这个问题我们还是得从源码下手。
image.png
现在知道了Mybatis是如何选择Executor了,那么我们即将开始Debug。这次就直接在CachingExecutor的Query方法打断点。

  @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);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

源码分析:
(1)首先Mybatis从MappedStatmenet中获取缓存,你也可以认为缓存是个MappedStatement是绑定的或者和nameSpace绑定。
(2)判断是否需要刷新缓存,这个是需要刷新缓存是我们在mapper文件中写的,例如以下mapper。

//中间省略

(3)判断是否使用了cache,以及是否有resultHandler。如果满足条件则去缓存中先查询是否有数据,有的话返回没有的话继续查询数据库。

问题2:为什么提交了事务才能使二级缓存生效

继续分析,我们可以发现mybatis是从tcm这个对象中获取缓存数据,这个对象为 TransactionalCacheManager,该类用于处理缓存事务相关。还记得之前说的使用二级缓存的条件吗?其中就有一条,必须提交事务才会生效。那这是为什么呢?继续分析源码:
image.png
我们回到最开始的代码,从提交事务的地方开始debug,在
DefaultSQLSession
中有一个commit方法,他调用的是Executor的commit方法,而经过上述分析我们知道此时的Executor为CachingExecutor
image.png
进入CachingExecutor的commit方法,发现调用的是tcm的commit方法,继续Debug,最终来到了TransactionalCache的Commit方法。其中有一个flushPendingEntries()方法
image.pngimage.png
所以这个问题的答案就是,只有当sqlsession提交了事务之后,mybatis才会把数据写入缓存。那么为什么要在事务提交后才存入缓存呢?对此我个人的理解是这样的,比如一个sqlSession执行了多条SQL,如果事务还没提交就存入了缓存中,此时事务回滚了,就会造成脏数据(当然,也可以事务回滚一同删除缓存,这不过这样相对来说比较麻烦)

问题3:为什么在Query的时候也有一次添加缓存操作

在Debug的过程中,我们会发现在Query的过程中其实是有一次添加缓存的操作,那为什么我们说只有当事务提交的时候才会更新缓存呢?这是不是冲突了呢?带着这个问题我们进行Debug,源码能说明一切。
image.png
抛出问题:当执行了tcm.putObject(cache, key, list);后,理论上来说此时缓存已经添加了,那么我通过tcm.getObject(cache, key);方法应该是可以拿到缓存的,事实上是这样吗?让我们Debug一下
image.png
很明显为空,这似乎不太符合常理。按理来说我们已经把数据写入缓存了,为什么结果为null呢。这就要从Mybatis如何存/取缓存说起。


public class TransactionalCache implements Cache {
    /**
    *删除多余的代码,只保留了putObject方法
    */
  private final Map<Object, Object> entriesToAddOnCommit;

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }
}

可以看到Myabtis会先把缓存放到entriesToAddOnCommit这个Map中,从名字中我们就可以得知,这个
Map的作用是当事务提交的时候添加缓存。接下来在看看是怎么取的。


public class TransactionalCache implements Cache {
    /**
    *删除多余的代码,只保留getObject
    */
 private final Set<Object> entriesMissedInCache;

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

}

而取的时候则是从delegate(委托类,最终是PerpetualCache)去获取,这也就解释了为什么Query的时候也“写入了缓存”,但是为什么取不到。当事务提交后,Myabtis会把entriesToAddOnCommit 中的数据写入PerpetualCache中。为了帮助你理解我这里画了一个图,可能不是非常的准确。
image.png

四、总结

至此Mybatis缓存相关的只是我个人认为是讲的差不多了,当然还有很多地方没有涉及到,可能也有讲错的地方。个人看法,实际工作中一般不会使用二级缓存,理由如下:
1、首先Myabtis自带的缓存底层采用的是HashMap,我们知道HashMap并不是一个线程安全的类。
2、当有连表操作的时候,例如left join的时候相对来说会比较复杂,处理不好容易出现脏数据。
3、现在有更好的缓存中间件例如redis。
当然这是我一家之言,如果有不对的地方欢迎指出。接下来的内容会涉及到Mybatis 拦截器相关的知识,希望对你有所帮助。

未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值