Mybatis源码分析之二级缓存

最近一周都在研究mybatis源码,其实就mybatis源码相对于Spring框架源码来说,mybatis还是简单的,主要就是封装jdbc,然后应用各种设计模式优化整体架构:如 在mybatis中用到了以下的设计模式,

构建者(创建sqlSession对象用到了)

责任链(mybatis中主要特点之一就是大量的handler,他们就是通过责任链来增加,执行)

装饰者(mybatis中主要就是 Executor   主要这些:单例执行器SimpleExecutor,批量执行器BatchExecutor,以及一个缓存执行器CachingExecutor   )面试如果问执行器,可以简单来说 两类 即BaseExecutor CachingExecutor,具体在看情况回答   下图是idea查看源码,建议大家看这文章的时候,使用idea跟踪源码一步一步分析,看看类图,比较记忆深刻,本人学习源码也是一个一个断点跟踪。

 

下面说下官方的一个mybatis缓存的介绍

MyBatis支持声明式数据缓存(declarative data caching)。当一条SQL语句被标记为“可缓存”后,首次执行它时从数据库获取的所有数据会被存储在一段高速缓存中,今后执行这条语句时就会从高速缓存中读取结果,而不是再次命中数据库。MyBatis提供了默认下基于Java HashMap的缓存实现,以及用于与OSCache、Ehcache、Hazelcast和Memcached连接的默认连接器。MyBatis还提供API供其他缓存实现使用。

重点的那句话就是:MyBatis执行SQL语句之后,这条语句就是被缓存,以后再执行这条语句的时候,会直接从缓存中拿结果,而不是再次执行SQL

这也就是大家常说的MyBatis一级缓存,一级缓存的作用域scope是SqlSession。

MyBatis同时还提供了一种全局作用域global scope的缓存,这也叫做二级缓存,也称作全局缓存。

 

下面给大家演示 一级缓存

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


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

@Test
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次数据库查询:
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}


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

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

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 2(Integer)
<==      Total: 1
User{id=2, name='FFF', age=50, birthday=Sat Dec 06 17:12:01 CST 2014}

不同session,进行相同查询:

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



==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)

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

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


更新操作之后缓存会被清除:

==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==>  Preparing: update USERS SET NAME = ? , AGE = ? , BIRTHDAY = ? where ID = ? 
==> Parameters: format(String), 23(Integer), 2014-10-12 23:20:13.0(Timestamp), 1(Integer)
<==    Updates: 1
==>  Preparing: select * from USERS WHERE ID = ? 
==> Parameters: 1(Integer)
<==      Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}

很明显,结果验证了一级缓存的概念,在同个SqlSession中,查询语句相同的sql会被缓存,但是一旦执行新增或更新或删除操作,缓存就会被清除

 

一级二级缓存结论已经展示,下面带大家看下源码结构如何实现的,其实就看了好多源码情况,一般底层的结构存储缓存就是 map数据结构,具体什么缓存策略 无非就是  FIFO  LRU  LFU   定时,懒加载 什么的。

在分析MyBatis的一级缓存之前,我们先简单看下MyBatis中几个重要的类和接口:

Configuration类:主要MyBatis全局配置信息类

看这个图片是不是大家就豁然开朗了,就是 读取xml  解析各个节点信息,最终将这个解析到的全局信息存放到 configuration对象中

SqlSessionFactory:操作SqlSession的工厂接口,具体的实现类是DefaultSqlSessionFactory

SqlSession接口:执行sql,管理事务的接口,具体的实现类是DefaultSqlSession

Executor接口:sql执行器,SqlSession执行sql最终是通过该接口实现的,常用的实现类有SimpleExecutor和CachingExecutor,这些实现类都使用了装饰者设计模式(io  其实也用到了装饰者,装饰者就是解决了方法的增强)

-------------------------------------------------------------分割线---------------------------------------------------------------------------------------------------

 

基本分析mybatis需要了解的一本概念如果上面大家都清楚了,那么就可以往下面走了,相信我,下面so easy,如果上面还有模糊,请百度~

一级缓存的作用域是SqlSession,那么我们就先看一下SqlSession的select过程:

认真看了上面文章就知道,每个sql的执行其实就是通过 session对象来调用方法,那么session 是怎么来的呢?  1.读取配置文件,生成enviroment对象放入全局对象  configuration对象,  2.由sessionFactory openSession 生成sqlSession对象(构建者模式生成的,通过configuration对象中存储的属性配置)

 

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

 看这里可以先看下  下面的第二张图片  介绍 MappedStatement 这个就是 mybatis对sql解析后 存放的对象

接下来我们看下DefaultSqlSession中的executor接口属性具体是哪个实现类。

DefaultSqlSession的构造过程(DefaultSqlSessionFactory内部)因为sqlSession 是由factory生成的,我们就需要看factory的类实现:

Executor根据ExecutorType的不同而创建,最常用的是SimpleExecutor,本文的例子也是创建这个实现类。 最后我们发现如果cacheEnabled这个属性为true的话,那么executor会被包一层装饰器,这个装饰器是CachingExecutor。其中cacheEnabled这个属性是mybatis总配置文件中settings节点中cacheEnabled子节点的值,默认就是true,也就是说我们在mybatis总配置文件中不配cacheEnabled的话,它也是默认为打开的。


executor = (Executor) interceptorChain.pluginAll(executor);  这行代码就是 应用了责任链设计模式,不在此次解说返回,读者自行百度

 

Executor根据ExecutorType的不同而创建,最常用的是SimpleExecutor,本文的例子也是创建这个实现类。 最后我们发现如果cacheEnabled这个属性为true的话,那么executor会被包一层装饰器,这个装饰器是CachingExecutor。其中cacheEnabled这个属性是mybatis总配置文件中settings节点中cacheEnabled子节点的值,默认就是true,也就是说我们在mybatis总配置文件中不配cacheEnabled的话,它也是默认为打开的。

大家看到这其实 已经看得差不多了,就剩下下面一小点了,总结上面就是  默认给我们生成了个 单例的执行器,然后由于默认配置 cacheEanbled,所有采用装饰者,最终先由cachingExecutor 执行器用了 执行sql

------------------------------------------------------分割线----------------------------------------------------------------------------------------------------------

现在,问题就剩下一个了,CachingExecutor执行sql的时候到底做了什么?

回到sql 执行query方法我们继续跟踪

此时应该看得就是  CachingExecutor 代码中的query方法了

带着这个问题,我们继续走下去(CachingExecutor的query方法):

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, parameterObject, boundSql);
        if (!dirty) {
          cache.getReadWriteLock().readLock().lock();
          try {
            @SuppressWarnings("unchecked")
            List<E> cachedList = (List<E>) cache.getObject(key);
            if (cachedList != null) return cachedList;
          } finally {
            cache.getReadWriteLock().readLock().unlock();
          }
        }
        List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
        return list;
      }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

其中Cache cache = ms.getCache();这句代码中,这个cache实际上就是个二级缓存,由于我们没有开启二级缓存(二级缓存的内容下面会分析),因此这里执行了最后一句话。这里的delegate也就是SimpleExecutor,SimpleExecutor没有Override父类的query方法,因此最终执行了SimpleExecutor的父类BaseExecutor的query方法。

 

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

 

未完待续。。。。。。要和小姐姐吃饭去了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值