Mybatis之缓存分析

前言

缓存能够说是提高性能的标配,操做系统,cpu,各类各样的框架咱们总能看到缓存的身影,固然Mybatis也不例外,Mybatis提供了强大的缓存功能,分别有一级缓存和二级缓存,接下来咱们来作一一介绍。java

缓存配置

在深刻以前咱们先看看Mybatis都提供了哪些缓存的配置,方便开发者使用,能够大体归为三类配置,下面分别详细说明:git

1.setting配置

setting配置中包含了相关缓存的配置有:cacheEnabled和localCacheScope;
cacheEnabled:全局地开启或关闭配置文件中的全部映射器已经配置的任何缓存,默认值true;
localCacheScope:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种状况下会缓存一个会话中执行的全部查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不一样调用将不会共享数据,默认为session。github

2.statement配置

XML映射文件包括select标签和insert/update/delete标签两类;select标签包括flushCache和useCache,另外三个只有useCache:
flushCache:将其设置为true后,只要语句被调用,都会致使本地缓存和二级缓存被清空,默认值:false;
useCache:将其设置为true后,将会致使本条语句的结果被二级缓存缓存起来,默认值:对select元素为true。redis

3.cache标签

XML映射文件能够包含cache和cache-ref两类标签:
cache:对给定命名空间的缓存配置,要启用全局的二级缓存,只须要在你的SQL映射文件中添加一行<cache/>,固然里面也包含一些自定义的属性,以下完整的配置:sql

<cache 
        blocking="true" 
        eviction="FIFO" 
        flushInterval="60000"
        readOnly="true" 
        size="512" 
        type="org.apache.ibatis.cache.impl.PerpetualCache">
    </cache>

eviction:清除策略常见的有:LRU,FIFO,SOFT,WEAK;默认的清除策略是LRU;
flushInterval:(刷新间隔)属性能够被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认状况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新;
size:(引用数目)属性能够被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024;
readOnly:(只读)属性能够被设置为 true 或 false。只读的缓存会给全部调用者返回缓存对象的相同实例。 所以这些对象不能被修改。这就提供了可观的性能提高。而可读写的缓存会(经过序列化)返回缓存对象的拷贝。 速度上会慢一些,可是更安全,所以默认值是 false;
blocking:当在缓存中找不到元素时,它设置对缓存键的锁定;这样其余线程将等待此元素被填充,而不是命中数据库;
type:指定缓存器类型,能够自定义缓存;数据库

cache-ref:对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可使用 cache-ref 元素来引用另外一个缓存。apache

缓存测试

1.默认配置

默认是没有开启二级缓存的,只有一级缓存,而且缓存范围是SESSION,flushCache为false语句被调用,不会致使本地缓存;segmentfault

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config-sourceCode.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper<Blog> mapper = session.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlog(160));
            // 默认开启一级缓存,在参数和sql相同的状况下,只执行一次sql
            System.out.println(mapper.selectBlog(160));
        } finally {
            session.close();
        }
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper<Blog> mapper = session2.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlog(160));
        } finally {
            session.close();
        }
    }

分别建立了2个session,第一个session连续查询了两次,第二session查询了一次结果以下:缓存

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689

由于开启了一级缓存而且缓存范围是SESSION,因此session1的两次查询返回同一个对象;而不一样的session2返回了不一样的对象;安全

2.flushCache为true

一样执行以上的程序,结果以下:

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689
com.mybatis.vo.Blog@4afcd809

由于设置了只要语句被调用,都会致使本地缓存,因此获取的对象都是不一样的;

3.localCacheScope设置为STATEMENT

一样执行以上的程序,结果以下:

com.mybatis.vo.Blog@2b71e916
com.mybatis.vo.Blog@13c9d689
com.mybatis.vo.Blog@4afcd809

设置值为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不一样调用将不会共享数据,因此获取的对象都是不一样的;

4.cacheEnabled设置为false

一样执行以上的程序,结果以下:

com.mybatis.vo.Blog@6771beb3
com.mybatis.vo.Blog@6771beb3
com.mybatis.vo.Blog@411f53a0

能够发现此配置对一级缓存并不起做用,只做用于二级缓存;

5.配置cache标签

在xxMapper.xml中配置<cache />,一样执行以上的程序,结果以下:

com.mybatis.vo.Blog@292b08d6
com.mybatis.vo.Blog@292b08d6
com.mybatis.vo.Blog@24313fcc

