mybatis源码解析–mapper解析之cache
cache用法
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis 3 中的缓存实现的很多改进都已经实现了,使得它更加强大而且易于配置。
默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环 依赖也是必须的。要开启二级缓存,需要我们在自己的maper文件中添加
<cache type="" eviction="FIFO" flushInterval="60000" size="512" readOnly="" blocking="">
<property name="" value=""/>
</cache>
#type
表示自定义的缓存的类,没有的情况下会存在一个默认的缓存类PerpetualCache.class。
自定义的缓存类需要有一个以String为参数的构造函数
#eviction
表示缓存的策略(只支持默认的缓存类,自定义的缓存类不支持)
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
#flushInterval
(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。
默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
#size
(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。
默认值是 1024
#readOnly
(只读)属性可以被设置为 true 或 false。
只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。
这会慢一些,但是安全,因此默认是 false
#blocking
是否采用阻塞的方式(加锁)
同时还需要我们在对应的SQL上面调整缓存的使用
<!--根据ID查询-->
<select id="selectById"
resultType="com.liubin.study.mybatis.object.dataobject.SupplierLabelDO"
useCache="true"
flushCache="false">
<include refid="selectFields"/>
<where>
label.id = #{id}
</where>
</select>
#useCache 是否使用缓存。
从缓存中读取,如果存在,那么直接返回。不存在,则查询数据库并更新缓存。
#flushCache 是否更新缓存
强制更新缓存
实现自定义的缓存
对于自定义的缓存,我们需要实现Cache接口,可以看一下Cache接口的内容
public interface Cache {
/**
* 返回缓存的唯一标识
*/
String getId();
/**
* 设置缓存
*/
void putObject(Object key, Object value);
/**
* 换取缓存
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* 清空缓存
*/
void clear();
/**
* 可选的,获取缓存的数量
*/
int getSize();
/**
* 可选的,获取缓存的读写锁
*/
ReadWriteLock getReadWriteLock();
}
我们自行实现一个用mapper存储的缓存
/**
* 自定义实行的mybatis的缓存<br/>
*
* @Type
* @Desc
* @date 2018/8/2
* @Version
*/
public class SimpleCache implements Cache {
private final Logger LOGGER = LoggerFactory.getLogger(SimpleCache.class);
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public SimpleCache(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public void putObject(Object key, Object value) {
LOGGER.info("putObject:{},{}",key,value);
cache.put(key,value);
}
public Object getObject(Object key) {
Object value = cache.get(key);
LOGGER.info("getObject:{},value:{}",key,value);
return value;
}
public Object removeObject(Object key) {
LOGGER.info("removeObject:{}",key);
return null;
}
public void clear() {
LOGGER.info("clear");
}
public int getSize() {
LOGGER.info("getSize");
return 0;
}
public ReadWriteLock getReadWriteLock() {
LOGGER.info("getReadWriteLock");
return null;
}
}
在Mapper.xml中使用自定义的缓存
<!--
自行实现的缓存缓存的配置
-->
<cache type="com.liubin.study.mybatis.cache.SimpleCache" eviction="FIFO" flushInterval="60000" size="512">
</cache>
<!--使用默认的缓存
<cache eviction="FIFO" flushInterval="60000" size="512">
</cache>-->
<!--根据ID查询-->
<select id="selectById"
resultType="com.liubin.study.mybatis.object.dataobject.SupplierLabelDO"
useCache="true">
<include refid="selectFields"/>
<where>
label.id = #{id}
</where>
</select>
测试的代码如下
SqlSessionFactory factory = buildFactory();
SqlSession session = factory.openSession();
try{
ISupplierLabelDAO supplierLabelDAO = session.getMapper(ISupplierLabelDAO.class);
SupplierLabelDO labelDO = supplierLabelDAO.selectById(1L);
System.out.println(labelDO);
labelDO = supplierLabelDAO.selectById(1L);
System.out.println(labelDO);
}catch (Exception e){
LOGGER.error("执行查询SQL失败",e);
}finally {
session.close();
}
执行的结果是
[INFO]-[com.liubin.study.mybatis.cache.SimpleCache] getObject:-534486525:156909814:com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById:0:2147483647:SELECT
label.id,
label.supplier_type,
label.supplier_id,
label.label_type,
label.label_id,
label.label_name,
label.gmt_created,
label.gmt_modified
FROM supplier_label label
WHERE label.id = ?:1:dev,value:null
[DEBUG]-[com.liubin.study.mybatis.object.ISupplierLabelDAO] Cache Hit Ratio [com.liubin.study.mybatis.object.ISupplierLabelDAO]: 0.0
[DEBUG]-[org.apache.ibatis.transaction.jdbc.JdbcTransaction] Opening JDBC Connection
[DEBUG]-[org.apache.ibatis.datasource.pooled.PooledDataSource] Created connection 394721749.
[DEBUG]-[org.apache.ibatis.transaction.jdbc.JdbcTransaction] Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1786f9d5]
[DEBUG]-[com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById] ==> Preparing: SELECT label.id, label.supplier_type, label.supplier_id, label.label_type, label.label_id, label.label_name, label.gmt_created, label.gmt_modified FROM supplier_label label WHERE label.id = ?
[DEBUG]-[com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById] ==> Parameters: 1(Long)
[DEBUG]-[com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById] <== Total: 1
com.liubin.study.mybatis.object.dataobject.SupplierLabelDO@15b3e5b[id=1,supplierType=12,supplierId=1,labelType=1,labelId=1,labelName=1,gmtCreated=Thu Aug 02 22:04:35 CST 2018,gmtModified=Thu Aug 02 22:04:38 CST 2018]
[INFO]-[com.liubin.study.mybatis.cache.SimpleCache] getObject:-534486525:156909814:com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById:0:2147483647:SELECT
label.id,
label.supplier_type,
label.supplier_id,
label.label_type,
label.label_id,
label.label_name,
label.gmt_created,
label.gmt_modified
FROM supplier_label label
WHERE label.id = ?:1:dev,value:null
[DEBUG]-[com.liubin.study.mybatis.object.ISupplierLabelDAO] Cache Hit Ratio [com.liubin.study.mybatis.object.ISupplierLabelDAO]: 0.0
com.liubin.study.mybatis.object.dataobject.SupplierLabelDO@15b3e5b[id=1,supplierType=12,supplierId=1,labelType=1,labelId=1,labelName=1,gmtCreated=Thu Aug 02 22:04:35 CST 2018,gmtModified=Thu Aug 02 22:04:38 CST 2018]
[INFO]-[com.liubin.study.mybatis.cache.SimpleCache] putObject:-534486525:156909814:com.liubin.study.mybatis.object.ISupplierLabelDAO.selectById:0:2147483647:SELECT
label.id,
label.supplier_type,
label.supplier_id,
label.label_type,
label.label_id,
label.label_name,
label.gmt_created,
label.gmt_modified
FROM supplier_label label
WHERE label.id = ?:1:dev,[com.liubin.study.mybatis.object.dataobject.SupplierLabelDO@15b3e5b[id=1,supplierType=12,supplierId=1,labelType=1,labelId=1,labelName=1,gmtCreated=Thu Aug 02 22:04:35 CST 2018,gmtModified=Thu Aug 02 22:04:38 CST 2018]]
[DEBUG]-[org.apache.ibatis.transaction.jdbc.JdbcTransaction] Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1786f9d5]
从执行结果可以看到,在第一次的时候,因为缓存中不存在,去查询了数据库。第二次因为缓存中存在,则没有查询数据库。
cache 源码的解析
之前我们已经知道了对于mapper.xml中所有元素的解析都由XMLMapperBuilder来进行。现在我们深入了解一下对于缓存的解析。
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// 不存在,默认使用PERPETUAL缓存
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 不存在,默认使用LRU的方式
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
// 不存在,默认为false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 不存在,默认为false
boolean blocking = context.getBooleanAttribute("blocking", false);
// 对于参数的解析
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
从上面我们看到,是以硬编码的形式去解析cache标签下面的所有的元素的。对于缓存的构建,则委托给了builderAssistant的useNewCache方法。
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;
}
这里可以看到,对于缓存的构建交由CacheBuilder来进行构建的。我们继续深入。
public Cache build() {
// 设置默认的实现类
setDefaultImplementations();
// 实例化缓存
Cache cache = newBaseCacheInstance(implementation, id);
// 设置缓存的参数
setCacheProperties(cache);
// 自定义的缓存实现类和mybatis提供的缓存实现类的构建
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;
}
从上面我们可以看出,缓存是这里实例化并传入参数的。同时如果缓存的实现类是PerpetualCache,那么会构建一个缓存的装饰器链出来。链尾由PerpetualCache来实现。
如果是自定义的其他的缓存类,则会再外部再包装一个LoggingCache(用来记录缓存的命中概率)的实现。
缓存-设置缓存的默认实现类
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
如果默认实现类为空,那么实现类采用PerpetualCache(使用map实现的缓存),装饰器为空,那么默认使用LruCache(最新使用的缓存-策略实现)。
缓存-实例化缓存实现类
/**
* 实例化缓存
*/
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
try {
return cacheConstructor.newInstance(id);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
}
}
/**
* 获取基础的缓存构造函数
*/
private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
try {
return cacheClass.getConstructor(String.class);
} catch (Exception e) {
throw new CacheException("Invalid base cache implementation (" + cacheClass + "). " +
"Base cache implementations must have a constructor that takes a String id as a parameter. Cause: " + e, e);
}
}
从这里可以看出,是获取到一个缓存类的一个带有String.class的构造参数,然后进行构建缓存信息。
缓存–设置缓存的参数
private void setCacheProperties(Cache cache) {
if (properties != null) {
// 根据setter方法获取需要传入的参数信息(只支持基础的包装类)
MetaObject metaCache = SystemMetaObject.forObject(cache);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String name = (String) entry.getKey();
String value = (String) entry.getValue();
if (metaCache.hasSetter(name)) {
Class<?> type = metaCache.getSetterType(name);
if (String.class == type) {
metaCache.setValue(name, value);
} else if (int.class == type
|| Integer.class == type) {
metaCache.setValue(name, Integer.valueOf(value));
} else if (long.class == type
|| Long.class == type) {
metaCache.setValue(name, Long.valueOf(value));
} else if (short.class == type
|| Short.class == type) {
metaCache.setValue(name, Short.valueOf(value));
} else if (byte.class == type
|| Byte.class == type) {
metaCache.setValue(name, Byte.valueOf(value));
} else if (float.class == type
|| Float.class == type) {
metaCache.setValue(name, Float.valueOf(value));
} else if (boolean.class == type
|| Boolean.class == type) {
metaCache.setValue(name, Boolean.valueOf(value));
} else if (double.class == type
|| Double.class == type) {
metaCache.setValue(name, Double.valueOf(value));
} else {
throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
}
}
}
}
// 如果实现了 InitializingObject 接口,会调用InitializingObject 的initialize()方法
if (InitializingObject.class.isAssignableFrom(cache.getClass())){
try {
((InitializingObject) cache).initialize();
} catch (Exception e) {
throw new CacheException("Failed cache initialization for '" +
cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
}
}
缓存 – 设置缓存的策略
// 实例化缓存的策略类
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
//获取缓存策略类中的包含Cache.class的构造函数
private Constructor<? extends Cache> getCacheDecoratorConstructor(Class<? extends Cache> cacheClass) {
try {
return cacheClass.getConstructor(Cache.class);
} catch (Exception e) {
throw new CacheException("Invalid cache decorator (" + cacheClass + "). " +
"Cache decorators must have a constructor that takes a Cache instance as a parameter. Cause: " + e, e);
}
}
从这里我们看到,对于缓存的策略,我们也可以自己单独的去实现,只需要在中注册对应的缓存策略,同时在的eviction中使用自己注册的策略即可
设置标准的缓存策略
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果清空时间段不为空,那么采用ScheduledCache来包装一层
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 如果读写标志为true,那么采用SerializedCache来包装一层
if (readWrite) {
cache = new SerializedCache(cache);
}
// 默认添加命中率日志的包装
cache = new LoggingCache(cache);
// 默认添加锁机制的包装
cache = new SynchronizedCache(cache);
// 如果阻塞获取为true,那么采用阻塞策略包装一层
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
从这段代码,我们可以知道,如果使用默认的缓存。那么缓存会实现一些默认的策略。会打印对应的命中率、保证线程安全性。