1、概述
缓存是Mybatis中非常重要的特性。应用程序和数据库都是单节点的情况下,合理使用缓存能够减少数据库IO,显著提升性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性的问题。Mybatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。
2、Mybatis缓存的使用
Mybatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,Mybatis核心开发人员做出了解释:Mybatis的一些关键特性(例如通过association和collection建立级联映射、避免循环引用,加速重复嵌套查询等)都是基于Mybatis一级缓存实现的,而且Mybatis结果集映射相关代码重度依赖CacheKey,所以目前Mybatis不支持关闭一级缓存。
Mybatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
Mybatis的一级缓存,用户只能控制缓存的级别,并不能关闭。
Mybatis二级缓存的使用需要以下几步:
(1)、在Mybatis主配置文件中指定cacheEnabled属性值为true。
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
(2)、在Mybatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,例如
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
(3)、在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存,另外,还可以通过flushCache属性指定Mapper执行后属否刷新缓存。
<select id="getUserById"
resultType="com.matrix.dao.User"
resultMap="user"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
SELECT <include refid="userAllColumn"/> FROM user WHERE id = #{id}
</select>
通过上面的配置,Mybatis的二级缓存就可以生效了,执行查询操作时,查询结果会缓存到二级缓存中,执行更新操作后,二级缓存会被清空。
3、Mybatis缓存实现类
Mybatis缓存的实现原理,Mybatis的缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。Mybatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
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);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
(1)、getId():该方法用于获取缓存的Id,通常情况下缓存的Id为Mapper的命名空间名称。
(2)、putObject():该方法用于将一个Java对象添加到缓存中,该方法有两个参数,第一个参数为缓存的key,即Cachekey的实例;第二个参数为需要缓存的对象。
(3)、getObject():该方法用于获取缓存Key对应的缓存对象。
(4)、removeObject():该方法用于讲一个对象从缓存中移除。
(5)、clear():该方法用于清空缓存。
(6)、getReadWritedlock():该方法返回一个ReadWriteLock对象。该方法已经被弃用。
Mybatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的ID相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象相同的ID作为因子生成hashCode。
除了基础的PerpetualCache类之外,Mybatis中为了对PerpetualCache类的功能进行增强,提供了一些缓存的装饰类。
这些缓存装饰器类功能如下:
(1)、BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的key对应的数据。
(2)、FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
(3)、LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志给出缓存命中率。
(4)、LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和value,LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将key值保存在LruCache类的eldestKey属性中,然后当缓存中添加对象时,淘汰eldestKey对应的value值。
(5)、ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存,清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
(6)、SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
(7)、SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
(8)、SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
(9)、TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback(),当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
(10)、WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
4、Mybatis一级缓存实现原理
Mybatis的一级缓存是SqlSession级别的缓存,在介绍Mybatis核心组件时,有提到过SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模版方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。
一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性,代码如下:
// Mybatis一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
其中,localCache属性用于缓存Mybatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。这两个属性在BaseExecutor构造方法中进行初始化,代码如下:
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
Mybatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象通过BaseExecutor类的createCacheKey()方法创建。
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
从上面的代码可以看出,缓存的key与下面这些因素有关:
(1)、Mapper的ID,即Mapper命名空间与select|update|insert|delete标签的ID组成的全局限定名。
(2)、查询结果的偏移量及查询的条数
(3)、具体的SQL语句及SQL语句中需要传递的所有参数。
(4)、Mybatis主配置文件中,通过environment标签配置的环境信息对应的ID属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为这两次查询执行的是相同的SQL语句,缓存才会生效。
@Override
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 {
// 若缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
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;
}
需要注意的是,如果localCacheStatement属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。除此之外,Mybatis会在执行完任意更新语句后清空缓存。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
可以看到,Mybatis在调用doUpdate()方法完成更新操作之前,首先会调用clearLocalCache()方法清空缓存。
5、Mybatis二级缓存实现原理
Mybatis二级缓存默认情况下是关闭的,因此需要通过设置cacheEnabled参数值为true来开启二级缓存。
SqlSession将执行Mapper的逻辑委托给Executor组件完成,而Executor接口有几种不同的实现,分别为SimpleExecutor、BatchExecutor、ReuseExecutor。另外,还有一个比较特殊的CachingExecutor,CachingExecutor用到了装饰器模式,在其他几种Executor的基础上增加了二级缓存功能。
Executor实例采用工厂模式创建,Configuration类提供了一个工厂方法newExecutor(),该方法返回一个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);
}
// 如果cacheEnabled属性为true,则使用CachingExecutor对Executor进行装饰。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor在普通的Executor的基础上增加了二级缓存的功能,CachingExecutor类的属性信息:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
CachingExecutor类中维护了一个TransactionalCacheManager实例,TransactionalCacheManager用于管理所有的二级缓存对象。TransactionalCacheManager类的实现如下:
public class TransactionalCacheManager {
// 通过HashMap对象维护二级缓存对应的TransactionalCache实例。
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
// 获取二级缓存对应的TransactionalCache对象,然后根据缓存Key获取缓存对象。
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
// 获取二级缓存对应的TransactionalCache对象
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// 如果获取不到,则创建,然后添加到Map中
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
二级缓存的工作机制:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 调用createCacheKey()方法创建缓存Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取MappedStatement对象中维护的二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否需要刷新二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从MappedStatement对象对应的二级缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果缓存数据不存在,则从数据库中查询数据
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将数据存放到MappedStatement对象对应的二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MappedStatement对象创建过程中二级缓存实例的创建。XMLMapperBuilder在解析Mapper配置时会调用cacheElement()方法解析cache标签cacheElement方法解析cache标签。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);
}
}
最后通过useNewCache方法来创建Cache。