为何已经设置了二级缓存,获取的对象仍是不同;主要缘由是cache中默认的readOnly属性为false,也就是说会返回缓存对象的拷贝,全部这里对象不一致,但其实并无再次查询数据库;再次设置readOnly属性以下所示:

<cache readOnly="true"/>

再次运行,能够发现全部对象都是同一个,结果以下:

com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480

注:须要注意的是若是设置readOnly=true须要注意其余线程修改此对象值,进而影响当前线程的对象值,由于全部线程都是共享的同一个对象,若是设置为false那么其余线程获取的对象都是拷贝,不会影响当前线程数据。
映射语句文件中的全部insert、update和delete语句会刷新缓存,在session2查询以前执行更新操做以下:

SqlSession session21 = sqlSessionFactory.openSession();
try {
    BlogMapper mapper = session21.getMapper(BlogMapper.class);
    Blog blog = new Blog();
    blog.setId(158);
    blog.setTitle("hello java new");
    mapper.updateBlog(blog);
    session21.commit();
} finally {
    session21.close();
}

再次运行,缓存已经被清除,获取新的对象,结果以下:

com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@6c6cb480
com.mybatis.vo.Blog@4b2bac3f

6.配置cache属性blocking="true"

blocking=true的状况下其余线程将等待此元素被填充,而不是命中数据库;能够简单作个测试,首先不设置blocking,而后分别建立两个线程分别查询selectBlog:

new Thread(new Runnable() {
        @Override
        public void run() {
              ...selectBlog...
       }
}).start();
new Thread(new Runnable() {
        @Override
        public void run() {
              ...selectBlog...
        }
}).start();

结果以下,每一个线程都查询了一次数据库,这样若是是很费时的sql,起不到缓存的做用:

com.mybatis.vo.Blog@4eebc002
com.mybatis.vo.Blog@354d6d02

注:若是以上查询出来是两个相同的结果,能够增长相关查询的sql时间,这样效果更加明显;修改配置设置blocking="true",配置以下:

<cache readOnly="true" blocking="true" />

再次运行结果以下:

com.mybatis.vo.Blog@f9ecfca
com.mybatis.vo.Blog@f9ecfca

能够发现对象是同一个,说明只查询了一次数据库;

缓存分析

上节中对一级缓存和二级缓存经过实例测试的方式,详细结束了如何使用,以及注意点;本节从源码入手,更加深刻的了解mybatis的缓存机制;

1.缓存类型

Mybatis提供了缓存接口类Cache,具体以下所示:

public interface Cache {
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();
}

提供了put,get,remove操做,同时还提供了清除,获取大小,获取读写锁功能;基于此接口Mybatis提供了多个实现类,具体以下图所示:
image.png
FifoCache,LruCache,SoftCache,WeakCache:这四个是能够在cache标签里面配置的策略eviction默认为LruCache;

  • LRU– 最近最少使用:移除最长时间不被使用的对象。
  • FIFO– 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT– 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

其余的缓存是不能看成缓存策略来配置的,他们主要被用来看成以上四种策略的补充,配合四种策略使用的:
BlockingCache:在咱们设置blocking="true"时会自动使用此缓存,用来防止多个线程同时执行相同sql,查询屡次数据库的问题;
LoggingCache:为缓存提供日志功能的;
SerializedCache:当缓存有读写功能的时候,提供序列化功能;
ScheduledCache:若是配置了刷新间隔flushInterval,提供检查是否到刷新时间;
SynchronizedCache:提供同步功能synchronized关键字;
PerpetualCache:提供缓存最基本,最纯粹的功能,内置HashMap保存数据;能够说以上配置的四种策略都由此类提供保存功能;一级缓存就是直接使用此类;
TransactionalCache:提供事务管理机制;

2.一级缓存

Mybatis默认开启一级缓存,使用的是PerpetualCache做为缓存工具类,内部就是一个最简单的HashMap,使用CacheKey做为Map的key,value就是查询处理的数据;相关功能能够参考BaseExecutor的query方法:

public <E> List<E> 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<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) 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;
  }

首先断定是否开启了flushCache开关,以前说过若是开启了开关,则每次查询都会清除缓存,可是这里加了另一个条件,必须queryStack==0的状况下;从名字能够大体猜想出就是查询的堆栈深度,查询以前+1,结束以后-1;为何须要等于0,大概猜想一下多是为了防止在查询的过程当中,有其余线程进来直接把缓存给清掉了;
而后把queryStack+1,而且只有在没有设置resultHandler的状况下才会从本地缓存里面获取值,不然不会从缓存获取,直接查询数据库;结束时queryStack-1,本次查询结束,queryStack归0;
最后一样是在queryStack==0的状况下处理延迟加载,以及缓存范围若是是STATEMENT,则清除缓存数据;
总结一下:一级缓存是默认开启的,也没有开关对其进行关闭,惟一的两个参数分别是localCacheScope和flushCache用来控制删除缓存,固然session关闭的时候也会清除缓存;另一个问题就是为何本地缓存没有引入删除策略好比lru等,可能仍是由于session的生命周期比较短,关闭session便可删除缓存。

