缓存能够说是提高性能的标配,操做系统,cpu,各类各样的框架咱们总能看到缓存的身影,固然Mybatis也不例外,Mybatis提供了强大的缓存功能,分别有一级缓存和二级缓存,接下来咱们来作一一介绍。java
在深刻以前咱们先看看Mybatis都提供了哪些缓存的配置,方便开发者使用,能够大体归为三类配置,下面分别详细说明:git
setting配置中包含了相关缓存的配置有:cacheEnabled和localCacheScope;
cacheEnabled:全局地开启或关闭配置文件中的全部映射器已经配置的任何缓存,默认值true;
localCacheScope:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种状况下会缓存一个会话中执行的全部查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不一样调用将不会共享数据,默认为session。github
XML映射文件包括select标签和insert/update/delete标签两类;select标签包括flushCache和useCache,另外三个只有useCache:
flushCache:将其设置为true后,只要语句被调用,都会致使本地缓存和二级缓存被清空,默认值:false;
useCache:将其设置为true后,将会致使本条语句的结果被二级缓存缓存起来,默认值:对select元素为true。redis
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
默认是没有开启二级缓存的,只有一级缓存,而且缓存范围是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返回了不一样的对象;安全
一样执行以上的程序,结果以下:
com.mybatis.vo.Blog@2b71e916 com.mybatis.vo.Blog@13c9d689 com.mybatis.vo.Blog@4afcd809
由于设置了只要语句被调用,都会致使本地缓存,因此获取的对象都是不一样的;
一样执行以上的程序,结果以下:
com.mybatis.vo.Blog@2b71e916 com.mybatis.vo.Blog@13c9d689 com.mybatis.vo.Blog@4afcd809
设置值为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不一样调用将不会共享数据,因此获取的对象都是不一样的;
一样执行以上的程序,结果以下:
com.mybatis.vo.Blog@6771beb3 com.mybatis.vo.Blog@6771beb3 com.mybatis.vo.Blog@411f53a0
能够发现此配置对一级缓存并不起做用,只做用于二级缓存;
在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
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的缓存机制;
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提供了多个实现类,具体以下图所示:
FifoCache,LruCache,SoftCache,WeakCache:这四个是能够在cache标签里面配置的策略eviction默认为LruCache;
其余的缓存是不能看成缓存策略来配置的,他们主要被用来看成以上四种策略的补充,配合四种策略使用的:
BlockingCache:在咱们设置blocking="true"时会自动使用此缓存,用来防止多个线程同时执行相同sql,查询屡次数据库的问题;
LoggingCache:为缓存提供日志功能的;
SerializedCache:当缓存有读写功能的时候,提供序列化功能;
ScheduledCache:若是配置了刷新间隔flushInterval,提供检查是否到刷新时间;
SynchronizedCache:提供同步功能synchronized关键字;
PerpetualCache:提供缓存最基本,最纯粹的功能,内置HashMap保存数据;能够说以上配置的四种策略都由此类提供保存功能;一级缓存就是直接使用此类;
TransactionalCache:提供事务管理机制;
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便可删除缓存。
上面咱们介绍到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缓存的相关配置项,一一介绍;而后经过改变各类参数进行一一验证,并从源码层面进行分析重点分析了一级缓存,二级缓存;最后介绍了自定义缓存,以及官方提供的一下扩展缓存实现。