1、简介
MyBatis提供了支持一级缓存和二级缓存。
一级缓存:基于PerpetualCache 的 HashMap本地缓存,其存储作用域是Session,当session flush或者close时,该session中的cache全部清空。
二级缓存:与一级缓存机制类似,也是使用的PerpetualCache的HashMap实现,其作用域是mapper.xml(namespace),可自定义存储源,比如Encache。
更新机制:只要在作用域内进行了增删改操作后,该作用域所有的select的cache全部clear。
PerpetualCache源码:
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
2、MyBatis的一级缓存
作用域是Session,当session flush或者close时,该session中的cache全部清空,默认开启。
注:a、单独使用MyBatis而不继承Spring时,使用原生的Mybatis的SqlSessionFactory来创建session,一级缓存有效。
b、集成spring(使用Mybatis-spring)时,每次查询spring会重新创建sqlSession,所以一级缓存不生效。但是开启事务时,在一个事务中,使用的是一个sqlSession,这时一级缓存生效。
3、MyBatis的二级缓存
二级缓存的使用:
全局缓存,其作用域为 Mapper(Namespace),默认关闭。
a、在mybatis.xml中配置开启缓存
<setting name="cacheEnabled" value="true"/>
b、在XXXmapper.xml文件中配置缓存:
<mapper namespace="com.yanwei.mapper.UserMapper"> <!-- Cache 配置 --> <cache
blocking=""eviction ="FIFO" flushInterval ="60000" size ="512" readOnly ="true"
type ="" /> </ mapper >
- blocking:true或者false,Blocking=true,那么会调用BlockingCache,则针对同一个CacheKey,拿数据与放数据、删数据是互斥的。
- eviction:缓存回收策略,默认就是LRU.
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
- flushInterval:刷新间隔。默认无刷新间隔,只有调用语句时刷新。
- size:缓存的对象数目。默认值是 1024
- readonly:属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例
- type:缓存类,可以自定义缓存,实现Cache,默认是"PERPETUAL",也就是org.apache.ibatis.cache.impl.PerpetualCache
c、XXXmapper.xml文件中sql配置缓存
<select ... flushCache="false" useCache="true"/> <insert ... flushCache="true"/> <update ... flushCache="true"/> <delete ... flushCache="true"/>
- flushCache:true或false,刷新缓存,也就是清除缓存。
useCache:是否使用缓存,true使用cache,false不使用cache。
d、引用其他命名空间的缓存策略:
<cache-ref namespace="com.yanwei.mapper.OtherMapper"/>
二级缓存的实例化过程:
读取mapper.xml中的<cache/>进行实例化,调用XMLMapperBuilder的cacheElement方法:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
这段代码获取xml中的cache元素,并且设置cache元素的属性默认值。后面两行是获取设置的默认值,并进行创建Cache,下面看最后一行调用的userNewCache方法。
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;
}
这里用了创建型模式-生成器,其中调用的properties(pros)是封装属性值,build()进行生成对象。下面看build()方法:
public Cache build() {
setDefaultImplementations();
//我标注的注释:构建基础的缓存,implementation指的是type配置的值,这里是默认的PerpetualCache
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
//我标注的注释:如果是PerpetualCache,根据eviction进行继续装饰,到这一步,给PerpetualCache加上了LRU的功能
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;
}
接下来看标准装饰(setStandardDecorators)的源码实现:
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);
}
}
这个方法中是根据配置参数进行装饰:
- 如果配置了flushInterval,那么继续装饰为ScheduledCache,这意味着在调用Cache的getSize、putObject、getObject、removeObject四个方法的时候都会进行一次时间判断,如果到了指定的清理缓存时间间隔,那么就会将当前缓存清空
- 如果readWrite=true,那么继续装饰为SerializedCache,这意味着缓存中所有存储的内存都必须实现Serializable接口
- 跟配置无关,将之前装饰好的Cache继续装饰为LoggingCache与SynchronizedCache,前者在getObject的时候会打印缓存命中率,后者将Cache接口中所有的方法都加了Synchronized关键字进行了同步处理
- 如果blocking=true,那么继续装饰为BlockingCache,这意味着针对同一个CacheKey,拿数据与放数据、删数据是互斥的,即拿数据的时候必须没有在放数据、删数据
二级缓存说明:
1. 映射语句文件中的所有select语句将会被缓存,内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象。
2. 映射语句文件中的所有insert,update和delete语句会刷新缓存,也就是清空该namespace下的缓存。
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;
}
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;
}
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;
}
3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
4. 缓存会根据指定的时间间隔来刷新。
5. 缓存会存储1024个对象
6、假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存。
7、二级缓存的创建生成策略是使用的装饰器模式实现的。
8、二级缓存必须保证所有的增删查改都在同一个命名空间才可以。
4、自定义MyBatis缓存
实现org.apache.ibatis.cache.Cache接口自定义缓存,或者其他第三方缓存方案创建适配器来完成覆盖缓存行为。
在mapper.xml中引入缓存时,type定义为自己的自定义的缓存类,或者第三方缓存类。
<mapper namespace="dao.userdao"> <!-- Cache 配置 --> <cache
type="com.yanwei.utils.MyOwnCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"><property name="cacheFile" value="/tmp/my-owm-cache.tmp"/>
</cache>
</ mapper > 配置自己的缓存,通过 cache元素来传递属性,上面代码会在自定义缓存实现中调用一个称为“setCacheFile(String file)”的方法
底层org.apache.ibatis.cache.Cache接口的源码:
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
/**
* SPI缓存提供者
* 为每个名称空间创建一个缓存实例
* 缓存实现必须有一个作为字符串参数接收缓存id的构造函数
* Mybatis将把名称空间最为id传递给构造函数。
*
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
*
* @author Clinton Begin
*/
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();
}
自定义的缓存类:
package com.yanwei.utils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
public class MyOwnCache implements Cache{
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
//必须要有这个构造方法,详见Cache接口注释
public MyOwnCache(String id){
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(cache);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
public boolean equals(Object o){
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
}