3.二级缓存

上面咱们介绍到cacheEnabled==true的状况下才会开启二级缓存,默认为true;在configuration中会建立Executor,以下所示:

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

executorType在configuration中设置,以下所示:

<!-- 执行器类型:SIMPLE,REUSE.BATCH -->
<setting name="defaultExecutorType" value="SIMPLE" />

若是没有设置executorType,默认为ExecutorType.SIMPLE;能够看到建立完Executor以后会判断cacheEnabled是否为true,只有为true才会建立CachingExecutor,此类是专门用来处理二级缓存的;固然并非设置了cacheEnabled就开启了二级缓存,还必须设置cache标签,否则一样不会开启二级缓存;具体看CachingExecutor中的查询功能:

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

首先获取缓存实现类,若是没有配置cache标签,这里获取的实现类就为null,因此就直接查询数据库;若是不为null,则先判断是否开启了flushCache功能,能够发现此功能不只用在一级缓存,一样用在二级缓存,若是设置为则直接清除缓存;
接下来会判断select标签是否开启了useCache功能,默认是开启的;同时还须要没有设置resultHandler,这一点和本地缓存同样;
最后就是查询数据而后放入缓存中,这里并无直接用获取的Cache实现类去get/put操做,而是外层有一个包装缓存类TransactionalCache,也就是默认开启了事务;

下面看一下是如何获取Cache的,这个主要和xxxMapper.xml中配置的cache标签有关;Cache实现类在MapperBuilderAssistant中实现,具体以下:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

能够发现这里设置的参数基本和cache标签里面设置的一致:
implementation:对应cache标签中的type,若是没有设置默认为PerpetualCache;
addDecorator:对应cache标签中的eviction,也就是清除策略,默认是LruCache;
clearInterval:对应cache标签中的flushInterval,默认状况是不设置,也就是没有刷新间隔;
readWrite:对应cache标签中的readOnly,默认为false,支持读写功能;
blocking:对应cache标签中的blocking,默认为false;

最后执行build方法,具体代码以下:

public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
  
    private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

首先newBaseCacheInstance,这里默认的implementation其实就是PerpetualCache,固然如何这里指定了自定义的缓存类型,就直接返回用户自定义的类型了;若是没有指定那么继续往下会newCacheDecoratorInstance,这里的decorators就是配置的eviction,默认是LruCache,同时包含了默认的PerpetualCache;
而后执行setStandardDecorators方法,这个方法其实就是判断用户是否配置了相关的参数好比:flushInterval,readOnly,blocking等,每一个新的缓存实例都会包含原来的实例,相似装饰者模式;具体每一个缓存实例这里就不过多介绍了,反正就是每一个实现一个功能,最后就是把全部功能过滤一遍,有点像过滤器;

自定义缓存

从上面的内容中咱们能够知道,能够在cache标签中设置type类型,这里其实就能够指定自定义的缓存类型了;而且咱们在分析二级缓存源码的时候若是type类型不是PerpetualCache实现类,那么就不会有下面的setStandardDecorators,直接返回用户自定义的缓存,不少功能就没有了,因此自定义缓存仍是要当心谨慎;
固然简单实现一个自定义的缓存仍是比较简单的,实现接口Cache便可;好比咱们经常使用的redis,Memcached,EhCache等作缓存,其实也能够经过扩展做为Mybatis的二级缓存,Mybatis官方也提供了实现:二级缓存扩展,咱们只须要引入jar便可:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-memcached</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

引入相关jar包之后,只须要在Cache标签中配置type类型便可:

type="org.mybatis.caches.memcached.MemcachedCache"
type="org.mybatis.caches.redis.RedisCache"
type="org.mybatis.caches.ehcache.EhcacheCache"

总结

本文首先介绍了Mybatis缓存的相关配置项,一一介绍;而后经过改变各类参数进行一一验证,并从源码层面进行分析重点分析了一级缓存,二级缓存;最后介绍了自定义缓存,以及官方提供的一下扩展缓存实现。

示例代码地址

Github

相关文章
相关标签/搜索