缓存是MyBatis中非常重要的特性。在应用程序和数据库都是单节点的情况下,合理使用缓存能够减少数据库IO,显著提升系统性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性问题。MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。本章我们就来学习一下MyBatis缓存的使用,并分析MyBatis缓存的实现原理。
MyBatis缓存的使用
MyBatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于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"/>
<setting name="lazyLoadingEnabled" value="false"/>
<setting name="multipleResultSetsEnabled" value="false"/>
<setting name="useColumnLabel" value="true"/>
</settings>
(2)在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,例如:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
(3)在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:
<select id="selectAllAuthors" flushCache="false" useCache="true"
resultType="org.apache.ibatis.domain.blog.Author">
select * from author
</select>
通过上面的配置,MyBatis的二级缓存就可以生效了。执行查询操作时,查询结果会缓存到二级缓存中,执行更新操作后,二级缓存会被清空。
MyBatis缓存实现类
了解了MyBatis缓存的使用后,我们再来学习MyBatis缓存的实现原理。MyBatis的缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。MyBatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:
public interface Cache {
/**
* 该方法用于获取缓存的Id,通常情况下缓存的Id为Mapper的命名空间名称。
*/
String getId();
/**
* 该方法用于将一个Java对象添加到缓存中
* @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);
/**
* 该方法用于清空缓存。
*/
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();
}
MyBatis中的缓存类采用装饰器模式设计,Cache接口有一个基本的实现类,即PerpetualCache类,该类的实现比较简单,通过一个HashMap实例存放缓存对象。需要注意的是,PerpetualCache类重写了Object类的equals()方法,当两个缓存对象的Id相同时,即认为缓存对象相同。另外,PerpetualCache类还重写了Object类的hashCode()方法,仅以缓存对象的Id作为因子生成hashCode。
除了基础的PerpetualCache类之外,MyBatis中为了对PerpetualCache类的功能进行增强,提供了一些缓存的装饰器类,如下图所示。
这些缓存装饰器类功能如下。
BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。
LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。
ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
下面是PerpetualCache类及MyBatis提供了缓存装饰类的使用案例:
void shouldDemonstrateEqualsAndHashCodeForVariousCacheTypes() {
PerpetualCache cache = new PerpetualCache("test_cache");
assertEquals(cache, cache);
assertEquals(cache, new SynchronizedCache(cache));
assertEquals(cache, new SerializedCache(cache));
assertEquals(cache, new LoggingCache(cache));
assertEquals(cache, new ScheduledCache(cache));
assertEquals(cache.hashCode(), new SynchronizedCache(cache).hashCode());
assertEquals(cache.hashCode(), new SerializedCache(cache).hashCode());
assertEquals(cache.hashCode(), new LoggingCache(cache).hashCode());
assertEquals(cache.hashCode(), new ScheduledCache(cache).hashCode());
Set<Cache> caches = new HashSet<>();
caches.add(cache);
caches.add(new SynchronizedCache(cache));
caches.add(new SerializedCache(cache));
caches.add(new LoggingCache(cache));
caches.add(new ScheduledCache(cache));
assertEquals(1, caches.size());
}
如上面的代码所示,我们可以使用MyBatis提供的缓存装饰器类对基础的PerpetualCache类的功能进行增强,使用不同的装饰器后,缓存对象则拥有对应的功能。
另外,MyBatis提供了一个CacheBuilder类,通过生成器模式创建缓存对象。下面是使用CacheBuilder构造缓存对象的案例:
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;
}
MyBatis一级缓存实现原理
MyBatis的一级缓存是SqlSession级别的缓存,在介绍MyBatis核心组件时,有提到过SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。
接下来我们了解一下MyBatis一级缓存的实现细节。一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性,代码如下:
public abstract class BaseExecutor implements Executor {
// 一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
其中,localCache属性用于缓存MyBatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。这两个属性在BaseExecutor构造方法中进行初始化,代码如下:
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
MyBatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象(CacheKey对象决定了缓存的Key与哪些因素有关系)。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象通过BaseExecutor类的createCacheKey()方法创建,代码如下:
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()); // Mapper Id
cacheKey.update(rowBounds.getOffset()); // 偏移量
cacheKey.update(rowBounds.getLimit()); // 数据条数
cacheKey.update(boundSql.getSql()); // SQL语句
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
MetaObject metaObject = null;
// 所有参数值
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 {
if (metaObject == null) {
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与下面这些因素有关:
- Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名。
- 查询结果的偏移量及查询的条数。
- 具体的SQL语句及SQL语句中需要传递的所有参数。
- MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。接下来我们看一下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 {
// 若缓存中获取不到,则调用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;
}
如上面的代码所示,在BaseExecutor类的query()方法中,首先根据缓存Key从localCache属性中查找是否有缓存对象,如果查找不到,则调用queryFromDatabase()方法从数据库中获取数据,然后将数据写入localCache对象中。如果localCache中缓存了本次查询的结果,则直接从缓存中获取。
需要注意的是,如果localCacheScope属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。除此之外,MyBatis会在执行完任意更新语句后清空缓存,我们可以看一下BaseExecutor类的update()方法,代码如下:
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()方法清空缓存。
注意:在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
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;
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);
}
return (Executor) interceptorChain.pluginAll(executor);
}
如上面的代码所示,Configuration类的newExecutor()工厂方法的逻辑比较简单,根据defaultExecutorType参数指定的Executor类型创建对应的Executor实例。如果cacheEnabled属性值为true(开启了二级缓存),则使用CachingExecutor对普通的Executor对象进行装饰,CachingExecutor在普通Executor的基础上增加了二级缓存功能,我们可以重点关注一下CachingExecutor类的实现。下面是CachingExecutor类的属性信息:
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
如上面的代码所示,CachingExecutor类中维护了一个TransactionalCacheManager实例,TransactionalCacheManager用于管理所有的二级缓存对象。TransactionalCacheManager类的实现如下:
public class TransactionalCacheManager {
// 通过HashMap对象维护二级缓存对应的TransactionalCache实例
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
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) {
// 从transactionalCaches中获取对应的TransactionalCache对象,如果获取不到,则创建并添加到transactionalCaches中
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
如上面的代码所示,在TransactionalCacheManager类中,通过一个HashMap对象维护所有二级缓存实例对应的TransactionalCache对象,在TransactionalCacheManager类的getObject()方法和putObject()方法中都会调用getTransactionalCache()方法获取二级缓存对象对应的TransactionalCache对象,然后对TransactionalCache对象进行操作。在getTransactionalCache()方法中,首先从HashMap对象中获取二级缓存对象对应的TransactionalCache对象,如果获取不到,则创建新的TransactionalCache对象添加到HashMap对象中。
接下来以查询操作为例介绍二级缓存的工作机制。下面是CachingExecutor的query()方法的实现:
@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);
// 从MappedStatement对象对应的二级缓存中获取数据
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果缓存数据不存在,则从数据库查询数据
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将数据存放到MappedStatement对象对应的二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如上面的代码所示,在CachingExecutor的query()方法中,首先调用createCacheKey()方法创建缓存Key对象,然后调用MappedStatement对象的getCache()方法获取MappedStatement对象中维护的二级缓存对象。然后尝试从二级缓存对象中获取结果,如果获取不到,则调用目标Executor对象的query()方法从数据库获取数据,再将数据添加到二级缓存中。当执行更新语句后,同一命名空间下的二级缓存将会被清空。下面是CachingExecutor的update()方法的实现:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
如上面的代码所示,CachingExecutor的update()方法中会调用flushCacheIfRequired()方法确定是否需要刷新缓存,该方法代码如下:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
在flushCacheIfRequired()方法中会判断<select|update|delete|insert>标签的flushCache属性,如果属性值为true,就清空缓存。<select>标签的flushCache属性值默认为false,而<update|delete|insert>标签的flushCache属性值默认为true。
最后,我们回顾一下MappedStatement对象创建过程中二级缓存实例的创建。XMLMapperBuilder在解析Mapper配置时会调用cacheElement()方法解析<cache>标签,cacheElement()方法代码如下:
private void cacheElement(XNode context) {
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);
}
}
如上面的代码所示,在获取<cache>标签的所有属性信息后,调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例,然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中。下面是创建MappedStatement对象的关键代码:
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
MyBatis使用Redis缓存
MyBatis除了提供内置的一级缓存和二级缓存外,还支持使用第三方缓存(例如Redis、Ehcache)作为二级缓存。本节我们就来了解一下在MyBatis中如何使用Redis作为二级缓存以及它的实现原理。
MyBatis官方提供了一个mybatis-redis模块,该模块用于整合Redis作为二级缓存。使用该模块整合缓存,首先需要引入该模块的依赖,如果项目通过Maven构建,则只需要向pom.xml文件中添加如下内容:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta3-SNAPSHOT</version>
</dependency>
然后需要在Mapper的XML配置文件中添加缓存配置,例如:
<cache type="org.mybatis.caches.redis.RedisCache" flushInterval="60000" size="512" readOnly="true"/>
最后,需要在classpath下新增redis.properties文件,配置Redis的连接信息。下面是redis.properties配置案例:
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.soTimeout=5000
redis.password=
redis.database=0
redis.clientName=
#serializer type(kryo or jdk with jdk being default)
redis.serializer=jdk
接下来我们简单地了解一下mybatis-redis模块的实现。该模块提供了一个比较核心的缓存实现类,即RedisCache类。RedisCache实现了Cache接口,使用Jedis客户端操作Redis,在RedisCache构造方法中建立与Redis的连接,代码如下:
public final class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
private String id;
private static JedisPool pool;
private final RedisConfig redisConfig;
private Integer timeout;
public RedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(),
redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName(),
redisConfig.isSsl(), redisConfig.getSslSocketFactory(), redisConfig.getSslParameters(),
redisConfig.getHostnameVerifier());
}
在RedisCache构造方法中,首先获取RedisConfigurationBuilder对象,将redis.properties文件中的配置信息转换为RedisConfig对象,RedisConfig类是描述Redis配置信息的Java Bean。获取RedisConfig对象后,接着创建JedisPool对象,通过JedisPool对象与Redis服务器建立连接。
RedisCache使用Redis的Hash数据结构存放缓存数据。在RedisCache类的putObject()方法中,首先对Java对象进行序列化,mybatis-redis模块提供了两种序列化策略,即JDK内置的序列化机制和第三方序列化框架Kryo,具体使用哪种序列化方式,可以在redis.properties文件中配置。对象序列化后,将序列化后的信息存放在Redis中。RedisCache类的putObject()方法实现如下:
@Override
public void putObject(final Object key, final Object value) {
execute(new RedisCallback() {
@Override
public Object doWithRedis(Jedis jedis) {
final byte[] idBytes = id.getBytes();
jedis.hset(idBytes, key.toString().getBytes(), redisConfig.getSerializer().serialize(value));
if (timeout != null && jedis.ttl(idBytes) == -1) {
jedis.expire(idBytes, timeout);
}
return null;
}
});
}
在RedisCache类的getObject()方法中,先根据Key获取序列化的对象信息,再进行反序列化操作,代码如下:
@Override
public Object getObject(final Object key) {
return execute(new RedisCallback() {
@Override
public Object doWithRedis(Jedis jedis) {
return redisConfig.getSerializer().unserialize(jedis.hget(id.getBytes(), key.toString().getBytes()));
}
});
}
需要注意的是,使用Redis作为二级缓存,需要通过<cache>标签的type属性指定缓存实现类为org.mybatis.caches.redis.RedisCache。MyBatis启动时会解析Mapper配置信息,为每个命名空间创建对应的RedisCache实例,由于JedisPool实例是RedisCache类的静态属性,因此JedisPool实例是所有RedisCache对象共享的。RedisCache的完整源码读者可参考mybatis-redis模块。
除了Redis外,MyBatis还提供了整合其他缓存的适配器。例如,ehcache-cache项目用于整合EhCache缓存,oscache-cache项目用于整合OSCache缓存,memcached-cache项目用于整合Memcached缓存。