前言
缓存可以说是提升性能的标配,操作系统,cpu,各种各样的框架我们总能看到缓存的身影,当然Mybatis也不例外,Mybatis提供了强大的缓存功能,分别有一级缓存和二级缓存,接下来我们来做一一介绍。
缓存配置
在深入之前我们先看看Mybatis都提供了哪些缓存的配置,方便开发者使用,可以大致归为三类配置,下面分别详细说明:
1.setting配置
setting配置中包含了相关缓存的配置有:cacheEnabled和localCacheScope;
cacheEnabled:全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存,默认值true;
localCacheScope:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据,默认为session。
2.statement配置
XML映射文件包括select标签和insert/update/delete标签两类;select标签包括flushCache和useCache,另外三个只有useCache:
flushCache:将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false;
useCache:将其设置为true后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对select元素为true。
3.cache标签
XML映射文件可以包含cache和cache-ref两类标签:
cache:对给定命名空间的缓存配置,要启用全局的二级缓存,只需要在你的SQL映射文件中添加一行<cache/>,当然里面也包含一些自定义的属性,如下完整的配置:
<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 元素来引用另一个缓存。
缓存测试
1.默认配置
默认是没有开启二级缓存的,只有一级缓存,并且缓存范围是SESSION,flushCache为false语句被调用,不会导致本地缓存;
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提供了多个实现类,具体如下图所示:
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缓存的相关配置项,一一介绍;然后通过改变各种参数进行一一验证,并从源码层面进行分析重点分析了一级缓存,二级缓存;最后介绍了自定义缓存,以及官方提供的一下扩展缓存实现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。